diff --git a/.github/workflows/source-regressions.yml b/.github/workflows/source-regressions.yml index ce19daf1e..73beaf2d3 100644 --- a/.github/workflows/source-regressions.yml +++ b/.github/workflows/source-regressions.yml @@ -6,6 +6,7 @@ on: paths: - nx2/nup.py - nx2/make_game.py + - tests/test_copy_writable.py - nx2/build.py - nx2/unpack.py - third_party/a64-kit/tools/toolchain.py @@ -124,6 +125,7 @@ on: paths: - nx2/nup.py - nx2/make_game.py + - tests/test_copy_writable.py - nx2/build.py - nx2/unpack.py - third_party/a64-kit/tools/toolchain.py @@ -350,6 +352,8 @@ jobs: run: python -I -B tests/test_table_track.py - name: Check descriptor timing stays off run: python -I -B tests/test_desc_clock.py + - name: Check the work folder's copies stay writable + run: python -I -B tests/test_copy_writable.py - name: Check a mesh program's stage buffer starts at zero run: python -I -B tests/test_mesh_stage_buffer_zero.py - name: Check a mesh program with control topology 1 draws points diff --git a/nx2/make_game.py b/nx2/make_game.py index 00c9e8609..9cf8693f1 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -595,6 +595,15 @@ def profile_readable(prof): return r.returncode == 0 +def copy_writable(src, dst): + """src's bytes at dst, writable whatever src's mode: copy2 took the mode along, and from a read-only install (a Nix + store, a packaged tools folder) the copies in the work folder were read-only, so lift.py could not write chunks.txt + again (PermissionError) and every later build failed on the same file""" + if os.path.exists(dst): + os.chmod(dst, stat.S_IREAD | stat.S_IWRITE) + shutil.copyfile(src, dst) + + def link_or_copy(src, dst): """the romfs by hard links where the disk allows: 26 GB not written twice""" files = [os.path.join(d, f) for d, _, fs in os.walk(src) for f in fs] @@ -715,11 +724,11 @@ def main(): dst = os.path.join(work, name) # (copied only when it differs: a compiler still ending from a cancelled build keeps the profile mapped, WinError 1224) if os.path.exists(src) and not (os.path.exists(dst) and filecmp.cmp(src, dst, shallow=False)): - shutil.copy2(src, dst) + copy_writable(src, dst) os.makedirs(os.path.join(work, "image"), exist_ok=True) src = os.path.join(ROOT, "work", "image", "chunks.txt") # (where the lifted code's files begin: addresses) if os.path.exists(src): - shutil.copy2(src, os.path.join(work, "image", "chunks.txt")) + copy_writable(src, os.path.join(work, "image", "chunks.txt")) env = {"OOT_WORK": work, "OOT_EXEFS": exefs, "OOT_ROMFS": romfs, "OOT_DUMP": out, "RX_RELEASE": "1", "A64_HOT_CHUNK": "1"} if os.path.exists(os.path.join(work, "hot_funcs.txt")): env["A64_HOT_FUNCS"] = os.path.join(work, "hot_funcs.txt") diff --git a/tests/test_copy_writable.py b/tests/test_copy_writable.py new file mode 100644 index 000000000..59f26c9d1 --- /dev/null +++ b/tests/test_copy_writable.py @@ -0,0 +1,42 @@ +"""make_game copies the profile files from a read-only install as writable files. + +From a read-only tools folder (a Nix store, a packaged install) shutil.copy2 took the read-only mode along, and lift.py +then failed to write work/image/chunks.txt (PermissionError), on that build and every later one. +python -I -B tests/test_copy_writable.py +""" +import importlib.util +import os +from pathlib import Path +import stat +import tempfile + +SOURCE = Path(__file__).resolve().parents[1] / "nx2" / "make_game.py" +spec = importlib.util.spec_from_file_location("make_game_under_test", SOURCE) +make_game = importlib.util.module_from_spec(spec) +spec.loader.exec_module(make_game) + + +def main(): + text = SOURCE.read_text(encoding="utf-8") + if 'shutil.copy2(src, os.path.join(work, "image", "chunks.txt"))' in text: + raise SystemExit("chunks.txt is copied with its source's mode again") + with tempfile.TemporaryDirectory(prefix="hookshot-copy-test-") as d: + src, dst = os.path.join(d, "src.txt"), os.path.join(d, "dst.txt") + with open(src, "w") as f: + f.write("new") + with open(dst, "w") as f: + f.write("old") + os.chmod(src, stat.S_IREAD) + os.chmod(dst, stat.S_IREAD) # (a copy left read-only by an earlier build) + make_game.copy_writable(src, dst) + with open(dst, "a") as f: # (what lift.py does to chunks.txt) + f.write("+") + with open(dst) as f: + if f.read() != "new+": + raise SystemExit("the copy does not hold the source's bytes") + os.chmod(src, stat.S_IREAD | stat.S_IWRITE) + print("PASS: the work folder's copies stay writable") + + +if __name__ == "__main__": + main()