fix: keep interrupted ROMFS copies out of the destination #1395
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1395
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/romfs-interrupted-copy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When the Builder receives extracted ROMFS folders on a different drive, a hard link fails and it copies each asset. An interrupted copy leaves a partial file at the final path. A retry skips that path because it already exists.
Stage each fallback copy beside its destination and move it into place after the copy and metadata update succeed. Clean up failed stages, including copies that inherited a read-only attribute on Windows. Existing destination assets still follow the Builder's resume policy.
Game-free fixtures simulate a cross-volume disk-full error, then retry and check the complete asset and an existing destination file. A second test simulates a denied replacement of a read-only copy. All ten cases in
tests/test_make_game_cache.pypassed on macOS. Windows execution is pending CI.AI assistance from OpenAI Codex is recorded in the commit trailers.
This landed in the v2.0.12 branch (merged there for testing; it reaches
mainwith that release), so closing. Thanks!Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise.
Nice, small fix, and the approach is right: staging in the destination's own directory keeps the rename on one volume, so it's atomic on both Linux and Windows. os.replace onto an existing file also works on Windows (the exists() guard means it should never have to). mkstemp is O_EXCL so name collisions aren't a concern, the BaseException cleanup covers Ctrl-C, the read-only chmod before unlink on nt is correct, and the tests are picked up by run_all.py's tests/test_*.py glob. Windows CI is green; the ubuntu job was still pending when I looked.
One thing I'd like handled (inline): the cleanup only runs if the process gets to run Python again. The scenario in the description is an interrupted copy, and for a kill/power loss/crash, the
.romfs-*stage is left in the romfs tree forever (retry skips it, nothing sweeps it). It's a full-size asset sitting in the game's romfs, and anything that walks that tree will see it. A sweep of stale.romfs-*files at the start of link_or_copy (or in the retry path) would close that.Nit: no fsync before the replace, so after a power loss the final name can exist with empty or partial data on some filesystems. Probably not worth the cost for 26 GB, just noting.
Overlap with the sibling PRs: #1393 also edits nx2/make_game.py main() (the check_cache_paths/link_or_copy call site, a different hunk from yours) and tests/test_make_game_cache.py; #1397 edits the same test file with a hunk right at the end of file, next to where you add RomfsCopyTests, so expect a textual conflict in the tests whichever lands second. #1391 and #1412 only touch nx2/nup.py and nx2/test_nup_paths.py, so no overlap. No settings option ids involved.
@ -826,1 +826,3 @@shutil.copy2(s, t)# A retry skips existing targets, so publish the copy only after it is complete.fd, staged = tempfile.mkstemp(prefix=".romfs-", dir=os.path.dirname(t))try:If the process dies here (SIGKILL, power loss, OOM) the
.romfs-XXXXstage is never removed, and the next run won't touch it. Consider deleting leftover.romfs-*files under dst before the loop starts.