Skip to content

Commit 6d23850

Browse files
committed
actually generate linked files
1 parent 68f8221 commit 6d23850

File tree

1 file changed

+81
-35
lines changed

1 file changed

+81
-35
lines changed

bapctools/generate.py

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ def resolve_path(path_str: str, *, allow_absolute: bool, allow_relative: bool) -
117117
return Path("generators") / path
118118

119119

120+
def is_local_symlink(file: Path) -> bool:
121+
if not file.is_symlink():
122+
return False
123+
dest = file.readlink()
124+
if dest.parent != Path():
125+
return False
126+
if file.name.split(".", 1)[0] != dest.name.split(".", 1)[0]:
127+
return False
128+
if "".join(file.suffixes) not in config.KNOWN_DATA_EXTENSIONS:
129+
return False
130+
return True
131+
132+
120133
# An Invocation is a program with command line arguments to execute.
121134
# The following classes inherit from Invocation:
122135
# - GeneratorInvocation
@@ -727,7 +740,7 @@ def __init__(
727740

728741
def _has_required_in(t, infile: Path) -> bool:
729742
for required in t.required_in:
730-
if all(infile.with_suffix(ext).is_file() or ext in t.linked for ext in required):
743+
if all(infile.with_suffix(ext).is_file() for ext in required):
731744
return True
732745
return False
733746

@@ -1020,6 +1033,21 @@ def check_deterministic(force: bool = False) -> None:
10201033
# clean up
10211034
shutil.rmtree(tmp)
10221035

1036+
def generate_linked(type: str) -> bool:
1037+
# cache entries are already set in generate_from_rule
1038+
for source_ext, target_ext in t.linked.items():
1039+
source_type = source_ext.split(".", 2)[1]
1040+
if source_type != type:
1041+
continue
1042+
source = infile.with_suffix(source_ext)
1043+
target = infile.with_suffix(target_ext)
1044+
if not target.is_file():
1045+
bar.error(
1046+
f"link {source_ext[1:]}->{target_ext[1:]} is invalid since {target_ext[1:]} was not generated"
1047+
)
1048+
ensure_symlink(source, target, relative=True)
1049+
return True
1050+
10231051
def generate_from_rule() -> bool:
10241052
nonlocal meta_yaml
10251053

@@ -1029,6 +1057,8 @@ def generate_from_rule() -> bool:
10291057
rule_hashes["source_hash"] = t.hash
10301058
for ext, string in t.hardcoded.items():
10311059
rule_hashes["hardcoded_" + ext[1:]] = hash_string(string)
1060+
for link, target in t.linked.items():
1061+
rule_hashes["linked_" + link[1:]] = hash_string(target)
10321062
if t.generator:
10331063
rule_hashes["generator_hash"] = t.generator.hash(seed=t.seed)
10341064
rule_hashes["generator"] = t.generator.cache_command(seed=t.seed)
@@ -1049,47 +1079,57 @@ def generate_from_rule() -> bool:
10491079

10501080
# Step 2: Copy `copy:` files for all known extensions.
10511081
if t.copy:
1052-
# We make sure to not silently overwrite changes to files in data/
1053-
# that are copied from generators/.
10541082
copied = False
10551083
for ext in config.KNOWN_DATA_EXTENSIONS:
10561084
ext_file = t.copy.with_suffix(ext)
1057-
if ext_file.is_file():
1058-
shutil.copy(ext_file, infile.with_suffix(ext), follow_symlinks=True)
1085+
file = infile.with_suffix(ext)
1086+
if is_local_symlink(ext_file):
1087+
dest_ext = "".join(ext_file.readlink().suffixes)
1088+
ensure_symlink(file, infile.with_suffix(dest_ext), relative=True)
1089+
copied = True
1090+
elif ext_file.is_file():
1091+
shutil.copy(ext_file, file, follow_symlinks=True)
10591092
copied = True
10601093
if not copied:
10611094
bar.warn(f"No files copied from {t.copy}.")
10621095

10631096
# Step 3: Write hardcoded files.
1064-
# Note: we cannot generate links yet, since files like .ans are not yet generated
10651097
for ext, contents in t.hardcoded.items():
1098+
file = infile.with_suffix(ext)
1099+
if file.exists():
1100+
file.unlink()
10661101
# substitute in contents? -> No!
1067-
infile.with_suffix(ext).write_text(contents)
1102+
file.write_text(contents)
1103+
1104+
# Step 4: Write linked files
1105+
# Note: we cannot generate all links yet, since .ans files are not yet generated
1106+
if not generate_linked("in"):
1107+
return False
10681108

1069-
# Step 4: Error if infile was not generated.
1109+
# Step 5: Error if infile was not generated.
10701110
if not t._has_required_in(infile):
10711111
msg = ", ".join(" and ".join(required) for required in t.required_in)
10721112
bar.error(f"No {msg} file was generated!")
10731113
return False
10741114

1075-
# Step 5: save which files where generated
1115+
# Step 6: save which files where generated
10761116
meta_yaml.generated_extensions = [
10771117
ext for ext in config.KNOWN_DATA_EXTENSIONS if infile.with_suffix(ext).is_file()
10781118
]
10791119

1080-
# Step 6: update cache
1120+
# Step 7: update cache
10811121
meta_yaml.rule_hashes = rule_hashes
10821122
meta_yaml.write()
10831123

1084-
# Step 7: check deterministic:
1124+
# Step 8: check deterministic:
10851125
check_deterministic(True)
10861126
else:
10871127
check_deterministic(False)
10881128

10891129
assert t._has_required_in(infile), f"Failed to generate in file: {infile.name}"
10901130
return True
10911131

1092-
def check_match(testcase: Testcase, ext: str, bar: ProgressBar) -> None:
1132+
def check_match(testcase: Testcase, ext: str) -> bool:
10931133
nonlocal meta_yaml
10941134

10951135
updated = False
@@ -1106,7 +1146,9 @@ def check_match(testcase: Testcase, ext: str, bar: ProgressBar) -> None:
11061146
if name not in cache:
11071147
if text is None:
11081148
file = testcase.in_path.with_suffix(f".{ext}")
1109-
assert file.is_file()
1149+
if not file.is_file():
1150+
bar.error(f"Invalid match entry, {ext} was not generated")
1151+
return False
11101152
text = file.read_text()
11111153
match = pattern.search(text)
11121154
cache[name] = f"[{match.start()}, {match.end()})" if match else None
@@ -1115,10 +1157,11 @@ def check_match(testcase: Testcase, ext: str, bar: ProgressBar) -> None:
11151157
if cache[name]:
11161158
bar.debug(f"Found match for '{name}'': {cache[name]}")
11171159
else:
1118-
bar.warn(f"Found not match for '{name}'")
1160+
bar.warn(f"Found no match for '{name}'")
11191161

11201162
if updated:
11211163
meta_yaml.write()
1164+
return True
11221165

11231166
def generate_from_solution(testcase: Testcase) -> bool:
11241167
nonlocal meta_yaml
@@ -1314,20 +1357,14 @@ def generate_empty_interactive_sample_ans() -> bool:
13141357
return True
13151358
if not problem.interactive and not problem.multi_pass:
13161359
return True
1317-
for ext in ["", ".statement", ".download"]:
1318-
ans_ext_file = infile.with_suffix(f".ans{ext}")
1319-
if ans_ext_file.exists():
1320-
return True
1321-
if infile.with_suffix(f".in{ext}").exists():
1322-
ans_ext_file.write_text("")
1323-
return True
1360+
assert infile.is_file()
1361+
if not ansfile.is_file():
1362+
ansfile.write_text("")
13241363
return True
13251364

13261365
def warn_override() -> None:
13271366
def find_override(*exts: str) -> list[str]:
1328-
found = [
1329-
ext for ext in exts if infile.with_suffix(ext).is_file() or ext in t.linked
1330-
]
1367+
found = [ext for ext in exts if infile.with_suffix(ext).is_file()]
13311368
if len(found) > 1:
13321369
bar.warn(f"There should be at most one of {', '.join(found)}")
13331370
return found
@@ -1353,10 +1390,11 @@ def copy_generated() -> None:
13531390
source = infile.with_suffix(ext)
13541391
target = target_infile.with_suffix(ext)
13551392

1356-
if ext in t.linked:
1393+
if is_local_symlink(source):
13571394
generator_config.known_files.add(target)
1358-
dest = target_infile.with_suffix(t.linked[ext])
1359-
if not dest.is_file():
1395+
dest_ext = "".join(source.readlink().suffixes)
1396+
dest = target_infile.with_suffix(dest_ext)
1397+
if not source.is_file():
13601398
bar.warn(
13611399
f"{target.name}->{dest.name} is broken since {dest.name} was not generated"
13621400
)
@@ -1465,31 +1503,39 @@ def add_test_case_to_cache() -> None:
14651503
return
14661504

14671505
# Step 3.1: check patterns
1468-
check_match(testcase, "in", bar)
1506+
if not check_match(testcase, "in"):
1507+
return
14691508

14701509
# Step 4: generate .ans and .interaction if needed
14711510
if not generate_from_solution(testcase):
14721511
return
1512+
# Step 4.1: for interactive and/or multi-pass samples, generate empty .ans if it does not exist
1513+
if not generate_empty_interactive_sample_ans():
1514+
return
1515+
# Step 4.2: link ans files
1516+
if not generate_linked("ans"):
1517+
return
14731518

14741519
# Step 5: validate .ans (and .out if it exists)
14751520
if not t.validate_ans_and_out(problem, testcase, meta_yaml, bar):
14761521
return
14771522

14781523
# Step 5.1: check patterns
1479-
check_match(testcase, "ans", bar)
1524+
if not check_match(testcase, "ans"):
1525+
return
14801526

14811527
# Step 6: generate visualization if needed
14821528
if not generate_visualization(testcase):
14831529
return
1530+
else:
1531+
# Step 4.2: link ans files (This is independent of the infile)
1532+
if not generate_linked("ans"):
1533+
return
14841534

1485-
# Step 7: for interactive and/or multi-pass samples, generate empty .ans if it does not exist
1486-
if not generate_empty_interactive_sample_ans():
1487-
return
1488-
1489-
# Step 8: warn if statement/download files are inconsistent
1535+
# Step 7: warn if statement/download files are inconsistent
14901536
warn_override()
14911537

1492-
# Step 9: copy and link all generated files
1538+
# Step 8: copy all generated files
14931539
copy_generated()
14941540

14951541
# Note that we set this to true even if not all files were overwritten -- a different log/warning message will be displayed for that.

0 commit comments

Comments
 (0)