scripts/: build_ipa_from_nsp.py moves here and runs from the CLI too
- Scripts the user runs now live in scripts/ at the repository root; build_ipa_from_nsp.py moves there from dm/ios/tools (history kept). AGENTS.md records the convention. - It still runs in Spyder (F5, settings block, prompts for an empty NSP folder or prod.keys). From a terminal it now takes options that override the settings: <NSP folder>, -k prod.keys, -p version, -o out.ipa, --app-name, --keep-extracted, --env-file (-h for help). Without keyboard input a missing value is an error instead of a hung prompt. Exit codes 0 / 1 / 130 in a terminal; inside IPython (Spyder) it does not call sys.exit, so the console stays clean. - The repository root is found by walking up to dm/tools/nx_extract rather than a fixed depth. - Checked: -h from another directory; error paths (missing folder, no terminal input, empty folder, unregistered version) exit 1 with a clear message; a CLI run on the Engage NSP folder identifies the game, skips DLC, picks base + update, passes the tool check and stops at the free-space check; IPython %run under the Spyder interpreter (prompts, -h, an error) keeps the console alive. - README, dm/games READMEs and docs/agents point at the new path.
This commit is contained in:
1 parent
6c0a14dcb8
commit
97fb028917
7 files changed
+78
-28
No files matched your search
@@ -3,7 +3,8 @@
|
||||
- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. One game is supported so far: Super Mario Bros. Wonder (base game v0 and update 1.2.1, profile `smbw`). Start with [README.md](README.md) for status and build steps.
|
||||
- Each game's constants and shader tables form a game profile in `dm/games/<id>/`: `game.tsv` plus the tables and `dmsh_chain.h`. Mario's profile is `smbw`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. All default to `smbw`. Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)).
|
||||
- Per-build knowledge is keyed by the NSO build ID: rewriter cuts in `dm/tools/patch_nso.py`, the profile's `build` rows (the builds the NSP script accepts, and their display versions), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`, `DM_D2R_BUILD` for code inherited from D2R). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in.
|
||||
- To analyse or port **another** Switch title, first run `dm/ios/tools/build_ipa_from_nsp.py` on its NSP folder. With no matching profile, it writes a survey to `<NSP folder>/<title ID>_survey/survey.md`. Then follow [docs/agents](docs/agents/README.md) (extraction, binary survey, host bring-up failure catalogue, graphics specifics, debugging recipes) and add a profile.
|
||||
- To analyse or port **another** Switch title, first run `scripts/build_ipa_from_nsp.py` on its NSP folder. With no matching profile, it writes a survey to `<NSP folder>/<title ID>_survey/survey.md`. Then follow [docs/agents](docs/agents/README.md) (extraction, binary survey, host bring-up failure catalogue, graphics specifics, debugging recipes) and add a profile.
|
||||
- Scripts the user runs live in `scripts/` at the repository root (now `build_ipa_from_nsp.py`). Each one runs both in Spyder (F5, configured by the settings block at its top) and from a terminal (command-line options override those settings; non-zero exit on error). Tools that only agents or other scripts call stay under `dm/`.
|
||||
- Game files, keys and anything extracted from them live in the user's workspace (`~/<id>-work`, Mario `~/smbw-work`) or in the NSP folder's `<id>_build/`, never in this repository. Do not print key values.
|
||||
- Code comments are bilingual (Chinese line, then English line); red log lines start with `【红】`. Keep both conventions.
|
||||
- After changing anything on the shader translation chain, run the chain procedure for each affected profile. The chain covers `dm/gfx/tools/dmsh_*`, `Program.cs`, the profile's `shader_map.tsv` / `shader_facts.tsv` / `shader_pairs.tsv`, and its `nvn_*` / `shader_*` keys. The procedure:
|
||||
|
||||
@@ -40,23 +40,31 @@ How the profile is chosen and used:
|
||||
- Code inherited from D2R that relies on D2R's addresses or files runs only on D2R's build.
|
||||
|
||||
To add a game:
|
||||
1. Run `build_ipa_from_nsp.py` on its NSP folder (see [Build from the NSPs](#build-from-the-nsps)). With no matching profile, it writes a survey to `<title ID>_survey/survey.md`.
|
||||
1. Run `scripts/build_ipa_from_nsp.py` on its NSP folder (see [Build from the NSPs](#build-from-the-nsps)). With no matching profile, it writes a survey to `<title ID>_survey/survey.md`.
|
||||
2. Add a profile as described in [dm/games/README.md](dm/games/README.md).
|
||||
3. Bring the game up following the guides in [docs/agents](docs/agents/README.md).
|
||||
|
||||
## Build from the NSPs
|
||||
|
||||
`dm/ios/tools/build_ipa_from_nsp.py` runs the whole chain: extract, rewrite, shaders, IPA. It needs only three things:
|
||||
`scripts/build_ipa_from_nsp.py` runs the whole chain: extract, rewrite, shaders, IPA. It needs only three things:
|
||||
- this repository;
|
||||
- a folder with one game's NSPs: the base game plus the updates in the profile's `shader_versions` (for Mario, v0 and 1.2.1). DLC is skipped;
|
||||
- `prod.keys`.
|
||||
|
||||
The script runs in Spyder or with `python3`. That Python needs lz4, capstone and fonttools. The settings at the top of the file are:
|
||||
The script runs in Spyder (open it and press F5) or from a terminal. That Python needs lz4, capstone and fonttools. In Spyder it uses the settings at the top of the file:
|
||||
- the NSP folder and keys (asked for at run time if empty);
|
||||
- the version to pack (default: the profile's `package`);
|
||||
- the output IPA (default `~/Desktop/<the profile's ipa>`);
|
||||
- `APP_NAME`, the home-screen name. Left empty, it is the game name from the base NSP.
|
||||
|
||||
From a terminal, options override those settings (`-h` lists them all):
|
||||
|
||||
```
|
||||
python3 scripts/build_ipa_from_nsp.py <NSP folder> -k <prod.keys> [-p <version>] [-o <out.ipa>] [--app-name <name>] [--keep-extracted]
|
||||
```
|
||||
|
||||
It exits with 0 when done and 1 on an error.
|
||||
|
||||
The script picks the profile from the NSPs' title ID and accepts only the builds the profile lists. Every intermediate file goes to `<id>_build/` in the NSP folder. For a game with no profile, it writes a survey instead of an IPA. The survey lists the modules, SDK, engine, containers, rewrite sites, and imports compared with the host hooks.
|
||||
|
||||
## Build step by step (Mac)
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ Everything game-specific belongs here. Code reads the profile; it does not hard-
|
||||
| Shader tools (`shader_split.py`, `dmsh_chain.py`, `dmsh_build.py`, `dmsh_facts.py`) | `DM_GAME=<id>` | tables, `nvn_*`, `shader_*` keys |
|
||||
| Host (`make -C dm/host`, `dm/gfx/tests`) | `GAME=<id>` | `gen/<id>/game_profile.h`, generated from `game.tsv` by `game.py header` |
|
||||
| `dm/ios/tools/build_ipa.sh` | `GAME=<id>` | app name, bundle ID, IPA name, modules, RomFS marker, tables |
|
||||
| `dm/ios/tools/build_ipa_from_nsp.py` | the NSPs' title ID | everything above. With no matching profile it only writes a survey |
|
||||
| `scripts/build_ipa_from_nsp.py` | the NSPs' title ID | everything above. With no matching profile it only writes a survey |
|
||||
|
||||
All of them default to `smbw`. To query a profile:
|
||||
- `python3 dm/games/game.py list` lists the profiles.
|
||||
@@ -42,7 +42,7 @@ The translator (`dmsh_xlate/Program.cs`) still has Mario's texture / sampler bas
|
||||
|
||||
## Adding a game
|
||||
|
||||
1. **Survey.** Run `dm/ios/tools/build_ipa_from_nsp.py` on the game's NSP folder. Without a profile it extracts only the code modules and a RomFS list, then writes `<NSP folder>/<title ID>_survey/survey.md`. The survey covers modules and build IDs, SDK, engine and graphics API markers, RomFS containers, the rewrite-site census, and imports compared with the host hooks.
|
||||
1. **Survey.** Run `scripts/build_ipa_from_nsp.py` on the game's NSP folder. Without a profile it extracts only the code modules and a RomFS list, then writes `<NSP folder>/<title ID>_survey/survey.md`. The survey covers modules and build IDs, SDK, engine and graphics API markers, RomFS containers, the rewrite-site census, and imports compared with the host hooks.
|
||||
2. **Profile.** Copy `smbw/game.tsv` to `dm/games/<id>/game.tsv` and fill in the identity, `build` rows and `modules`. Leave the memory and NVN values as Mario's until the bring-up measures them ([docs/agents](../../docs/agents/README.md)).
|
||||
3. **Shader tables.** Run `DM_GAME=<id> shader_split.py … dm/games/<id>/shader_map.tsv dm/games/<id>/shader_facts.tsv dm/games/<id>/shader_pairs.tsv`. Add an empty `shader_map.tsv.conflicts.tsv`, `bf_want.tsv` and `pso_manifest.tsv` (copy Mario's headers).
|
||||
4. **Chain.**
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| 着色器工具(`shader_split.py`、`dmsh_chain.py`、`dmsh_build.py`、`dmsh_facts.py`) | `DM_GAME=<名>` | 各表,`nvn_*`、`shader_*` 键 |
|
||||
| 宿主(`make -C dm/host`、`dm/gfx/tests`) | `GAME=<名>` | `gen/<名>/game_profile.h`(`game.py header` 由 `game.tsv` 生成) |
|
||||
| `dm/ios/tools/build_ipa.sh` | `GAME=<名>` | App 名、包名、IPA 名、代码模块、RomFS 认定路径、各表 |
|
||||
| `dm/ios/tools/build_ipa_from_nsp.py` | NSP 的标题号 | 以上全部。没有对应档案时只写粗查报告 |
|
||||
| `scripts/build_ipa_from_nsp.py` | NSP 的标题号 | 以上全部。没有对应档案时只写粗查报告 |
|
||||
|
||||
缺省都是 `smbw`。查档案:
|
||||
- `python3 dm/games/game.py list` 列出全部档案。
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
## 加一个游戏
|
||||
|
||||
1. **粗查。** 对这个游戏的 NSP 文件夹跑 `dm/ios/tools/build_ipa_from_nsp.py`。没有档案时它只解出代码模块和 RomFS 清单,写出 `<NSP 文件夹>/<标题号>_survey/survey.md`。报告包括模块与构建号、SDK、引擎与图形接口标记、RomFS 容器、改写点统计、导入与宿主钩子的对比。
|
||||
1. **粗查。** 对这个游戏的 NSP 文件夹跑 `scripts/build_ipa_from_nsp.py`。没有档案时它只解出代码模块和 RomFS 清单,写出 `<NSP 文件夹>/<标题号>_survey/survey.md`。报告包括模块与构建号、SDK、引擎与图形接口标记、RomFS 容器、改写点统计、导入与宿主钩子的对比。
|
||||
2. **档案。** 把 `smbw/game.tsv` 拷成 `dm/games/<名>/game.tsv`,填身份、`build` 行和 `modules`。内存与 NVN 的值先沿用马力欧的,等起机时实测再改([docs/agents](../../docs/agents/README.md))。
|
||||
3. **着色器表。** 跑 `DM_GAME=<名> shader_split.py … dm/games/<名>/shader_map.tsv dm/games/<名>/shader_facts.tsv dm/games/<名>/shader_pairs.tsv`。再补空的 `shader_map.tsv.conflicts.tsv`、`bf_want.tsv` 和 `pso_manifest.tsv`(表头照马力欧的抄)。
|
||||
4. **翻译链。**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Do this before touching the host. Every item answers "what will break, and how much work is it".
|
||||
|
||||
**The first pass is automatic.** Run `dm/ios/tools/build_ipa_from_nsp.py` on the title's NSP folder. When no game profile (`dm/games/*/game.tsv`) has its title ID, the script does no build. It extracts only the code modules plus a RomFS manifest (`nx_extract --manifest`: path, size and first 32 bytes of every file, with no game files written). It then writes `<NSP folder>/<title ID>_survey/survey.md`, which covers:
|
||||
**The first pass is automatic.** Run `scripts/build_ipa_from_nsp.py` on the title's NSP folder. When no game profile (`dm/games/*/game.tsv`) has its title ID, the script does no build. It extracts only the code modules plus a RomFS manifest (`nx_extract --manifest`: path, size and first 32 bytes of every file, with no game files written). It then writes `<NSP folder>/<title ID>_survey/survey.md`, which covers:
|
||||
|
||||
- game name and display version (from the NACP). Newer SDKs compress the NACP's title area: a u16 size, then raw deflate that inflates to 32 language entries of 0x300 bytes (Mario 1.2.1 does this). LibHac 0.19 does not decode it; `nx_extract` and the script do;
|
||||
- base / update / DLC NSPs;
|
||||
|
||||
@@ -22,7 +22,7 @@ Everything here was learned on two titles: Diablo II: Resurrected (SDK ~12, NVN
|
||||
|
||||
## Checklist for a new title
|
||||
|
||||
1. Run `dm/ios/tools/build_ipa_from_nsp.py` on the title's NSP folder. With no profile for its title ID, it writes `<NSP folder>/<title ID>_survey/survey.md`:
|
||||
1. Run `scripts/build_ipa_from_nsp.py` on the title's NSP folder. With no profile for its title ID, it writes `<NSP folder>/<title ID>_survey/survey.md`:
|
||||
- modules and build IDs, SDK version and components, engine and graphics API markers;
|
||||
- RomFS containers;
|
||||
- the rewrite-site census and imports vs. host hooks.
|
||||
|
||||
Regular → Executable
+59
-18
@@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
# 从 NSP 一路出未签名 IPA:认游戏(标题号 → dm/games/<名>/ 游戏档案)→ 解包(nx_extract)→ 改写代码模块(patch_nso.py)→ 拆出着色器并与档案里的表核对(shader_split.py)
|
||||
# → iOS 着色器库(dmsh_build.py)→ build_ipa.sh。没有对应档案的游戏只做粗查(<NSP 文件夹>/<标题号>_survey/survey.md),不出 IPA。
|
||||
# 在 Spyder 里打开本文件按 F5 运行(也可 python3 build_ipa_from_nsp.py);下面「设置」留空的两项运行时会问。
|
||||
# 两种跑法:在 Spyder 里打开本文件按 F5(用下面「设置」里的值,留空的 NSP 文件夹与 prod.keys 运行时会问);
|
||||
# 或在终端里 python3 scripts/build_ipa_from_nsp.py <NSP 文件夹> -k <prod.keys> [选项](命令行给的值盖过「设置」,-h 看全部选项;
|
||||
# 终端不接键盘输入时缺的值不问、直接报错;成功退出码 0,出错 1,中断 130)。Spyder 的「运行配置」里填命令行选项也一样生效。
|
||||
# 只用三样东西:本仓库、放一个游戏 NSP 的文件夹(基础版 + 档案 shader_versions 列的更新都要;DLC 跳过,本移植不读追加内容)、prod.keys。
|
||||
# 所有中间产物都在 NSP 文件夹里的 <名>_build/:extract/(解出的游戏文件,出包后默认删掉)、tools/、patched_*/、shader/(着色器包与缓存,留着下次出包快)、logs/(每步的完整输出)、tmp/、ipa/。
|
||||
# 着色器翻译器用仓内的 dm/gfx/tools/dmsh_xlate_bin(与 dmsh_chain.lock 指纹一致的那份;重编出的 dll 指纹不同,dmsh_build 会拒绝)。
|
||||
# 运行本文件的 Python 要有 lz4、capstone、fonttools;另需 Xcode、xcodegen、.NET SDK 10、zstd、spirv-cross。马力欧首次约 35 分钟(大头是着色器库),再跑约 10 分钟(着色器库走缓存)。密钥值一律不打印。
|
||||
# Unsigned IPA straight from the NSPs: identify the game (title ID → game profile dm/games/<id>/) → extract (nx_extract) → rewrite the code modules (patch_nso.py) → split out the shaders and check them against the profile's tables (shader_split.py)
|
||||
# → iOS shader libraries (dmsh_build.py) → build_ipa.sh. A game with no matching profile is only surveyed (<NSP folder>/<title ID>_survey/survey.md); no IPA.
|
||||
# Open this file in Spyder and press F5 (or run python3 build_ipa_from_nsp.py); the two settings below that are left empty are asked for when it runs.
|
||||
# Two ways to run it: open this file in Spyder and press F5 (it uses the values under "Settings" below; an empty NSP folder or prod.keys is asked for at run time);
|
||||
# or in a terminal: python3 scripts/build_ipa_from_nsp.py <NSP folder> -k <prod.keys> [options] (values given on the command line override "Settings", -h lists every option;
|
||||
# without keyboard input a missing value is not asked for but reported as an error; exit code 0 on success, 1 on error, 130 when interrupted). Command-line options entered in Spyder's run configuration work the same way.
|
||||
# Uses only three things: this repository, a folder holding one game's NSPs (the base game plus the updates listed in the profile's shader_versions are required; DLC is skipped, this port does not load add-on content), and prod.keys.
|
||||
# Every intermediate file is under <id>_build/ in the NSP folder: extract/ (the extracted game files, deleted after the IPA is built by default), tools/, patched_*/, shader/ (shader package and caches, kept so the next build is fast), logs/ (each step's full output), tmp/, ipa/.
|
||||
# The shader translator is the in-repo dm/gfx/tools/dmsh_xlate_bin (the build whose fingerprints match dmsh_chain.lock; a rebuilt dll has different fingerprints and dmsh_build refuses it).
|
||||
@@ -37,7 +41,14 @@ def say(msg):
|
||||
print(f'[{time.strftime("%H:%M:%S")}] {msg}', flush=True)
|
||||
|
||||
|
||||
def ask(prompt):
|
||||
def in_ipython(): # Spyder(与 Jupyter)在 IPython 里跑本文件:输入框可用,且不能 sys.exit(会把报错堆在控制台上) | Spyder (and Jupyter) run this file inside IPython: input works there, and sys.exit must not be called (it piles an error onto the console)
|
||||
import builtins
|
||||
return hasattr(builtins, '__IPYTHON__')
|
||||
|
||||
|
||||
def ask(prompt, zh, en): # zh / en:缺的是什么,报错用 | zh / en: what is missing, for the error message
|
||||
if not in_ipython() and not (sys.stdin and sys.stdin.isatty()):
|
||||
raise BuildError(f'没给{zh},又没有键盘输入可问:在命令行上给出(-h 看用法)| {en} not given and there is no keyboard input to ask for it: give it on the command line (-h for usage)')
|
||||
s = input(prompt).strip().strip('"').strip("'").replace('\\ ', ' ') # 拖进来的路径带引号或转义空格 | dragged-in paths come quoted or with escaped spaces
|
||||
return os.path.expanduser(s)
|
||||
|
||||
@@ -346,13 +357,36 @@ def build_shaders(py, repo, sh, xlate, env, logs, g):
|
||||
return ios
|
||||
|
||||
|
||||
def main():
|
||||
def find_repo(here): # 本文件在仓库的 scripts/ 下;往上找到有 dm/tools/nx_extract/Program.cs 的那一层 | this file lives in the repository's scripts/; walk up to the level that has dm/tools/nx_extract/Program.cs
|
||||
for d in here.parents:
|
||||
if (d / 'dm/tools/nx_extract/Program.cs').exists(): return d
|
||||
raise BuildError(f'从 {here} 往上找不到仓库根目录(没有 dm/tools/nx_extract)| cannot find the repository root above {here} (no dm/tools/nx_extract)')
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
"""命令行选项:给了就盖过「设置」;Spyder 里 F5 不带选项,全用「设置」。-h 看说明
|
||||
Command-line options: when given they override "Settings"; F5 in Spyder passes none, so "Settings" is used as is. -h shows the help"""
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(prog='build_ipa_from_nsp.py',
|
||||
description='从 NSP 出未签名 IPA;没有游戏档案的游戏只写粗查报告 | Build an unsigned IPA from a game\'s NSPs; a game with no profile only gets a survey.',
|
||||
epilog='例 | example: python3 scripts/build_ipa_from_nsp.py ~/nsp/"Super Mario Bros Wonder" -k ~/keys/prod.keys')
|
||||
ap.add_argument('nsp_dir', nargs='?', help='放一个游戏 NSP 的文件夹(缺省 = 设置 NSP_DIR,再没有就问)| folder holding one game\'s NSPs (default: the NSP_DIR setting, else asked)')
|
||||
ap.add_argument('-k', '--keys', help='prod.keys 的路径(缺省 = 设置 PROD_KEYS,再没有就问)| path of prod.keys (default: the PROD_KEYS setting, else asked)')
|
||||
ap.add_argument('-p', '--package', help='打进 IPA 的版本,如 v327680 / v0(缺省 = 游戏档案的 package)| version packed into the IPA, e.g. v327680 / v0 (default: the profile\'s package)')
|
||||
ap.add_argument('-o', '--out', help='输出的 IPA(缺省 ~/Desktop/<档案的 ipa>)| output IPA (default ~/Desktop/<the profile\'s ipa>)')
|
||||
ap.add_argument('--app-name', help='主屏上的 App 名(缺省 = 基础版 NSP 里的游戏名)| app name on the home screen (default: the game name in the base NSP)')
|
||||
ap.add_argument('--keep-extracted', action='store_true', help='出包后保留解出的游戏文件 | keep the extracted game files after the IPA is built')
|
||||
ap.add_argument('--env-file', help='诊断包的环境开关文件(每行 KEY=VALUE)| environment switches for a diagnostic build (one KEY=VALUE per line)')
|
||||
return ap.parse_args(argv)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
args = args or parse_args([])
|
||||
here = Path(globals().get('__file__') or sys.argv[0]).resolve()
|
||||
repo = here.parents[3] # dm/ios/tools/本文件 | dm/ios/tools/this file
|
||||
if not (repo / 'dm/tools/nx_extract/Program.cs').exists(): raise BuildError(f'从 {here} 推不出仓库根目录 | cannot find the repository root from {here}')
|
||||
repo = find_repo(here)
|
||||
sys.path.insert(0, str(repo / 'dm/games')); import game as gamemod # 游戏档案 | game profiles
|
||||
nsp_dir = Path(os.path.expanduser(NSP_DIR) or ask('NSP 文件夹 | NSP folder: ')).resolve()
|
||||
keys = Path(os.path.expanduser(PROD_KEYS) or ask('prod.keys 路径 | path of prod.keys: ')).resolve()
|
||||
nsp_dir = Path(os.path.expanduser(args.nsp_dir or NSP_DIR or ask('NSP 文件夹 | NSP folder: ', ' NSP 文件夹', 'the NSP folder'))).resolve()
|
||||
keys = Path(os.path.expanduser(args.keys or PROD_KEYS or ask('prod.keys 路径 | path of prod.keys: ', ' prod.keys(-k)', 'prod.keys (-k)'))).resolve()
|
||||
if not nsp_dir.is_dir(): raise BuildError(f'不是文件夹 | not a folder: {nsp_dir}')
|
||||
if not keys.is_file(): raise BuildError(f'没有这个文件 | no such file: {keys}')
|
||||
env = dict(os.environ, PATH=os.pathsep.join(TOOL_PATHS + [os.environ.get('PATH', '/usr/bin:/bin:/usr/sbin:/sbin')]))
|
||||
@@ -367,10 +401,10 @@ def main():
|
||||
return survey(nsp_dir, app, fam, keys, repo, py, env)
|
||||
say(f'游戏档案 | game profile: {g.id}')
|
||||
builds, need = g.builds(), g.words('shader_versions')
|
||||
pkg_ver = PACKAGE or g.get('package')
|
||||
if pkg_ver not in builds: raise BuildError(f"PACKAGE 只能是游戏档案登记的版本 | PACKAGE must be a version the game profile registers: {', '.join(builds)}")
|
||||
pkg_ver = args.package or PACKAGE or g.get('package')
|
||||
if pkg_ver not in builds: raise BuildError(f"打进 IPA 的版本(-p / PACKAGE)只能是游戏档案登记的 | the version to pack (-p / PACKAGE) must be one the game profile registers: {', '.join(builds)}")
|
||||
versions = need + ([pkg_ver] if pkg_ver not in need else [])
|
||||
out_ipa = Path(os.path.expanduser(OUT_IPA or f'~/Desktop/{g.get("ipa")}'))
|
||||
out_ipa = Path(os.path.expanduser(args.out or OUT_IPA or f'~/Desktop/{g.get("ipa")}')).resolve()
|
||||
work = nsp_dir / f'{g.id}_build'
|
||||
xlate = repo / 'dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate'
|
||||
# DM_GAME 选档案;DM_ROOT / WORK 指到 <名>_build:各工具的缺省路径与临时目录都不出它 | DM_GAME picks the profile; DM_ROOT / WORK point at <id>_build: the tools' default paths and temporary directories all stay inside it
|
||||
@@ -392,7 +426,7 @@ def main():
|
||||
if u is None and clone is None and can_clone(ex): clone = dirs[ver] / 'RomFS'
|
||||
game = dirs[pkg_ver]
|
||||
base = next((dirs[v] for v in versions if picks[v][1] is None), dirs[versions[0]]) # 基础版单独解出的目录 | the folder the base game was extracted to alone
|
||||
app_name = APP_NAME or nacp_name(base) or g.get('app_name') # 设置 > 基础版 NSP 里的游戏名 > 档案 | setting > game name in the base NSP > profile
|
||||
app_name = args.app_name or APP_NAME or nacp_name(base) or g.get('app_name') # 设置 > 基础版 NSP 里的游戏名 > 档案 | setting > game name in the base NSP > profile
|
||||
say(f'App 名 | app name: {app_name}')
|
||||
|
||||
patched = work / f'patched_{pkg_ver}'
|
||||
@@ -404,19 +438,26 @@ def main():
|
||||
if free_gb(out_ipa.parent) < 5: raise BuildError(f'{out_ipa.parent} 所在的卷剩余不足 5 GB | less than 5 GB free on the volume of {out_ipa.parent}')
|
||||
say(f'出 IPA | building the IPA → {out_ipa}')
|
||||
ipa_env = dict(env, GAME=g.id, W=str(work / 'ipa'), PATCHED=str(patched), ROMFS=str(game / 'RomFS'), MSL_IOS=str(ios), FONT_PY=py, DM_DISPLAY_NAME=app_name)
|
||||
if DM_ENV_FILE: ipa_env['DM_ENV_FILE'] = os.path.expanduser(DM_ENV_FILE)
|
||||
env_file = args.env_file or DM_ENV_FILE
|
||||
if env_file: ipa_env['DM_ENV_FILE'] = str(Path(os.path.expanduser(env_file)).resolve())
|
||||
run(['bash', repo / 'dm/ios/tools/build_ipa.sh', out_ipa], logs / 'build_ipa.log', env=ipa_env)
|
||||
if not out_ipa.is_file(): raise BuildError(f'没出 IPA | no IPA at {out_ipa}')
|
||||
rmtree(work / 'tmp')
|
||||
if not KEEP_EXTRACTED:
|
||||
if not (args.keep_extracted or KEEP_EXTRACTED):
|
||||
say(f'删掉解出的游戏文件 | deleting the extracted game files: {ex}'); rmtree(ex)
|
||||
say(f'完成 | done: {out_ipa}({out_ipa.stat().st_size / 2**30:.1f} GB)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def cli(argv): # 返回退出码:0 完成,1 出错,130 中断 | returns the exit code: 0 done, 1 error, 130 interrupted
|
||||
try:
|
||||
main()
|
||||
main(parse_args(argv))
|
||||
except BuildError as e:
|
||||
print(f'【红】{e}', flush=True)
|
||||
print(f'【红】{e}', flush=True); return 1
|
||||
except KeyboardInterrupt:
|
||||
print('【红】中断 | interrupted', flush=True)
|
||||
print('【红】中断 | interrupted', flush=True); return 130
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
code = cli(sys.argv[1:])
|
||||
if not in_ipython(): sys.exit(code) # 终端里给出退出码;Spyder 里不退(控制台保持干净) | give an exit code in a terminal; do not exit in Spyder (keeps the console clean)
|
||||
Reference in new issue
Block a user