Jesseb342

Untitled

Nov 4th, 2026
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.61 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. content:
  16. description: "Choose which game content to include"
  17. required: true
  18. type: choice
  19. default: "Base game + update + DLC"
  20. options:
  21. - "Base game only"
  22. - "Base game + update (no DLC)"
  23. - "Base game + DLC (no update)"
  24. - "Base game + update + DLC"
  25.  
  26. permissions:
  27. contents: read
  28.  
  29. jobs:
  30. build:
  31. # Both NxNative paths require an ARM64 macOS host.
  32. runs-on: macos-latest
  33. timeout-minutes: 360
  34.  
  35. steps:
  36. # CHECKOUT
  37. - name: Checkout BC fallback version
  38. if: inputs.device == 'iPhone 15 or older (BC fallback)'
  39. uses: actions/checkout@v4
  40. with:
  41. repository: mianmuhammadse/nxnative
  42. ref: d158df611bf2f3fa315c4f16585575cdfdb2a1d3
  43.  
  44. - name: Checkout standard nxnative version
  45. if: inputs.device == 'iPhone 15 Pro/Pro Max or newer (standard)'
  46. uses: actions/checkout@v4
  47. with:
  48. repository: seedds/nxnative
  49. ref: main
  50.  
  51. # VERIFIED SDK MEMORY PATCHES
  52. - name: Add verified SDK memory patches (BC fallback)
  53. if: inputs.device == 'iPhone 15 or older (BC fallback)'
  54. shell: bash
  55. run: |
  56. set -euo pipefail
  57.  
  58. UPSTREAM_COMMIT="777a5f6c618a38cd2d7f795f465466fb56920534"
  59. RAW="https://raw.githubusercontent.com/seedds/nxnative/$UPSTREAM_COMMIT/dm/tools"
  60.  
  61. curl -fsSL "$RAW/nso_code.py" -o dm/tools/nso_code.py
  62. curl -fsSL "$RAW/nso_match.py" -o dm/tools/nso_match.py
  63. curl -fsSL "$RAW/sdk_patches.py" -o dm/tools/sdk_patches.py
  64.  
  65. python - <<'PY'
  66. from pathlib import Path
  67.  
  68. path = Path("dm/tools/patch_nso.py")
  69. source = path.read_text()
  70.  
  71. old = """BUILD_ID = d[0x40:0x50].hex()
  72. cuts = list(CUTS.get(BUILD_ID, []))
  73. """
  74.  
  75. new = """BUILD_ID = d[0x40:0x50].hex()
  76. cuts = list(CUTS.get(BUILD_ID, []))
  77.  
  78. # Apply upstream's SDK-wide address-space workarounds only to SDK modules.
  79. # resolve() matches complete function fingerprints and verifies every
  80. # original instruction before the existing byte-checked cut loop runs.
  81. if NAME == 'sdk':
  82. import sdk_patches
  83. from nso_match import resolve
  84. cuts += resolve(IMG, TEXT_END, sdk_patches.MATCH_CUTS.get('sdk', []), red=False)
  85. """
  86.  
  87. if source.count(old) != 1:
  88. raise SystemExit(
  89. "Could not find the expected SDK cut block in patch_nso.py; "
  90. "refusing to patch an unknown BC-fallback source version."
  91. )
  92.  
  93. path.write_text(source.replace(old, new, 1))
  94. print("Installed verified upstream SDK function-matching patches for BC fallback.")
  95. PY
  96.  
  97. # METAL HARDWARE DIAGNOSTICS
  98. - name: Inspect Mac runner Metal hardware (BC fallback)
  99. if: inputs.device == 'iPhone 15 or older (BC fallback)'
  100. shell: bash
  101. run: |
  102. set -euo pipefail
  103. echo "=== Runner identity ==="
  104. uname -a
  105. sw_vers
  106. echo "=== Display / GPU hardware ==="
  107. system_profiler SPDisplaysDataType
  108. echo "=== Xcode ==="
  109. xcodebuild -version
  110.  
  111. - name: Add Metal residency-set diagnostics (BC fallback)
  112. if: inputs.device == 'iPhone 15 or older (BC fallback)'
  113. shell: bash
  114. run: |
  115. set -euo pipefail
  116.  
  117. python - <<'PY'
  118. from pathlib import Path
  119.  
  120. path = Path("dm/gfx/nvn/nvn_mtl.m")
  121. source = path.read_text()
  122.  
  123. old = ''' g_q = [g_dev newCommandQueue];
  124. MTLResidencySetDescriptor *rd = [MTLResidencySetDescriptor new];
  125. rd.initialCapacity = 4096;
  126. NSError *re = nil;
  127. g_res = [g_dev newResidencySetWithDescriptor:rd error:&re];
  128. if (!g_res) MDIE("failed to create the MTLResidencySet");
  129. '''
  130.  
  131. new = ''' MLOG("Metal device: %s; registryID=%llu; unifiedMemory=%d; maxBufferLength=%llu; recommendedMaxWorkingSetSize=%llu; OS=%s",
  132. g_dev.name.UTF8String,
  133. (unsigned long long)g_dev.registryID,
  134. g_dev.hasUnifiedMemory,
  135. (unsigned long long)g_dev.maxBufferLength,
  136. (unsigned long long)g_dev.recommendedMaxWorkingSetSize,
  137. NSProcessInfo.processInfo.operatingSystemVersionString.UTF8String);
  138. g_q = [g_dev newCommandQueue];
  139. MTLResidencySetDescriptor *rd = [MTLResidencySetDescriptor new];
  140. rd.initialCapacity = 4096;
  141. NSError *re = nil;
  142. g_res = [g_dev newResidencySetWithDescriptor:rd error:&re];
  143. if (!g_res) {
  144. MLOG("FATAL failed to create the MTLResidencySet");
  145. if (re) {
  146. MLOG("Metal residency-set NSError: domain=%s code=%ld description=%s userInfo=%s",
  147. re.domain.UTF8String, (long)re.code,
  148. re.localizedDescription.UTF8String,
  149. re.userInfo.description.UTF8String);
  150. } else {
  151. MLOG("Metal residency-set NSError was nil");
  152. }
  153. abort();
  154. }
  155. '''
  156. if old not in source:
  157. raise SystemExit(
  158. "Could not find the expected Metal residency-set initialization block; "
  159. "refusing to patch an unknown NxNative source version."
  160. )
  161.  
  162. path.write_text(source.replace(old, new, 1))
  163. print("Added Metal device/OS and residency-set NSError diagnostics.")
  164. PY
  165.  
  166. # ENVIRONMENT
  167. - name: Set up Python
  168. uses: actions/setup-python@v5
  169. with:
  170. python-version: "3.13"
  171.  
  172. - name: Set up .NET SDK
  173. uses: actions/setup-dotnet@v4
  174. with:
  175. dotnet-version: "10.0.x"
  176.  
  177. - name: Install dependencies
  178. shell: bash
  179. run: |
  180. set -euo pipefail
  181. brew install xcodegen zstd spirv-cross unar
  182. python -m pip install --upgrade pip
  183. python -m pip install lz4 'capstone>=5,<6' fonttools backports.zstd
  184.  
  185. # DOWNLOAD CACHE KEY
  186. - name: Prepare download cache
  187. id: cache-key
  188. env:
  189. GAME_URL: ${{ secrets.Game_URL }}
  190. UPDATE_URL: ${{ secrets.Update_URL }}
  191. DLC_URL: ${{ secrets.DLC_URL }}
  192. shell: bash
  193. run: |
  194. set -euo pipefail
  195.  
  196. python - <<'PY'
  197. import hashlib
  198. import os
  199.  
  200. urls = [
  201. os.environ.get("GAME_URL", "").strip(),
  202. os.environ.get("UPDATE_URL", "").strip(),
  203. os.environ.get("DLC_URL", "").strip(),
  204. ]
  205.  
  206. if not urls[0]:
  207. raise SystemExit("Game_URL secret is empty.")
  208.  
  209. cache_key = hashlib.sha256(
  210. "|".join(urls).encode()
  211. ).hexdigest()
  212.  
  213. with open(os.environ["GITHUB_OUTPUT"], "a") as f:
  214. f.write(f"key=nxnative-downloads-{cache_key}\n")
  215.  
  216. print("Download cache key prepared.")
  217. PY
  218.  
  219. mkdir -p "$RUNNER_TEMP/nxnative/download-cache"
  220.  
  221. - name: Restore downloaded game files
  222. id: download-cache
  223. uses: actions/cache/restore@v4
  224. with:
  225. path: ${{ runner.temp }}/nxnative/download-cache
  226. key: ${{ runner.os }}-${{ steps.cache-key.outputs.key }}
  227.  
  228. # DOWNLOAD GAME / UPDATE / DLC AND DECODE KEYS
  229. - name: Download game files and decode keys
  230. env:
  231. GAME_URL: ${{ secrets.Game_URL }}
  232. UPDATE_URL: ${{ secrets.Update_URL }}
  233. DLC_URL: ${{ secrets.DLC_URL }}
  234. KEYS_BASE64: ${{ secrets.Keys_URL }}
  235. CONTENT_SELECTION: ${{ inputs.content }}
  236. shell: bash
  237. run: |
  238. set -euo pipefail
  239.  
  240. WORK="$RUNNER_TEMP/nxnative"
  241. CACHE="$WORK/download-cache"
  242.  
  243. mkdir -p "$WORK" "$CACHE"
  244.  
  245. download_file() {
  246. local url="$1"
  247. local output="$2"
  248. local filename
  249. local cached
  250. local temporary
  251.  
  252. filename="$(basename "$output")"
  253. cached="$CACHE/$filename"
  254. temporary="$output.part"
  255.  
  256. if [[ -z "${url:-}" ]]; then
  257. echo "::error::Download URL is empty."
  258. exit 1
  259. fi
  260.  
  261. if [[ -s "$cached" ]]; then
  262. echo "========================================"
  263. echo "CACHE HIT: $filename"
  264. echo "Reusing cached download."
  265. echo "========================================"
  266. cp "$cached" "$output"
  267. file "$output"
  268. return
  269. fi
  270.  
  271. echo "========================================"
  272. echo "CACHE MISS: $filename"
  273. echo "Starting direct download..."
  274. echo "========================================"
  275.  
  276. curl \
  277. --fail \
  278. --location \
  279. --show-error \
  280. --progress-bar \
  281. --retry 3 \
  282. --retry-delay 5 \
  283. --connect-timeout 30 \
  284. --output "$temporary" \
  285. "$url"
  286.  
  287. if [[ ! -s "$temporary" ]]; then
  288. echo "::error::Downloaded file is empty."
  289. exit 1
  290. fi
  291.  
  292. mv "$temporary" "$output"
  293. echo "Download completed:"
  294. ls -lh "$output"
  295. file "$output"
  296.  
  297. cp "$output" "$cached"
  298. echo "Saved $filename to download cache."
  299. }
  300.  
  301. download_file "$GAME_URL" "$WORK/game.download"
  302.  
  303. case "${CONTENT_SELECTION:-}" in
  304. "Base game only")
  305. INCLUDE_UPDATE=false
  306. INCLUDE_DLC=false
  307. ;;
  308. "Base game + update (no DLC)")
  309. INCLUDE_UPDATE=true
  310. INCLUDE_DLC=false
  311. ;;
  312. "Base game + DLC (no update)")
  313. INCLUDE_UPDATE=false
  314. INCLUDE_DLC=true
  315. ;;
  316. "Base game + update + DLC")
  317. INCLUDE_UPDATE=true
  318. INCLUDE_DLC=true
  319. ;;
  320. *)
  321. echo "::error::Unknown content selection: ${CONTENT_SELECTION:-unset}"
  322. exit 1
  323. ;;
  324. esac
  325.  
  326. if [[ "$INCLUDE_UPDATE" == true ]]; then
  327. if [[ -z "${UPDATE_URL:-}" ]]; then
  328. echo "::error::Update was selected, but UPDATE_URL is not configured."
  329. exit 1
  330. fi
  331. download_file "$UPDATE_URL" "$WORK/update.download"
  332. else
  333. echo "Update excluded by content selection; skipping."
  334. fi
  335.  
  336. if [[ "$INCLUDE_DLC" == true ]]; then
  337. if [[ -z "${DLC_URL:-}" ]]; then
  338. echo "::error::DLC was selected, but DLC_URL is not configured."
  339. exit 1
  340. fi
  341. download_file "$DLC_URL" "$WORK/dlc.download"
  342. else
  343. echo "DLC excluded by content selection; skipping."
  344. fi
  345.  
  346. if [[ -z "${KEYS_BASE64:-}" ]]; then
  347. echo "::error::Keys_URL secret is empty."
  348. exit 1
  349. fi
  350.  
  351. echo "Decoding Base64 encryption keys..."
  352.  
  353. python - "$WORK/prod.keys" <<'PY'
  354. import base64
  355. import os
  356. import re
  357. import sys
  358. from pathlib import Path
  359.  
  360. encoded = os.environ["KEYS_BASE64"].strip()
  361. encoded = re.sub(r"\s+", "", encoded)
  362.  
  363. try:
  364. decoded = base64.b64decode(encoded, validate=True)
  365. except Exception as exc:
  366. raise SystemExit(f"Invalid Base64 encryption keys: {exc}")
  367.  
  368. if not decoded:
  369. raise SystemExit("Decoded keys file is empty.")
  370.  
  371. output = Path(sys.argv[1])
  372. output.write_bytes(decoded)
  373. output.chmod(0o600)
  374.  
  375. print(f"Successfully decoded keys file: {len(decoded):,} bytes")
  376. PY
  377.  
  378. test -s "$WORK/prod.keys"
  379. echo "All downloads and key preparation completed."
  380.  
  381. # SAVE DOWNLOAD CACHE
  382. - name: Save downloaded game files
  383. if: success() && steps.download-cache.outputs.cache-hit != 'true'
  384. uses: actions/cache/save@v4
  385. with:
  386. path: ${{ runner.temp }}/nxnative/download-cache
  387. key: ${{ runner.os }}-${{ steps.cache-key.outputs.key }}
  388.  
  389. # EXTRACT RAR / VALIDATE PACKAGES
  390. - name: Extract RAR archives or accept direct game packages
  391. shell: bash
  392. run: |
  393. set -euo pipefail
  394.  
  395. WORK="$RUNNER_TEMP/nxnative"
  396. GAME_DIR="$RUNNER_TEMP/GameFiles"
  397.  
  398. mkdir -p "$GAME_DIR"
  399.  
  400. python - "$WORK" "$GAME_DIR" <<'PY'
  401. import shutil
  402. import subprocess
  403. import sys
  404. from pathlib import Path
  405.  
  406. work = Path(sys.argv[1])
  407. game_dir = Path(sys.argv[2])
  408.  
  409. package_extensions = {".nsp", ".xci", ".nsz", ".xcz"}
  410. rar_signatures = (
  411. b"Rar!\x1a\x07\x00",
  412. b"Rar!\x1a\x07\x01\x00",
  413. )
  414.  
  415. def validate_package(path):
  416. with path.open("rb") as f:
  417. magic = f.read(4)
  418.  
  419. suffix = path.suffix.lower()
  420.  
  421. if suffix in (".nsp", ".nsz") and magic != b"PFS0":
  422. raise SystemExit(f"Invalid {suffix} package signature: {path.name}")
  423.  
  424. if suffix in (".xci", ".xcz") and magic != b"HEAD":
  425. raise SystemExit(f"Invalid {suffix} package signature: {path.name}")
  426.  
  427. def add_package(source, label):
  428. target = game_dir / source.name
  429.  
  430. if target.exists():
  431. raise SystemExit("Duplicate package filename: " + source.name)
  432.  
  433. validate_package(source)
  434. shutil.copy2(source, target)
  435.  
  436. print(
  437. f"Added {label}: {target.name} "
  438. f"({target.stat().st_size:,} bytes)"
  439. )
  440.  
  441. def process_download(label):
  442. source = work / f"{label}.download"
  443.  
  444. if not source.exists():
  445. print(f"No {label} download; skipping.")
  446. return
  447.  
  448. with source.open("rb") as f:
  449. signature = f.read(8)
  450.  
  451. is_rar = any(signature.startswith(magic) for magic in rar_signatures)
  452.  
  453. if is_rar:
  454. extract_dir = work / f"extracted-{label}"
  455. extract_dir.mkdir(parents=True, exist_ok=True)
  456.  
  457. print(f"{label}: RAR archive detected; extracting.")
  458.  
  459. subprocess.run(
  460. ["unar", "-o", str(extract_dir), str(source)],
  461. check=True,
  462. )
  463.  
  464. packages = sorted(
  465. p for p in extract_dir.rglob("*")
  466. if p.is_file() and p.suffix.lower() in package_extensions
  467. )
  468.  
  469. if not packages:
  470. raise SystemExit(
  471. f"No NSP/XCI/NSZ/XCZ packages found in the {label} RAR archive."
  472. )
  473.  
  474. for package in packages:
  475. add_package(package, label)
  476.  
  477. else:
  478. print(f"{label}: not a RAR archive; checking as a direct package.")
  479.  
  480. if signature[:4] == b"PFS0":
  481. extension = ".nsp"
  482. elif signature[:4] == b"HEAD":
  483. extension = ".xci"
  484. else:
  485. raise SystemExit(
  486. f"{label} is not a recognized RAR archive or direct NSP/XCI package."
  487. )
  488.  
  489. target = game_dir / f"{label}{extension}"
  490.  
  491. if target.exists():
  492. raise SystemExit(f"Output package already exists: {target.name}")
  493.  
  494. shutil.copy2(source, target)
  495. validate_package(target)
  496.  
  497. print(
  498. f"Added direct package: {target.name} "
  499. f"({target.stat().st_size:,} bytes)"
  500. )
  501.  
  502. process_download("game")
  503. process_download("update")
  504. process_download("dlc")
  505.  
  506. packages = sorted(
  507. p for p in game_dir.iterdir()
  508. if p.is_file() and p.suffix.lower() in package_extensions
  509. )
  510.  
  511. if not packages:
  512. raise SystemExit("No game packages were prepared.")
  513.  
  514. print("\n=== Prepared game packages ===")
  515.  
  516. for package in packages:
  517. print(f"{package.name}: {package.stat().st_size:,} bytes")
  518. PY
  519.  
  520. test -s "$WORK/prod.keys"
  521.  
  522. # DETECT BASE TITLE ID
  523. - name: Detect game title ID and select shader profile
  524. shell: bash
  525. run: |
  526. set -euo pipefail
  527.  
  528. GAME_DIR="$RUNNER_TEMP/GameFiles"
  529.  
  530. DETECTED_ID="$(python - "$GAME_DIR" <<'PY'
  531. import re
  532. import struct
  533. import sys
  534. from pathlib import Path
  535.  
  536. root = Path(sys.argv[1])
  537. extensions = {".nsp", ".xci", ".nsz", ".xcz"}
  538.  
  539. def pfs0_names(path):
  540. try:
  541. with path.open("rb") as f:
  542. header = f.read(16)
  543.  
  544. if len(header) != 16 or header[:4] != b"PFS0":
  545. return []
  546.  
  547. count, string_size = struct.unpack_from("<II", header, 4)
  548.  
  549. if count > 100000 or string_size > 100000000:
  550. return []
  551.  
  552. entries = f.read(count * 0x18)
  553. strings = f.read(string_size)
  554.  
  555. if len(entries) != count * 0x18:
  556. return []
  557.  
  558. names = []
  559.  
  560. for i in range(count):
  561. offset = struct.unpack_from(
  562. "<I", entries, i * 0x18 + 0x10
  563. )[0]
  564.  
  565. if offset >= len(strings):
  566. continue
  567.  
  568. end = strings.find(b"\0", offset)
  569.  
  570. if end < 0:
  571. continue
  572.  
  573. names.append(
  574. strings[offset:end].decode(errors="replace")
  575. )
  576.  
  577. return names
  578.  
  579. except (OSError, struct.error):
  580. return []
  581.  
  582. candidates = set()
  583.  
  584. for path in root.iterdir():
  585. if not path.is_file() or path.suffix.lower() not in extensions:
  586. continue
  587.  
  588. for name in pfs0_names(path):
  589. match = re.fullmatch(
  590. r"([0-9A-Fa-f]{16})[0-9A-Fa-f]{16}\.tik",
  591. name,
  592. )
  593.  
  594. if match:
  595. title_id = int(match.group(1), 16)
  596.  
  597. if (title_id & 0xFFF) == 0:
  598. candidates.add(f"{title_id:016X}")
  599.  
  600. match = re.search(r"\[([0-9A-Fa-f]{16})\]", path.name)
  601.  
  602. if match:
  603. title_id = int(match.group(1), 16)
  604.  
  605. if (title_id & 0xFFF) == 0:
  606. candidates.add(f"{title_id:016X}")
  607.  
  608. if len(candidates) != 1:
  609. raise SystemExit(
  610. "Could not uniquely identify the base game's title ID. "
  611. f"Candidates found: {sorted(candidates)}. "
  612. "Check the base NSP and its ticket/filename."
  613. )
  614.  
  615. print(next(iter(candidates)))
  616. PY
  617. )"
  618.  
  619. echo "Detected base title ID: $DETECTED_ID"
  620. echo "NxNative will select an existing profile or generate a survey and provisional profile automatically."
  621. echo "DETECTED_TITLE_ID=$DETECTED_ID" >> "$GITHUB_ENV"
  622.  
  623. # ADAPT SHADER TRANSLATION LOCK
  624. - name: Adapt shader translation lock to runner
  625. shell: bash
  626. run: |
  627. set -euo pipefail
  628.  
  629. python - <<'PY'
  630. from pathlib import Path
  631.  
  632. path = Path("scripts/build_ipa_from_nsp.py")
  633. source = path.read_text()
  634.  
  635. old = """ PROG.stage('iOS shaders')
  636. if provisional:
  637. PROG.set(step='chain header')
  638. run([py, repo / 'dm/gfx/tools/dmsh_chain.py', '--header'], logs / 'dmsh_chain.log', env=env) # the profile's chain version header (dmsh_build checks it)
  639. chain = re.search(r'#define DMSH_CHAIN "(.*)"', g.table('dmsh_chain.h').read_text()).group(1)
  640. """
  641.  
  642. new = """ PROG.stage('iOS shaders')
  643. PROG.set(step='toolchain lock')
  644. run([py, repo / 'dm/gfx/tools/dmsh_chain.py', '--update-lock'], logs / 'dmsh_chain_lock.log', env=env)
  645. PROG.set(step='chain header')
  646. run([py, repo / 'dm/gfx/tools/dmsh_chain.py', '--header'], logs / 'dmsh_chain.log', env=env)
  647. chain = re.search(r'#define DMSH_CHAIN "(.*)"', g.table('dmsh_chain.h').read_text()).group(1)
  648. """
  649.  
  650. if old not in source:
  651. raise SystemExit(
  652. "Could not find the expected nxnative shader-chain block; "
  653. "refusing to patch an unknown source version."
  654. )
  655.  
  656. path.write_text(source.replace(old, new, 1))
  657. print("Patched shader build to refresh the tool lock and chain header.")
  658. PY
  659.  
  660. # HANDLE UNITY COMPUTE KERNELS WITHOUT uniqueVariants
  661. - name: Handle Unity compute kernels without uniqueVariants
  662. shell: bash
  663. run: |
  664. set -euo pipefail
  665.  
  666. python - <<'PY'
  667. from pathlib import Path
  668.  
  669. path = Path("dm/gfx/tools/shader_split.py")
  670. lines = path.read_text().splitlines(keepends=True)
  671.  
  672. target = "for i, u in enumerate(k['uniqueVariants']):"
  673. matches = [i for i, line in enumerate(lines) if line.strip() == target]
  674.  
  675. if len(matches) != 1:
  676. raise SystemExit(
  677. f"Expected exactly one uniqueVariants loop, found {len(matches)}; "
  678. "refusing to patch an unknown shader_split.py version."
  679. )
  680.  
  681. i = matches[0]
  682. indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())]
  683.  
  684. lines[i] = (
  685. indent + "unique_variants = k.get('uniqueVariants')\n"
  686. + indent + "if unique_variants is None:\n"
  687. + indent + " print(f'[YELLOW] {base}#{o.get(\"m_Name\", \"?\")}#{k.get(\"name\", \"?\")}: ComputeShader kernel has no uniqueVariants field; skipping this kernel', flush=True)\n"
  688. + indent + " continue\n"
  689. + indent + "for i, u in enumerate(unique_variants):\n"
  690. )
  691.  
  692. path.write_text("".join(lines))
  693. print("Patched Unity compute shader scanning to skip kernels without uniqueVariants.")
  694. PY
  695.  
  696. # BUILD UNSIGNED IPA
  697. - name: Build unsigned IPA
  698. shell: bash
  699. run: |
  700. set -euo pipefail
  701.  
  702. WORK="$RUNNER_TEMP/nxnative"
  703. GAME_DIR="$RUNNER_TEMP/GameFiles"
  704. OUTPUT="$RUNNER_TEMP/NxNative.ipa"
  705.  
  706. python scripts/build_ipa_from_nsp.py \
  707. "$GAME_DIR" \
  708. -k "$WORK/prod.keys" \
  709. -o "$OUTPUT"
  710.  
  711. if [[ ! -s "$OUTPUT" ]]; then
  712. echo "::error::IPA build did not produce an output file."
  713. exit 1
  714. fi
  715.  
  716. echo "IPA successfully created: $OUTPUT"
  717. ls -lh "$OUTPUT"
  718.  
  719. # UPLOAD IPA
  720. - name: Upload IPA
  721. if: success()
  722. uses: actions/upload-artifact@v4
  723. with:
  724. name: NxNative-IPA-${{ inputs.device == 'iPhone 15 or older (BC fallback)' && 'BC-Fallback' || 'Standard' }}
  725. path: ${{ runner.temp }}/NxNative.ipa
  726. if-no-files-found: error
  727. retention-days: 7
  728.  
  729. # GENERATE BUILD DIAGNOSTICS
  730. - name: Generate build diagnostics
  731. if: always()
  732. env:
  733. DEVICE: ${{ inputs.device }}
  734. CONTENT_SELECTION: ${{ inputs.content }}
  735. BUILD_STATUS: ${{ job.status }}
  736. shell: bash
  737. run: |
  738. mkdir -p "$RUNNER_TEMP/NxNative-Diagnostics/build-logs"
  739.  
  740. REPORT="$RUNNER_TEMP/NxNative-Diagnostics/diagnostics.txt"
  741. GAME_DIR="$RUNNER_TEMP/GameFiles"
  742. WORK="$RUNNER_TEMP/nxnative"
  743.  
  744. {
  745. echo "=========================================="
  746. echo "NXNATIVE BUILD DIAGNOSTICS"
  747. echo "=========================================="
  748. echo ""
  749.  
  750. echo "BUILD INFORMATION"
  751. echo "-----------------"
  752. echo "Status: $BUILD_STATUS"
  753. echo "Device: $DEVICE"
  754. echo "Content selection: $CONTENT_SELECTION"
  755. echo "Profile selection: Automatic (nxnative profile lookup / survey fallback)"
  756. echo "Detected title ID: ${DETECTED_TITLE_ID:-Not detected}"
  757. echo "Runner OS: $RUNNER_OS"
  758. echo "Runner architecture: $(uname -m)"
  759. echo "Date: $(date -u)"
  760. echo ""
  761.  
  762. echo "SOURCE INFORMATION"
  763. echo "------------------"
  764. echo "Repository: $(git remote get-url origin 2>/dev/null || echo unknown)"
  765. echo "Commit: $(git rev-parse HEAD 2>/dev/null || echo unknown)"
  766. echo "Branch: $(git branch --show-current 2>/dev/null || echo unknown)"
  767. echo ""
  768.  
  769. echo "TOOL VERSIONS"
  770. echo "-------------"
  771. python --version 2>&1
  772. dotnet --version 2>&1 || true
  773. xcodebuild -version 2>&1 || true
  774.  
  775. brew list --versions 2>/dev/null | grep -E \
  776. 'xcodegen|zstd|spirv-cross|unar' || true
  777.  
  778. echo ""
  779.  
  780. echo "CACHE INFORMATION"
  781. echo "-----------------"
  782. echo "Cache key: ${{ steps.cache-key.outputs.key }}"
  783. echo "Cache hit: ${{ steps.download-cache.outputs.cache-hit }}"
  784. echo ""
  785.  
  786. echo "DOWNLOADED FILES"
  787. echo "----------------"
  788.  
  789. for f in \
  790. "$WORK/game.download" \
  791. "$WORK/update.download" \
  792. "$WORK/dlc.download"; do
  793.  
  794. if [[ -s "$f" ]]; then
  795. echo "$(basename "$f"): $(stat -f%z "$f") bytes"
  796. shasum -a 256 "$f"
  797. else
  798. echo "$(basename "$f"): Missing or not configured"
  799. fi
  800. done
  801.  
  802. if [[ -s "$WORK/prod.keys" ]]; then
  803. echo "prod.keys: Present (contents excluded)"
  804. echo "Key file size: $(stat -f%z "$WORK/prod.keys") bytes"
  805. else
  806. echo "prod.keys: Missing"
  807. fi
  808.  
  809. echo ""
  810. echo "PREPARED PACKAGES"
  811. echo "-----------------"
  812.  
  813. if [[ -d "$GAME_DIR" ]]; then
  814. find "$GAME_DIR" -maxdepth 1 -type f \
  815. -exec sh -c '
  816. for f do
  817. echo "$(basename "$f"): $(stat -f%z "$f") bytes"
  818. shasum -a 256 "$f"
  819. done
  820. ' sh {} +
  821. else
  822. echo "GameFiles directory was not created."
  823. fi
  824.  
  825. echo ""
  826. echo "IPA OUTPUT"
  827. echo "----------"
  828.  
  829. if [[ -s "$RUNNER_TEMP/NxNative.ipa" ]]; then
  830. ls -lh "$RUNNER_TEMP/NxNative.ipa"
  831. shasum -a 256 "$RUNNER_TEMP/NxNative.ipa"
  832. else
  833. echo "IPA was not created."
  834. fi
  835.  
  836. echo ""
  837. echo "DISK SPACE"
  838. echo "----------"
  839. df -h "$RUNNER_TEMP" 2>&1 || true
  840.  
  841. echo ""
  842. echo "DIAGNOSTICS COMPLETE"
  843. echo "=========================================="
  844.  
  845. } > "$REPORT" 2>&1
  846.  
  847. if [[ -d "$GAME_DIR" ]]; then
  848. find "$GAME_DIR" -type d \
  849. \( -name logs -o -name '*_survey' -o -name profile \) \
  850. -print0 |
  851. while IFS= read -r -d '' dir; do
  852. destination="$RUNNER_TEMP/NxNative-Diagnostics/build-logs/$(basename "$(dirname "$dir")")-$(basename "$dir")"
  853. mkdir -p "$destination"
  854. cp -R "$dir/." "$destination/" 2>/dev/null || true
  855. done
  856. fi
  857.  
  858. echo "Diagnostics report generated."
  859. cat "$REPORT"
  860.  
  861. # ALWAYS UPLOAD DIAGNOSTICS
  862. - name: Upload build diagnostics
  863. if: always()
  864. uses: actions/upload-artifact@v4
  865. with:
  866. name: NxNative-Build-Diagnostics
  867. path: ${{ runner.temp }}/NxNative-Diagnostics/
  868. if-no-files-found: warn
  869. retention-days: 14
  870.  
  871. # CLEANUP
  872. - name: Clean up downloaded files and keys
  873. if: always()
  874. shell: bash
  875. run: |
  876. rm -rf \
  877. "$RUNNER_TEMP/nxnative" \
  878. "$RUNNER_TEMP/GameFiles"
  879.  
Add Comment
Please, Sign In to add comment