On this page
Build tools
Homebrew .NET is on PATH but apphosts cannot find the runtime
- Symptom: the first extraction stops with
nx_extract exit code 131and.NET location: Not found, even thoughdotnetis onPATH. The shader translator can fail the same way. - Cause: standalone .NET apphosts search runtime roots and registered installations, not
PATH(.NET runtime discovery). Homebrew's formula installs the runtime underlibexecand itsbin/dotnetcan be a wrapper that setsDOTNET_ROOTonly for its own child (Homebrew caveat). Resolving that wrapper's symlink givesbin, not the runtime root. - Fix:
scripts/build_ipa_from_nsp.pyconfigure_dotnetderives the root from theMicrosoft.NETCore.Appentry indotnet --list-runtimes, using the build's environment, before any extraction or survey. It exportsDOTNET_ROOTboth to that environment and toos.environ, for tools launched with an explicit environment and those that inherit it. Caller-supplied roots, including architecture-specific overrides, retain precedence. A failed or malformed probe leaves the environment unchanged rather than guessing the wrapper's directory. - Found on: Balatro, first extraction with Homebrew .NET (PR #284). Applies to every game's build on that installation setup.
The shader step stops with an empty xlate_source
- Symptom: the extraction and captures succeed, then the shader step stops with
translation chain tools do not match the lock file: xlate_source: lock … / actualand nothing afteractual, which reads like a stale translator. - Cause: the translator (
dmsh_xlate_bin/dmsh_xlate) is a universal framework-dependent apphost. When it finds no .NET 10 runtime for the slice it starts as, it exits 131 with an empty stdout. A Python under Rosetta (an Intel Homebrew's) starts it as x86_64, and it finds only the arm64 .NET; extraction is not affected, becausenx_extractis built for the SDK's own architecture only. An Intel .NET on Apple silicon, or aDOTNET_ROOTthat names one, fails the same way.tools_now(then indmsh_chain.py) kept only the stdout ofdmsh_xlate --source-sha, without its exit code or stderr, so the apphost's own message was lost. - Fix: the NSP script checks the environment before extracting anything:
check_native_pythonstops under Rosetta (sysctl.proc_translated), andcheck_prereqsstarts the translator once with--source-shaand stops with its first message line (check_translator). It also checks glslang and, for an iPhone build, the iOS SDK (check_ios_sdk, before the Metal check, whose message would blame the Metal toolchain).own_compilers.pyno longer asks a Mac-only shader build (DMSH_TARGETS=mac) for the iOS Metal compiler.tool_lock.pyrecords--source-sha's failure instead of its empty stdout (xlate_embedded:does not run (<first message line>)), sodmsh_build.pyrun directly and--update-lockstop with the same message as the NSP script, without the lock and rebuild lines. The tool checks used to be indmsh_chain.py, which is part of the chain version and of every shader's cache key, so that fix rebuilt every library; they now live intool_lock.py, outside both, and a change to them rebuilds nothing. - Found on: NX2Apple builds (LNPANDY/NX2Apple#31, item 3; PR #477, PR #487); reproduced with
arch -x86_64. Applies to every game.
IPA archives
A streamed file changes size and the IPA fails its CRC check
- Symptom: packing succeeds, but
unzip -tor Python'sZipFile.testzip()reports a bad CRC for a large deflated file. - Cause:
pack_ipa.pywrote the uncompressed size collected before reading. Its streaming check compared the output size only for stored files, so a compressible file that grew or shrank still published an archive with the old size. - Fix: count uncompressed bytes as they are read and compare them with the listed size before completing either streamed method. A mismatch stops the pack and removes the partial archive, preserving the previous IPA and its reuse index. Regression tests cover growth and shrinkage with both deflate and store.
- Found on: synthetic IPA inputs while reviewing PR #279; no game files required.
RomFS images
RomFS folder transfers fail or leave stale game files
- Symptom:
devicectlcopying a large RomFS folder fails with POSIX error 24 (EMFILE) or spends most of its time on small files; deleting an old folder during iOS startup hits the launch watchdog. A push after changing extracted files can also keep sending old assets if an image is reused solely because it exists. - Cause: a folder transfer performs hundreds of thousands of file operations. The image cache must track its source tree, and magic plus total length cannot establish that an image's index is valid. Accepting that weak check before deleting the working folder loses the fallback when mounting later rejects the index.
- Fix:
dm/tools/romfs_image.pypacks oneDMROMFS1file: a 32-byte header, file bytes, then the index and name pool. Each directory has a sorted, contiguous run of child entries; the host binary-searches each path component and reads file ranges withpreadthrough one descriptor per mount. Overlay folders keep precedence and directory listings merge both sources. The shareddm/host/romfs_image.hloader checks the full index before either mounting or iOS folder cleanup: length, entry and name bounds, file ranges, a rooted breadth-first directory tree, and sorted, unique child names. It supports an empty root too. Invalid images fall back to the folders on iOS; an explicit invalidDM_ROMFSimage is refused with a[RED]line. The index check detects interrupted sequential transfers and structural damage, not same-length corruption inside file contents. The packer publishes by rename only after the source stays stable. Its reuse stamp tracks source names and file metadata including ctime, the packer and image identity; the push helper always checks it, and--repackforces a rebuild. Usage: Quick reinstalls. - Found on: Tears of the Kingdom, whose 304,000-file transfer and startup deletion failures are reported in PR #369. ANIMAL WELL provides the empty-image boot case. Synthetic packer/host tests cover actual file reads, case aliases, overlay merging, corrupt indexes and changed sources; the macOS Foundation test executes iOS's locator to check that rejected images preserve its folder.
A stale DM_ROMFS path crashes the game soon after nnMain
- Symptom: a direct
dmhostrun logsfs mount rom: → <path> (read-only)for a path that does not exist, then crashes in the game's code a fraction of a second afternnMain(Echoes of Wisdom:CRASH sig=11 addr=0x880.3 s in). The host now stops at the mount instead withFATAL the game files <path> (rom:) are missing: neither a folder nor a RomFS image. - Cause: an old run command still named the extraction's
RomFSfolder after a rebuild replaced it withRomFS.img(RomFS folder transfers). The read-only mount accepted any path, so every read failed and the game used the missing data. The iOS app and the Mac app launcher check their game files before they start the host; direct runs had no such check. - Fix:
mount_ex(dm/host/hle_fs.c) stops on a read-only, non-save mount whose path is neither a folder nor a regular file, with the samestatthat tells an image from a folder. Add-on content and system data check their paths before mounting, so only the rom mount (DM_ROMFS, or$DM_ROOT/romfswhen it is unset) reaches it.dm/host/tests/t_fs.cchecks a missing path and a fifo in a forked child. - Found on: Echoes of Wisdom 1.1.0 on the Mac (issue #615, PR #616).
Separate RomFS deployment loses its image or folder fallback
- Symptom: a successful
--romfs separate --no-keep-extractedbuild prints a push command pointing at deleted game files. On iOS, a complete image for another build can also prevent startup despite a validDocuments/romfsfolder. - Cause: separate mode omitted the extraction-retention check, so normal post-build cleanup deleted the image needed for transfer. The startup identity rejection returned before the existing folder lookup.
- Fix: reject separate mode without
--keep-extractedbefore extraction or building. Reject a mismatching image while continuing to the validated folder fallback; move a folder aside for deletion only after accepting the image. Matching images and the default bundled route retain their behavior. The identity covers image size, header and index, not same-length file-content changes. - Found on: synthetic CLI and Foundation startup tests while integrating PR #493, after PR #478; no game or device is needed to reproduce either failure.
A hand-copied RomFS.img is not found on iOS
- Symptom: a
--romfs separateapp whose image was copied into its Documents folder by hand (Finder, the Files app, a LiveContainer guest) logsboot: game files not found (Documents/romfs.img, Documents/romfs or <App>/romfs), and a build withromfs.idshows "There is no game file image on this device.", though the complete image is there. Renaming it toromfs.imgmakes the game start. - Cause: the NSP script writes the image as
extract/<version>/RomFS.img, andpush_romfs.shcopies it toDocuments/romfs.img. A hand copy keeps the source's name. The Mac's file system ignores letter case, so the mismatch never shows there, but iOS's does not, andmain.mlooked up only the exactromfs.img. - Fix:
main.m(romfs_document_images) lists Documents forromfs.imgin any letter case,romfs.imgitself first, and takes the first that is complete and passes theromfs.idcheck; a rejected or incomplete one falls through to the next spelling, then to the folders. The identity report, the backup exclusion and the log lines use the names found. Folder names (Documents/romfs) are still matched exactly. - Found on: South Park: The Fractured But Whole 1.05 (
01008F2005154000, no profile here) on an iPhone 15 Pro Max, issue #582. The Foundation startup test runs the locator on a case-sensitive APFS disk image.
Building creates a full RomFS tree before packing its image
- Symptom: reducing the iPhone transfer to one image still creates thousands of resource files on the Mac, and build scanners require the expanded folder.
- Cause: folder extraction followed by a separate image pack duplicates the work; ordinary filesystem APIs cannot address files inside an image.
- Fix: the NSP pipeline passes
--romfs-imagetonx_extract. LibHac layers the update over the base and checks its integrity while the writer streams file bytes into a temporaryDMROMFS1image, appends the sorted directory index and publishes by rename. ExeFS remains expanded; RomFS and each DLC remain one physical file.dm/tools/romfs.pygives the scanners read-only virtual paths, bounded seeks and archive mappings (Python 3.12+), without changing the process's standard filesystem APIs. The IPA packer maps the physical image toPayload/DmIOS.app/romfs.img; iOS and the Mac launcher validate and mount it in place. Neither installation nor startup expands the resource files. Documents overrides and legacy folder inputs remain supported. A source image is uncompressed; the IPA can deflate it, so file-count reduction does not imply a smaller installed byte count. Full images for separate game versions cannot share unchanged individual files through APFS clones as legacy folder extraction did. - Found on: ANIMAL WELL, whose direct image is an empty root (56 bytes); the local Baba Is You provisional build exercises 6,374 files in a 103,473,292-byte image, 131 shaders and an unsigned IPA containing only the single image entry. Raging Loop supplies Unity resources: all 1,473 source manifest entries match the image, and every registered shader remains in the scan. Synthetic integration tests call the actual LibHac writer, read unaligned archive ranges, and exercise IPA mapping, Xcode staging and bundled-image startup.
The zstd command given a path inside the image
- Symptom: the NSP script stops at the shader split of a registered profile with
[RED] 13744 file(s) could not be read, their shaders are left out (first: AI/FileEntry/FileEntry.Product.100.byml.zs: CalledProcessError: Command ['zstd', '-d', '-c', '-q', '…/extract/v0/RomFS.img/AI/…'] returned non-zero exit status 1.). - Cause:
shader_split.py'sunzsread a.zsfile throughromfs.open, which reads inside the image, but then handed its path to thezstdcommand, which can only open real files (Not a directory). - Fix:
unzsfeeds the command the bytes it read (input=). No other tool hands an external command a path inside the image. Test:test_romfs_read.pytest_zstd_file_inside_an_image. - Found on: Super Mario Bros. Wonder: every file of its RomFS is zstd-compressed, 13,744 left out in both versions; the first image-backed build after the folder extraction.
Launcher metadata
A Mac front end's default settings change Metal execution paths
- Symptom: starting a Mac app through a front end with unchanged settings enables the encode thread, and can disable Metal 4 pipelines and the pipeline archive; starting the same app from Finder keeps the host's defaults.
- Cause: Mac
launcher.jsonreused iOS defaults fromsettings.tsv.rec_oninnvn_rec.hdefaults off on Mac and on on iOS;pso4_oninnvn_mtl.mdefaults on on Mac and follows the profile on iOS. The exported defaults instead enabled the encode thread everywhere and followed the profile for Metal 4 everywhere. A front end applying those values changed the Mac host's behavior without a player override. - Fix: a setting's default may be
<ios>|<mac>, selected before resolving a profile macro or build variable. Metal 4 usesDM_GAME_PSO_MTL4|1, the encode thread1|0. The generator validates the platform and the pair. Tests cover a profile with Metal 4 on and one with it off, and both platforms; the host and shader chain are unchanged. - Found on: synthetic metadata generation for Super Mario Bros. Wonder while reviewing PR #352; checked against the native defaults, without a gameplay run.
External drives
A game folder on an exFAT drive reads one update as two, or fails to remove a folder
- Symptom: with the NSP folder on an exFAT or FAT drive, the build leaves out an update whose name has no
[v…]tag (the folder seems to hold two), or a stage stops withFileNotFoundErroron a._<name>path inside<id>_build/and the next run fails on the half-deleted folder. - Cause: these file systems have no extended attributes, so macOS keeps a file's attributes (a download's quarantine flag) in an AppleDouble file
._<name>beside it, and deletes it with the file.scan_nspstook._Update [<title ID>800].nsp, 4 KB of metadata, for an update: its name carries the title ID. Deletingxtakes._xwith it, soshutil.rmtree's later unlink of the listed._xfound it gone; before Python 3.13 that error ended the removal. - Fix:
scripts/build_ipa_from_nsp.pyscan_nspstakes a file only when its header is one nx_extract opens (is_package:PFS0at 0, or an XCI'sHEADat 0x100), and says which files it skips. The name tag alone no longer counts, so the same check also rejects any other tagged file that is not a package. Names that start with._are skipped without a line. The script'srmtreepasses ENOENT fromshutil.rmtree(the entry is gone, which is the goal) and raises every other error.dm/tools/tests/test_nsp_external_drive.pyandtest_nsp_scan.pycover both without game files. - Found on: the NX2Apple fork's users, who keep games on external drives (PR #476, part of issue #388); reproduced on an exFAT disk image made with
hdiutil create -fs ExFAT. Applies to every game.
The c1 guard stops on an AppleDouble file after the shader build
- Symptom: with the build folder on an exFAT or FAT drive, the shader step compiles every library and then stops in
c1_guard.pywithUnicodeDecodeError: 'utf-8' codec can't decode byte …(dmsh_build.py:[RED] c1_guard.py failed), reading a._<key>_b.metal. - Cause: the same AppleDouble files as above, beside the shader build's outputs.
c1_guard.pytook every name ending in.metalfor a shader's MSL and read the binary._file as text.cb_extent.py(every_b.json) skipped it only because it ignores what it cannot parse, anddmsh_mtlcache.py(every_b.metal) took._<key>_bfor a shader, found no readable reflection for it and so filled the Metal cache from none of that folder's libraries. - Fix: the three take only the names the shader build writes,
<16 hex key>_b….metal/.jsonasdmsh_build.py(OUT_KEY) andregress.pyalready did:c1_guard.pyoutputs(whichcb_extent.pyuses too) anddmsh_mtlcache.pyOUTPUT. None of them is part of the chain version.dm/tools/tests/test_nsp_chain.pyruns both table tools on a folder with._files; the oldc1_guard.pystops on it. On 13 Sentinels' 751 shaders both tables are unchanged. - Found on: a user's iOS shader build on an external drive (PR #555). Applies to every NVN game.
Compressed inputs
NSZ decompression rejects --keys
- Symptom:
decompress NSZstops immediately withnsz: error: unrecognized arguments: --keys …. - Cause:
nsz4.6.1 has no--keysargument. It loadskeys.txtbesidesys.argv[0], or falls back to~/.switch/prod.keys, while importing its modules, before parsing arguments (4.6.1 key loader). Version 5.0 supports--keys(argument parser); stagingkeys.txtalso supports 4.6.1. - Fix:
scripts/build_ipa_from_nsp.pydecompress_nszprefers the current Python's module: it links the file selected by-kaskeys.txtin the conversion's temporary folder and runsnsz -Dthere, withsys.argv[0]set tonszbefore import. Without the module,nsz_commandprobesnszandnsz-cli-macos-arm64on the build's PATH and passes--keysto the first whose successful help lists that option. An unsupported or failed candidate does not hide the next one; when none works, the error lists the reasons and installation options. Executable, input and output paths are absolute because conversion changes directories. The temporary folder and link are removed on success or failure; the user's default keys are untouched. - Found on: Super Mario Odyssey, converting its v0 NSZ. Applies to NSZ and XCZ inputs for every game. Synthetic tests during PR #327 also reproduce an older console script hiding the release binary, a failed help probe containing
--keys, and a relative PATH executable failing after the directory change; no game files are needed for those cases.
ZBIC-compressed NSO segments
- Symptom: the NSP script or any module tool stops on
main:compressed with zstd (NSO flag bit 7), which this loader does not decode yet; with the decoder but without its package,ZBIC (zstd) segments need the zbic package (python3 -m pip install zbic). - Cause: from system 22.0.0, an NSO may store its segments as ZBIC, NSO flag bit 7 for the whole module (the per-segment compression and check-hash bits keep their meaning). ZBIC is Nintendo's variant of zstd, not a container around standard frames: its frames start with the magic
ZBICand its FSE tables are written with binary interpolative coding, so neithercompression.zstdnor thezstdcommand can read them. - Fix:
dm/tools/nso.pydecodes such segments with thezbicpackage (Kinnay's, zstd with Atmosphère's ZBIC changes), imported only for a flag-bit-7 module, so an LZ4 title needs nothing new; every segment is still held to the header's size and SHA-256. zstd has no fixed ratio bound, so a ZBIC segment is held only to its own size, where an LZ4 one is also held to 255 bytes per stored byte. Install it with the other tools (README, step 1). - Check: Well Dweller's
maindecodes to 85,370,056 bytes with segments0x39680d0 / 0x9f7e74 / 0x9b1930, every SHA-256 matching;dm/tools/tests/test_nso.pychecks that a flag-bit-7 module whose segments are not ZBIC is an Error naming ZBIC. - Found on: Well Dweller (SDK 22.2.0).
Package identification
An NSP without a ticket or a title ID in its name stops with "no NSP with a readable title ID"
- Symptom:
[RED] no NSP with a readable title ID in <folder>, although the keys open the package (the launcher's key check passes). Renaming the file toGame [0100XXXXXXXXXXXX].nspmakes the build go on. - Cause:
title_oftook the title ID only from the ticket's file name (<rights ID>.tik) or from a[title ID]tag in the package's file name. Many NSPs carry no ticket (standard-crypto dumps, repacks), and a download's file name often has no tag. - Fix: with neither,
title_ofasks the package:nx_extract <prod.keys> --title <package>opens it, decrypts the header of each meta NCA with the keys'header_keyand prints the lowest title ID (the base game's before its update's or a DLC's). It works for NSP, NSZ (the meta NCA is not compressed), XCI and XCZ, and runs only when the ticket and the tag are both missing, so the common cases stay a read of the file name table.nx_extractis built into a temporary folder for it, since the build folder is named after the title ID. Key values are never printed. - Found on: NX2Apple, LNPANDY/NX2Apple#177 (Tears of the Kingdom, whose base NSP has no ticket). Applies to every game.
Cartridge dumps
A cartridge dump with its update on it extracts the update as the base game
- Symptom: the extraction of a game whose base is an XCI that also carries its update (a "1G+1U" dump) stops with
[RED] nx_extract exit code -6.extract_v<version>.loglists the update's build IDs even for the base game alone, then ends in an unhandled exception from the RomFS (ResultFsNonRealDataVerificationFailed (2002-4604): Hash error!fromRomFsFileSystem..ctor, orArgumentException/InvalidOperationExceptionfrom the patch layers). - Cause: the XCI's secure partition holds the update's NCAs beside the base game's, and the update's Program NCA has the program's title ID too.
nx_extracttook the first Program NCA of the lowest title ID as the base, which is the update's when the dump lists it first. Its data section is a patch (BKTR) over the base RomFS, so it cannot be read alone or under another patch. The ExeFS still opens, with the update's build IDs, because an update's code section is complete. The Control NCA (display version, icon) was also whichever of the two came last. - Fix:
dm/tools/nx_extract/Program.csOpenNspreads the base package's content metas (*.cnmtin its Meta NCAs) and keeps only the NCAs that its Application content meta lists, and prints how many it left out. When none of them is a Program NCA (NCA files renamed, so their names are not content IDs), it prints a[RED]line and reads every NCA as before. An NSP or a base-only XCI keeps every NCA but its meta NCA, so it extracts as before (Balatro's NSPs and Monster Hunter Rise's XCI give identical manifests). The update is still taken from the update NSP in the folder; the one on the cartridge is not used, so an XCI alone packs the base game's version. - Found on: The Legend of Zelda: Tears of the Kingdom, a 1G+1U XCI (base v0 with update 1.4.3, whose 524 MB Program NCA is listed before the base's 17.1 GB one) beside the 1.4.3 update NSP (PR #311). Reproduced with a package of Balatro's update NSP followed by its base NSP: the base alone and the base with the update now give the same manifests and control data as the separate NSPs. Applies to any game's XCI that carries its update.
Sparse base games
The survey's build ID lookup stops on the base game alone with a hash error
- Symptom: for a game with no profile, the NSP script stops at
build ID lookup · main of the base gamewith[RED] nx_extract exit code -6;<title ID>_survey/logs/exefs.logends inResultFsNonRealDataVerificationFailed (2002-4604): Hash error!fromPartitionFileSystemMetaCore.QueryMetaDataSizeunderNca.OpenFileSystem, although every NCA in both NSPs matches its content ID (the SHA-256 that names it) and the base game with its update extracts cleanly. - Cause: the base game's Program NCA is sparse: its code and data sections carry a sparse layer and leave out data that the update supplies, so the base is smaller than its update (1.6 GB against 2.9 GB). Horizon mounts such an NCA only through its patch; read alone, the left-out ranges fail the hash check from the first block of the ExeFS. The lookup (
same_build) tries main's build ID of the newest update first, then of the base game alone, andrunstopped the build on the second attempt's failure. - Fix:
dm/tools/nx_extract/Program.csstops with exit code 4 and[RED] the base game's program is sparse: it can be read only with its update NSPwhen it is given a sparse base without an update (any enabled section of the Program NCA with a sparse layer).scripts/build_ipa_from_nsp.pyruntakes the exit codes a caller accepts (ok), andsame_buildaccepts 4: the base game alone then has no main of its own and the lookup goes on. The base game with its update reads as before (LibHac layers the update over the sparse base; the whole 4.2 GB merged RomFS of the game below reads with every hash check on). - Found on: Minecraft Dungeons II (
0100A7C01B792000, no profile yet), base v0 (SDK 22.2.0) with update v131072 (1.1.3.0). Applies to any game whose base Program NCA is sparse.
Republished from the nxnative repository under its MIT license (license and attribution). Original: docs/reference/packing.md on GitHub.