From 94dfd1655efffc52a57e76130f11b6e0af0f8dd8 Mon Sep 17 00:00:00 2001 From: Erick Sanchez <7268597+nalancer08@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:10:48 -0600 Subject: [PATCH] Count unique archives when estimating extraction space Adapted from upstream PR #277 (841684012800327d6d6fa6b6fd79d3ab689c9751). --- dm/tools/tests/test_nsp_space.py | 33 ++++++++++++++++++++++++++++++++ scripts/build_ipa_from_nsp.py | 6 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 dm/tools/tests/test_nsp_space.py diff --git a/dm/tools/tests/test_nsp_space.py b/dm/tools/tests/test_nsp_space.py new file mode 100644 index 0000000..65d2e0a --- /dev/null +++ b/dm/tools/tests/test_nsp_space.py @@ -0,0 +1,33 @@ +"""NSP extraction space estimate: distinct archives count even when they have the same size; the shared base counts once. +No game files, keys or Xcode needed (temporary synthetic files only).""" +import importlib.util +from pathlib import Path +import tempfile +import unittest + +REPO = Path(__file__).resolve().parents[3] +spec = importlib.util.spec_from_file_location('build_ipa_from_nsp', REPO / 'scripts/build_ipa_from_nsp.py') +nsp = importlib.util.module_from_spec(spec); spec.loader.exec_module(nsp) + + +class Space(unittest.TestCase): + def test_equal_sized_archives(self): + with tempfile.TemporaryDirectory() as tmp: + base, update = Path(tmp) / 'base.nsp', Path(tmp) / 'update.nsp' + base.write_bytes(b'base'); update.write_bytes(b'updt') + self.assertEqual(nsp.picked_nsp_bytes({'v1': (base, update)}), 8) + + def test_shared_base(self): + with tempfile.TemporaryDirectory() as tmp: + base, first, second = [Path(tmp) / name for name in ('base.nsp', 'first.nsp', 'second.nsp')] + base.write_bytes(b'base'); first.write_bytes(b'first'); second.write_bytes(b'second') + self.assertEqual(nsp.picked_nsp_bytes({'v0': (base, None), 'v1': (base, first), 'v2': (base, second)}), 15) + + def test_base_without_update(self): + with tempfile.TemporaryDirectory() as tmp: + base = Path(tmp) / 'base.nsp'; base.write_bytes(b'base') + self.assertEqual(nsp.picked_nsp_bytes({'v0': (base, None)}), 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/build_ipa_from_nsp.py b/scripts/build_ipa_from_nsp.py index c5a189a..66a2794 100755 --- a/scripts/build_ipa_from_nsp.py +++ b/scripts/build_ipa_from_nsp.py @@ -490,6 +490,10 @@ def pick_nsps(fam, have, versions): return out +def picked_nsp_bytes(picks): # count each archive once, not each distinct size (the base occurs in every version) + return sum(p.stat().st_size for p in {p for base, update in picks.values() for p in (base, update) if p is not None}) + + def can_clone(d): # whether this volume supports APFS clones (nx_extract uses them to save space; without them it writes full copies) a, b = d / '.clone_test', d / '.clone_test2' try: @@ -1750,7 +1754,7 @@ def main(args=None): picks = pick_nsps(fam, have, versions) ex = work / 'extract'; ex.mkdir(parents=True, exist_ok=True); (work / 'tmp').mkdir(exist_ok=True) - gb = sum({p.stat().st_size for b, u in picks.values() for p in (b, u) if p}) / 2**30 + 6 # extraction + shader cache + slack + gb = picked_nsp_bytes(picks) / 2**30 + 6 # extraction + shader cache + slack if free_gb(work) < gb and not all((ex / v).exists() for v in versions): raise BuildError(f'less than {gb:.0f} GB free on the volume of {work}') heapck = provisional and HEAP_CHECK and not args.no_heap_check and not heap_checked(g) # the heap check, when the game needs one (once per provisional profile: it stamps the profile) PROG.plan(1 + len(versions) + bool(fam['dlc']) + 4 - (prepare_xcode or (mac and not mac_pack))) # prepare Xcode (a Mac-only build: the Mac host) replaces app build + IPA pack with one stage (a Mac app: Mac host + Mac app)