Jesseb342

Untitled

Oct 27th, 2026
18
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.75 KB | None | 0 0
  1.  
  2. name: NxNative - Generic IPA Builder
  3.  
  4. on:
  5. workflow_dispatch:
  6. inputs:
  7. device:
  8. description: "Choose the target device and texture support path"
  9. required: true
  10. type: choice
  11. default: "iPhone 15 or older (BC fallback)"
  12. options:
  13. - "iPhone 15 or older (BC fallback)"
  14. - "iPhone 15 Pro/Pro Max or newer (standard)"
  15. profile_id:
  16. description: "Optional nxnative profile ID override (leave blank for automatic detection)"
  17. required: false
  18. type: string
  19. default: ""
  20.  
  21. permissions:
  22. contents: read
  23.  
  24. jobs:
  25. build:
  26. runs-on: macos-latest
  27. timeout-minutes: 360
  28.  
  29. steps:
  30. - name: Checkout BC fallback version
  31. if: inputs.device == 'iPhone 15 or older (BC fallback)'
  32. uses: actions/checkout@v4
  33. with:
  34. repository: mianmuhammadse/nxnative
  35. ref: d158df611bf2f3fa315c4f16585575cdfdb2a1d3
  36.  
  37. - name: Checkout standard nxnative version
  38. if: inputs.device == 'iPhone 15 Pro/Pro Max or newer (standard)'
  39. uses: actions/checkout@v4
  40. with:
  41. repository: seedds/nxnative
  42. ref: main
  43.  
  44. - name: Set up Python
  45. uses: actions/setup-python@v5
  46. with:
  47. python-version: "3.13"
  48.  
  49. - name: Set up .NET SDK
  50. uses: actions/setup-dotnet@v4
  51. with:
  52. dotnet-version: "10.0.x"
  53.  
  54. - name: Install dependencies
  55. shell: bash
  56. run: |
  57. set -euo pipefail
  58. brew install xcodegen zstd spirv-cross unar
  59. python -m pip install --upgrade pip
  60. python -m pip install \
  61. lz4 capstone fonttools backports.zstd gdown
  62.  
  63. - name: Download game files and keys
  64. env:
  65. GAME_URL: ${{ secrets.Game_URL }}
  66. UPDATE_URL: ${{ secrets.Update_URL }}
  67. DLC_URL: ${{ secrets.DLC_URL }}
  68. KEYS_URL: ${{ secrets.Keys_URL }}
  69. shell: bash
  70. run: |
  71. set -euo pipefail
  72. WORK="$RUNNER_TEMP/nxnative"
  73. mkdir -p "$WORK"
  74.  
  75. download_file() {
  76. local url="$1"
  77. local output="$2"
  78. local file_id
  79.  
  80. if [[ -z "${url:-}" ]]; then
  81. echo "::error::A required download URL is empty."
  82. exit 1
  83. fi
  84.  
  85. file_id="$(python - "$url" <<'PY'
  86. import re
  87. import sys
  88. from urllib.parse import urlparse, parse_qs
  89.  
  90. url = sys.argv[1].strip()
  91. parsed = urlparse(url)
  92. query = parse_qs(parsed.query)
  93. file_id = None
  94.  
  95. if parsed.netloc.lower() in (
  96. "drive.google.com",
  97. "www.drive.google.com",
  98. ):
  99. match = re.search(r"/file/d/([^/]+)", parsed.path)
  100. if match:
  101. file_id = match.group(1)
  102. elif query.get("id"):
  103. file_id = query["id"][0]
  104.  
  105. if not file_id and re.fullmatch(
  106. r"[-A-Za-z0-9_]{20,}", url
  107. ):
  108. file_id = url
  109.  
  110. if not file_id:
  111. raise SystemExit(
  112. "Could not extract a Google Drive file ID."
  113. )
  114.  
  115. print(file_id)
  116. PY
  117. )"
  118.  
  119. echo "Downloading $(basename "$output")..."
  120. gdown "$file_id" -O "$output"
  121.  
  122. if [[ ! -s "$output" ]]; then
  123. echo "::error::Downloaded file is empty."
  124. exit 1
  125. fi
  126.  
  127. file "$output"
  128. }
  129.  
  130. download_file "$GAME_URL" "$WORK/game.download"
  131. download_file "$KEYS_URL" "$WORK/prod.keys"
  132.  
  133. if [[ -n "${UPDATE_URL:-}" ]]; then
  134. download_file "$UPDATE_URL" "$WORK/update.download"
  135. else
  136. echo "No UPDATE_URL configured; continuing without an update."
  137. fi
  138.  
  139. if [[ -n "${DLC_URL:-}" ]]; then
  140. download_file "$DLC_URL" "$WORK/dlc.download"
  141. else
  142. echo "No DLC_URL configured; continuing without DLC."
  143. fi
  144.  
  145. chmod 600 "$WORK/prod.keys"
  146.  
  147. - name: Extract RAR archives or accept direct game packages
  148. shell: bash
  149. run: |
  150. set -euo pipefail
  151. WORK="$RUNNER_TEMP/nxnative"
  152. GAME_DIR="$RUNNER_TEMP/GameFiles"
  153. mkdir -p "$GAME_DIR"
  154.  
  155. python - "$WORK" "$GAME_DIR" <<'PY'
  156. import shutil
  157. import subprocess
  158. import sys
  159. from pathlib import Path
  160.  
  161. work = Path(sys.argv[1])
  162. game_dir = Path(sys.argv[2])
  163. package_extensions = {
  164. ".nsp", ".xci", ".nsz", ".xcz"
  165. }
  166. rar_signatures = (
  167. b"Rar!\x1a\x07\x00",
  168. b"Rar!\x1a\x07\x01\x00",
  169. )
  170.  
  171. def validate_package(path):
  172. with path.open("rb") as f:
  173. magic = f.read(4)
  174.  
  175. suffix = path.suffix.lower()
  176.  
  177. if suffix in (".nsp", ".nsz") and magic != b"PFS0":
  178. raise SystemExit(
  179. f"Invalid {suffix} package signature: {path.name}"
  180. )
  181.  
  182. if suffix in (".xci", ".xcz") and magic != b"HEAD":
  183. raise SystemExit(
  184. f"Invalid {suffix} package signature: {path.name}"
  185. )
  186.  
  187. def add_package(source, label):
  188. target = game_dir / source.name
  189.  
  190. if target.exists():
  191. raise SystemExit(
  192. "Duplicate package filename: " + source.name
  193. )
  194.  
  195. validate_package(source)
  196. shutil.copy2(source, target)
  197.  
  198. print(
  199. f"Added {label}: {target.name} "
  200. f"({target.stat().st_size:,} bytes)"
  201. )
  202.  
  203. def process_download(label):
  204. source = work / f"{label}.download"
  205.  
  206. if not source.exists():
  207. print(f"No {label} download; skipping.")
  208. return
  209.  
  210. with source.open("rb") as f:
  211. signature = f.read(8)
  212.  
  213. is_rar = any(
  214. signature.startswith(magic)
  215. for magic in rar_signatures
  216. )
  217.  
  218. if is_rar:
  219. extract_dir = work / f"extracted-{label}"
  220. extract_dir.mkdir(parents=True, exist_ok=True)
  221.  
  222. print(f"{label}: RAR archive detected; extracting.")
  223.  
  224. subprocess.run(
  225. [
  226. "unar",
  227. "-o",
  228. str(extract_dir),
  229. str(source),
  230. ],
  231. check=True,
  232. )
  233.  
  234. packages = sorted(
  235. p for p in extract_dir.rglob("*")
  236. if p.is_file()
  237. and p.suffix.lower() in package_extensions
  238. )
  239.  
  240. if not packages:
  241. raise SystemExit(
  242. f"No NSP/XCI/NSZ/XCZ packages found "
  243. f"in the {label} RAR archive."
  244. )
  245.  
  246. for package in packages:
  247. add_package(package, label)
  248.  
  249. else:
  250. print(
  251. f"{label}: not a RAR archive; "
  252. "checking as a direct package."
  253. )
  254.  
  255. if signature[:4] == b"PFS0":
  256. extension = ".nsp"
  257. elif signature[:4] == b"HEAD":
  258. extension = ".xci"
  259. else:
  260. raise SystemExit(
  261. f"{label} is not a recognized RAR archive "
  262. "or direct NSP/XCI package. "
  263. "Direct downloads must be NSP or XCI files."
  264. )
  265.  
  266. target = game_dir / f"{label}{extension}"
  267.  
  268. if target.exists():
  269. raise SystemExit(
  270. f"Output package already exists: {target.name}"
  271. )
  272.  
  273. shutil.copy2(source, target)
  274. validate_package(target)
  275.  
  276. print(
  277. f"Added direct package: {target.name} "
  278. f"({target.stat().st_size:,} bytes)"
  279. )
  280.  
  281. process_download("game")
  282. process_download("update")
  283. process_download("dlc")
  284.  
  285. packages = sorted(
  286. p for p in game_dir.iterdir()
  287. if p.is_file()
  288. and p.suffix.lower() in package_extensions
  289. )
  290.  
  291. if not packages:
  292. raise SystemExit("No game packages were prepared.")
  293.  
  294. print("\n=== Prepared game packages ===")
  295. for package in packages:
  296. print(f"{package.name}: {package.stat().st_size:,} bytes")
  297. PY
  298.  
  299. test -s "$WORK/prod.keys"
  300.  
  301. - name: Detect game title ID and select shader profile
  302. env:
  303. PROFILE_OVERRIDE: ${{ inputs.profile_id }}
  304. shell: bash
  305. run: |
  306. set -euo pipefail
  307. GAME_DIR="$RUNNER_TEMP/GameFiles"
  308.  
  309. DETECTED_ID="$(python - "$GAME_DIR" <<'PY'
  310. import re
  311. import struct
  312. import sys
  313. from pathlib import Path
  314.  
  315. root = Path(sys.argv[1])
  316. extensions = {".nsp", ".xci", ".nsz", ".xcz"}
  317.  
  318. def pfs0_names(path):
  319. try:
  320. with path.open("rb") as f:
  321. header = f.read(16)
  322.  
  323. if len(header) != 16 or header[:4] != b"PFS0":
  324. return []
  325.  
  326. count, string_size = struct.unpack_from(
  327. "<II", header, 4
  328. )
  329.  
  330. if count > 100000 or string_size > 100000000:
  331. return []
  332.  
  333. entries = f.read(count * 0x18)
  334. strings = f.read(string_size)
  335.  
  336. if len(entries) != count * 0x18:
  337. return []
  338.  
  339. names = []
  340.  
  341. for i in range(count):
  342. offset = struct.unpack_from(
  343. "<I", entries, i * 0x18 + 0x10
  344. )[0]
  345.  
  346. if offset >= len(strings):
  347. continue
  348.  
  349. end = strings.find(b"\0", offset)
  350.  
  351. if end < 0:
  352. continue
  353.  
  354. names.append(
  355. strings[offset:end].decode(
  356. errors="replace"
  357. )
  358. )
  359.  
  360. return names
  361.  
  362. except (OSError, struct.error):
  363. return []
  364.  
  365. candidates = set()
  366.  
  367. for path in root.iterdir():
  368. if not path.is_file() or path.suffix.lower() not in extensions:
  369. continue
  370.  
  371. for name in pfs0_names(path):
  372. match = re.fullmatch(
  373. r"([0-9A-Fa-f]{16})[0-9A-Fa-f]{16}\.tik",
  374. name,
  375. )
  376.  
  377. if match:
  378. title_id = int(match.group(1), 16)
  379.  
  380. if (title_id & 0xFFF) == 0:
  381. candidates.add(f"{title_id:016X}")
  382.  
  383. match = re.search(
  384. r"\[([0-9A-Fa-f]{16})\]",
  385. path.name,
  386. )
  387.  
  388. if match:
  389. title_id = int(match.group(1), 16)
  390.  
  391. if (title_id & 0xFFF) == 0:
  392. candidates.add(f"{title_id:016X}")
  393.  
  394. if len(candidates) != 1:
  395. raise SystemExit(
  396. "Could not uniquely identify the base game's title ID. "
  397. f"Candidates found: {sorted(candidates)}. "
  398. "Check the base NSP and its ticket/filename."
  399. )
  400.  
  401. print(next(iter(candidates)))
  402. PY
  403. )"
  404.  
  405. PROFILE_ID="${PROFILE_OVERRIDE:-$DETECTED_ID}"
  406. PROFILE_ID="$(echo "$PROFILE_ID" | tr '[:lower:]' '[:upper:]')"
  407.  
  408. if [[ ! "$PROFILE_ID" =~ ^[0-9A-F]{16}$ ]]; then
  409. echo "::error::Profile ID must be exactly 16 hexadecimal characters."
  410. exit 1
  411. fi
  412.  
  413. echo "Detected base title ID: $DETECTED_ID"
  414. echo "Shader profile ID: $PROFILE_ID"
  415. echo "DM_GAME=$PROFILE_ID" >> "$GITHUB_ENV"
  416. echo "DETECTED_TITLE_ID=$DETECTED_ID" >> "$GITHUB_ENV"
  417.  
  418. - name: Update shader toolchain lock and header
  419. shell: bash
  420. run: |
  421. set -euo pipefail
  422. echo "Preparing shader toolchain for profile $DM_GAME"
  423.  
  424. CHAIN_SCRIPT="$(
  425. find . -name dmsh_chain.py -type f -print -quit
  426. )"
  427.  
  428. if [[ -z "$CHAIN_SCRIPT" ]]; then
  429. echo "::error::Could not find dmsh_chain.py"
  430. exit 1
  431. fi
  432.  
  433. python "$CHAIN_SCRIPT" --update-lock
  434. python "$CHAIN_SCRIPT" --header
  435.  
  436. - name: Build unsigned IPA
  437. shell: bash
  438. run: |
  439. set -euo pipefail
  440. WORK="$RUNNER_TEMP/nxnative"
  441. GAME_DIR="$RUNNER_TEMP/GameFiles"
  442. OUTPUT="$RUNNER_TEMP/NxNative.ipa"
  443.  
  444. python scripts/build_ipa_from_nsp.py \
  445. "$GAME_DIR" \
  446. -k "$WORK/prod.keys" \
  447. -o "$OUTPUT"
  448.  
  449. if [[ ! -s "$OUTPUT" ]]; then
  450. echo "::error::IPA build did not produce an output file."
  451. exit 1
  452. fi
  453.  
  454. echo "IPA successfully created: $OUTPUT"
  455. ls -lh "$OUTPUT"
  456.  
  457. - name: Upload IPA
  458. if: success()
  459. uses: actions/upload-artifact@v4
  460. with:
  461. name: NxNative-IPA-${{ inputs.device == 'iPhone 15 or older (BC fallback)' && 'BC-Fallback' || 'Standard' }}
  462. path: ${{ runner.temp }}/NxNative.ipa
  463. if-no-files-found: error
  464. retention-days: 7
  465.  
  466. - name: Upload build logs if available
  467. if: always()
  468. uses: actions/upload-artifact@v4
  469. with:
  470. name: NxNative-Build-Logs
  471. path: |
  472. ${{ runner.temp }}/GameFiles/*_build/logs/
  473. ${{ runner.temp }}/GameFiles/*_survey/
  474. if-no-files-found: ignore
  475. retention-days: 7
  476.  
  477. - name: Clean up downloaded files and keys
  478. if: always()
  479. shell: bash
  480. run: |
  481. rm -rf \
  482. "$RUNNER_TEMP/nxnative" \
  483. "$RUNNER_TEMP/GameFiles"
  484.  
Advertisement
Add Comment
Please, Sign In to add comment