Count unique archives when estimating extraction space

Adapted from upstream PR #277 (841684012800327d6d6fa6b6fd79d3ab689c9751).
This commit is contained in:
Erick Sanchez committed 2026-10-26 11:10:48 -06:00
1 parent 8d29a31d2b
commit 94dfd1655e
2 files changed
+38 -1

No files matched your search

+33
View File
@@ -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()
+5 -1
View File
@@ -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)