Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- name: NxNative - Generic IPA Builder
- 'on':
- workflow_dispatch:
- inputs:
- device:
- description: "Choose the target device and texture support path"
- required: true
- type: choice
- default: "iPhone 15 or older (BC fallback)"
- options:
- - "iPhone 15 or older (BC fallback)"
- - "iPhone 15 Pro/Pro Max or newer (standard)"
- content:
- description: "Choose which game content to include"
- required: true
- type: choice
- default: "Base game + update + DLC"
- options:
- - "Base game only"
- - "Base game + update (no DLC)"
- - "Base game + DLC (no update)"
- - "Base game + update + DLC"
- permissions:
- contents: read
- jobs:
- build:
- # Both NxNative paths require an ARM64 macOS host.
- runs-on: macos-latest
- timeout-minutes: 360
- steps:
- # CHECKOUT
- - name: Checkout BC fallback version
- if: inputs.device == 'iPhone 15 or older (BC fallback)'
- uses: actions/checkout@v4
- with:
- repository: mianmuhammadse/nxnative
- ref: d158df611bf2f3fa315c4f16585575cdfdb2a1d3
- - name: Checkout standard nxnative version
- if: inputs.device == 'iPhone 15 Pro/Pro Max or newer (standard)'
- uses: actions/checkout@v4
- with:
- repository: seedds/nxnative
- ref: main
- # VERIFIED SDK MEMORY PATCHES
- - name: Add verified SDK memory patches (BC fallback)
- if: inputs.device == 'iPhone 15 or older (BC fallback)'
- shell: bash
- run: |
- set -euo pipefail
- UPSTREAM_COMMIT="777a5f6c618a38cd2d7f795f465466fb56920534"
- RAW="https://raw.githubusercontent.com/seedds/nxnative/$UPSTREAM_COMMIT/dm/tools"
- curl -fsSL "$RAW/nso_code.py" -o dm/tools/nso_code.py
- curl -fsSL "$RAW/nso_match.py" -o dm/tools/nso_match.py
- curl -fsSL "$RAW/sdk_patches.py" -o dm/tools/sdk_patches.py
- python - <<'PY'
- from pathlib import Path
- path = Path("dm/tools/patch_nso.py")
- source = path.read_text()
- old = """BUILD_ID = d[0x40:0x50].hex()
- cuts = list(CUTS.get(BUILD_ID, []))
- """
- new = """BUILD_ID = d[0x40:0x50].hex()
- cuts = list(CUTS.get(BUILD_ID, []))
- # Apply upstream's SDK-wide address-space workarounds only to SDK modules.
- # resolve() matches complete function fingerprints and verifies every
- # original instruction before the existing byte-checked cut loop runs.
- if NAME == 'sdk':
- import sdk_patches
- from nso_match import resolve
- cuts += resolve(IMG, TEXT_END, sdk_patches.MATCH_CUTS.get('sdk', []), red=False)
- """
- if source.count(old) != 1:
- raise SystemExit(
- "Could not find the expected SDK cut block in patch_nso.py; "
- "refusing to patch an unknown BC-fallback source version."
- )
- path.write_text(source.replace(old, new, 1))
- print("Installed verified upstream SDK function-matching patches for BC fallback.")
- PY
- # METAL HARDWARE DIAGNOSTICS
- - name: Inspect Mac runner Metal hardware (BC fallback)
- if: inputs.device == 'iPhone 15 or older (BC fallback)'
- shell: bash
- run: |
- set -euo pipefail
- echo "=== Runner identity ==="
- uname -a
- sw_vers
- echo "=== Display / GPU hardware ==="
- system_profiler SPDisplaysDataType
- echo "=== Xcode ==="
- xcodebuild -version
- - name: Add Metal residency-set diagnostics (BC fallback)
- if: inputs.device == 'iPhone 15 or older (BC fallback)'
- shell: bash
- run: |
- set -euo pipefail
- python - <<'PY'
- from pathlib import Path
- path = Path("dm/gfx/nvn/nvn_mtl.m")
- source = path.read_text()
- old = ''' g_q = [g_dev newCommandQueue];
- MTLResidencySetDescriptor *rd = [MTLResidencySetDescriptor new];
- rd.initialCapacity = 4096;
- NSError *re = nil;
- g_res = [g_dev newResidencySetWithDescriptor:rd error:&re];
- if (!g_res) MDIE("failed to create the MTLResidencySet");
- '''
- new = ''' MLOG("Metal device: %s; registryID=%llu; unifiedMemory=%d; maxBufferLength=%llu; recommendedMaxWorkingSetSize=%llu; OS=%s",
- g_dev.name.UTF8String,
- (unsigned long long)g_dev.registryID,
- g_dev.hasUnifiedMemory,
- (unsigned long long)g_dev.maxBufferLength,
- (unsigned long long)g_dev.recommendedMaxWorkingSetSize,
- NSProcessInfo.processInfo.operatingSystemVersionString.UTF8String);
- g_q = [g_dev newCommandQueue];
- MTLResidencySetDescriptor *rd = [MTLResidencySetDescriptor new];
- rd.initialCapacity = 4096;
- NSError *re = nil;
- g_res = [g_dev newResidencySetWithDescriptor:rd error:&re];
- if (!g_res) {
- MLOG("FATAL failed to create the MTLResidencySet");
- if (re) {
- MLOG("Metal residency-set NSError: domain=%s code=%ld description=%s userInfo=%s",
- re.domain.UTF8String, (long)re.code,
- re.localizedDescription.UTF8String,
- re.userInfo.description.UTF8String);
- } else {
- MLOG("Metal residency-set NSError was nil");
- }
- abort();
- }
- '''
- if old not in source:
- raise SystemExit(
- "Could not find the expected Metal residency-set initialization block; "
- "refusing to patch an unknown NxNative source version."
- )
- path.write_text(source.replace(old, new, 1))
- print("Added Metal device/OS and residency-set NSError diagnostics.")
- PY
- # ENVIRONMENT
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.13"
- - name: Set up .NET SDK
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: "10.0.x"
- - name: Install dependencies
- shell: bash
- run: |
- set -euo pipefail
- brew install xcodegen zstd spirv-cross unar
- python -m pip install --upgrade pip
- python -m pip install lz4 'capstone>=5,<6' fonttools backports.zstd
- # DOWNLOAD CACHE KEY
- - name: Prepare download cache
- id: cache-key
- env:
- GAME_URL: ${{ secrets.Game_URL }}
- UPDATE_URL: ${{ secrets.Update_URL }}
- DLC_URL: ${{ secrets.DLC_URL }}
- shell: bash
- run: |
- set -euo pipefail
- python - <<'PY'
- import hashlib
- import os
- urls = [
- os.environ.get("GAME_URL", "").strip(),
- os.environ.get("UPDATE_URL", "").strip(),
- os.environ.get("DLC_URL", "").strip(),
- ]
- if not urls[0]:
- raise SystemExit("Game_URL secret is empty.")
- cache_key = hashlib.sha256(
- "|".join(urls).encode()
- ).hexdigest()
- with open(os.environ["GITHUB_OUTPUT"], "a") as f:
- f.write(f"key=nxnative-downloads-{cache_key}\n")
- print("Download cache key prepared.")
- PY
- mkdir -p "$RUNNER_TEMP/nxnative/download-cache"
- - name: Restore downloaded game files
- id: download-cache
- uses: actions/cache/restore@v4
- with:
- path: ${{ runner.temp }}/nxnative/download-cache
- key: ${{ runner.os }}-${{ steps.cache-key.outputs.key }}
- # DOWNLOAD GAME / UPDATE / DLC AND DECODE KEYS
- - name: Download game files and decode keys
- env:
- GAME_URL: ${{ secrets.Game_URL }}
- UPDATE_URL: ${{ secrets.Update_URL }}
- DLC_URL: ${{ secrets.DLC_URL }}
- KEYS_BASE64: ${{ secrets.Keys_URL }}
- CONTENT_SELECTION: ${{ inputs.content }}
- shell: bash
- run: |
- set -euo pipefail
- WORK="$RUNNER_TEMP/nxnative"
- CACHE="$WORK/download-cache"
- mkdir -p "$WORK" "$CACHE"
- download_file() {
- local url="$1"
- local output="$2"
- local filename
- local cached
- local temporary
- filename="$(basename "$output")"
- cached="$CACHE/$filename"
- temporary="$output.part"
- if [[ -z "${url:-}" ]]; then
- echo "::error::Download URL is empty."
- exit 1
- fi
- if [[ -s "$cached" ]]; then
- echo "========================================"
- echo "CACHE HIT: $filename"
- echo "Reusing cached download."
- echo "========================================"
- cp "$cached" "$output"
- file "$output"
- return
- fi
- echo "========================================"
- echo "CACHE MISS: $filename"
- echo "Starting direct download..."
- echo "========================================"
- curl \
- --fail \
- --location \
- --show-error \
- --progress-bar \
- --retry 3 \
- --retry-delay 5 \
- --connect-timeout 30 \
- --output "$temporary" \
- "$url"
- if [[ ! -s "$temporary" ]]; then
- echo "::error::Downloaded file is empty."
- exit 1
- fi
- mv "$temporary" "$output"
- echo "Download completed:"
- ls -lh "$output"
- file "$output"
- cp "$output" "$cached"
- echo "Saved $filename to download cache."
- }
- download_file "$GAME_URL" "$WORK/game.download"
- case "${CONTENT_SELECTION:-}" in
- "Base game only")
- INCLUDE_UPDATE=false
- INCLUDE_DLC=false
- ;;
- "Base game + update (no DLC)")
- INCLUDE_UPDATE=true
- INCLUDE_DLC=false
- ;;
- "Base game + DLC (no update)")
- INCLUDE_UPDATE=false
- INCLUDE_DLC=true
- ;;
- "Base game + update + DLC")
- INCLUDE_UPDATE=true
- INCLUDE_DLC=true
- ;;
- *)
- echo "::error::Unknown content selection: ${CONTENT_SELECTION:-unset}"
- exit 1
- ;;
- esac
- if [[ "$INCLUDE_UPDATE" == true ]]; then
- if [[ -z "${UPDATE_URL:-}" ]]; then
- echo "::error::Update was selected, but UPDATE_URL is not configured."
- exit 1
- fi
- download_file "$UPDATE_URL" "$WORK/update.download"
- else
- echo "Update excluded by content selection; skipping."
- fi
- if [[ "$INCLUDE_DLC" == true ]]; then
- if [[ -z "${DLC_URL:-}" ]]; then
- echo "::error::DLC was selected, but DLC_URL is not configured."
- exit 1
- fi
- download_file "$DLC_URL" "$WORK/dlc.download"
- else
- echo "DLC excluded by content selection; skipping."
- fi
- if [[ -z "${KEYS_BASE64:-}" ]]; then
- echo "::error::Keys_URL secret is empty."
- exit 1
- fi
- echo "Decoding Base64 encryption keys..."
- python - "$WORK/prod.keys" <<'PY'
- import base64
- import os
- import re
- import sys
- from pathlib import Path
- encoded = os.environ["KEYS_BASE64"].strip()
- encoded = re.sub(r"\s+", "", encoded)
- try:
- decoded = base64.b64decode(encoded, validate=True)
- except Exception as exc:
- raise SystemExit(f"Invalid Base64 encryption keys: {exc}")
- if not decoded:
- raise SystemExit("Decoded keys file is empty.")
- output = Path(sys.argv[1])
- output.write_bytes(decoded)
- output.chmod(0o600)
- print(f"Successfully decoded keys file: {len(decoded):,} bytes")
- PY
- test -s "$WORK/prod.keys"
- echo "All downloads and key preparation completed."
- # SAVE DOWNLOAD CACHE
- - name: Save downloaded game files
- if: success() && steps.download-cache.outputs.cache-hit != 'true'
- uses: actions/cache/save@v4
- with:
- path: ${{ runner.temp }}/nxnative/download-cache
- key: ${{ runner.os }}-${{ steps.cache-key.outputs.key }}
- # EXTRACT RAR / VALIDATE PACKAGES
- - name: Extract RAR archives or accept direct game packages
- shell: bash
- run: |
- set -euo pipefail
- WORK="$RUNNER_TEMP/nxnative"
- GAME_DIR="$RUNNER_TEMP/GameFiles"
- mkdir -p "$GAME_DIR"
- python - "$WORK" "$GAME_DIR" <<'PY'
- import shutil
- import subprocess
- import sys
- from pathlib import Path
- work = Path(sys.argv[1])
- game_dir = Path(sys.argv[2])
- package_extensions = {".nsp", ".xci", ".nsz", ".xcz"}
- rar_signatures = (
- b"Rar!\x1a\x07\x00",
- b"Rar!\x1a\x07\x01\x00",
- )
- def validate_package(path):
- with path.open("rb") as f:
- magic = f.read(4)
- suffix = path.suffix.lower()
- if suffix in (".nsp", ".nsz") and magic != b"PFS0":
- raise SystemExit(f"Invalid {suffix} package signature: {path.name}")
- if suffix in (".xci", ".xcz") and magic != b"HEAD":
- raise SystemExit(f"Invalid {suffix} package signature: {path.name}")
- def add_package(source, label):
- target = game_dir / source.name
- if target.exists():
- raise SystemExit("Duplicate package filename: " + source.name)
- validate_package(source)
- shutil.copy2(source, target)
- print(
- f"Added {label}: {target.name} "
- f"({target.stat().st_size:,} bytes)"
- )
- def process_download(label):
- source = work / f"{label}.download"
- if not source.exists():
- print(f"No {label} download; skipping.")
- return
- with source.open("rb") as f:
- signature = f.read(8)
- is_rar = any(signature.startswith(magic) for magic in rar_signatures)
- if is_rar:
- extract_dir = work / f"extracted-{label}"
- extract_dir.mkdir(parents=True, exist_ok=True)
- print(f"{label}: RAR archive detected; extracting.")
- subprocess.run(
- ["unar", "-o", str(extract_dir), str(source)],
- check=True,
- )
- packages = sorted(
- p for p in extract_dir.rglob("*")
- if p.is_file() and p.suffix.lower() in package_extensions
- )
- if not packages:
- raise SystemExit(
- f"No NSP/XCI/NSZ/XCZ packages found in the {label} RAR archive."
- )
- for package in packages:
- add_package(package, label)
- else:
- print(f"{label}: not a RAR archive; checking as a direct package.")
- if signature[:4] == b"PFS0":
- extension = ".nsp"
- elif signature[:4] == b"HEAD":
- extension = ".xci"
- else:
- raise SystemExit(
- f"{label} is not a recognized RAR archive or direct NSP/XCI package."
- )
- target = game_dir / f"{label}{extension}"
- if target.exists():
- raise SystemExit(f"Output package already exists: {target.name}")
- shutil.copy2(source, target)
- validate_package(target)
- print(
- f"Added direct package: {target.name} "
- f"({target.stat().st_size:,} bytes)"
- )
- process_download("game")
- process_download("update")
- process_download("dlc")
- packages = sorted(
- p for p in game_dir.iterdir()
- if p.is_file() and p.suffix.lower() in package_extensions
- )
- if not packages:
- raise SystemExit("No game packages were prepared.")
- print("\n=== Prepared game packages ===")
- for package in packages:
- print(f"{package.name}: {package.stat().st_size:,} bytes")
- PY
- test -s "$WORK/prod.keys"
- # DETECT BASE TITLE ID
- - name: Detect game title ID and select shader profile
- shell: bash
- run: |
- set -euo pipefail
- GAME_DIR="$RUNNER_TEMP/GameFiles"
- DETECTED_ID="$(python - "$GAME_DIR" <<'PY'
- import re
- import struct
- import sys
- from pathlib import Path
- root = Path(sys.argv[1])
- extensions = {".nsp", ".xci", ".nsz", ".xcz"}
- def pfs0_names(path):
- try:
- with path.open("rb") as f:
- header = f.read(16)
- if len(header) != 16 or header[:4] != b"PFS0":
- return []
- count, string_size = struct.unpack_from("<II", header, 4)
- if count > 100000 or string_size > 100000000:
- return []
- entries = f.read(count * 0x18)
- strings = f.read(string_size)
- if len(entries) != count * 0x18:
- return []
- names = []
- for i in range(count):
- offset = struct.unpack_from(
- "<I", entries, i * 0x18 + 0x10
- )[0]
- if offset >= len(strings):
- continue
- end = strings.find(b"\0", offset)
- if end < 0:
- continue
- names.append(
- strings[offset:end].decode(errors="replace")
- )
- return names
- except (OSError, struct.error):
- return []
- candidates = set()
- for path in root.iterdir():
- if not path.is_file() or path.suffix.lower() not in extensions:
- continue
- for name in pfs0_names(path):
- match = re.fullmatch(
- r"([0-9A-Fa-f]{16})[0-9A-Fa-f]{16}\.tik",
- name,
- )
- if match:
- title_id = int(match.group(1), 16)
- if (title_id & 0xFFF) == 0:
- candidates.add(f"{title_id:016X}")
- match = re.search(r"\[([0-9A-Fa-f]{16})\]", path.name)
- if match:
- title_id = int(match.group(1), 16)
- if (title_id & 0xFFF) == 0:
- candidates.add(f"{title_id:016X}")
- if len(candidates) != 1:
- raise SystemExit(
- "Could not uniquely identify the base game's title ID. "
- f"Candidates found: {sorted(candidates)}. "
- "Check the base NSP and its ticket/filename."
- )
- print(next(iter(candidates)))
- PY
- )"
- echo "Detected base title ID: $DETECTED_ID"
- echo "NxNative will select an existing profile or generate a survey and provisional profile automatically."
- echo "DETECTED_TITLE_ID=$DETECTED_ID" >> "$GITHUB_ENV"
- # ADAPT SHADER TRANSLATION LOCK
- - name: Adapt shader translation lock to runner
- shell: bash
- run: |
- set -euo pipefail
- python - <<'PY'
- from pathlib import Path
- path = Path("scripts/build_ipa_from_nsp.py")
- source = path.read_text()
- old = """ PROG.stage('iOS shaders')
- if provisional:
- PROG.set(step='chain header')
- 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)
- chain = re.search(r'#define DMSH_CHAIN "(.*)"', g.table('dmsh_chain.h').read_text()).group(1)
- """
- new = """ PROG.stage('iOS shaders')
- PROG.set(step='toolchain lock')
- run([py, repo / 'dm/gfx/tools/dmsh_chain.py', '--update-lock'], logs / 'dmsh_chain_lock.log', env=env)
- PROG.set(step='chain header')
- run([py, repo / 'dm/gfx/tools/dmsh_chain.py', '--header'], logs / 'dmsh_chain.log', env=env)
- chain = re.search(r'#define DMSH_CHAIN "(.*)"', g.table('dmsh_chain.h').read_text()).group(1)
- """
- if old not in source:
- raise SystemExit(
- "Could not find the expected nxnative shader-chain block; "
- "refusing to patch an unknown source version."
- )
- path.write_text(source.replace(old, new, 1))
- print("Patched shader build to refresh the tool lock and chain header.")
- PY
- # HANDLE UNITY COMPUTE KERNELS WITHOUT uniqueVariants
- - name: Handle Unity compute kernels without uniqueVariants
- shell: bash
- run: |
- set -euo pipefail
- python - <<'PY'
- from pathlib import Path
- path = Path("dm/gfx/tools/shader_split.py")
- lines = path.read_text().splitlines(keepends=True)
- target = "for i, u in enumerate(k['uniqueVariants']):"
- matches = [i for i, line in enumerate(lines) if line.strip() == target]
- if len(matches) != 1:
- raise SystemExit(
- f"Expected exactly one uniqueVariants loop, found {len(matches)}; "
- "refusing to patch an unknown shader_split.py version."
- )
- i = matches[0]
- indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())]
- lines[i] = (
- indent + "unique_variants = k.get('uniqueVariants')\n"
- + indent + "if unique_variants is None:\n"
- + indent + " print(f'[YELLOW] {base}#{o.get(\"m_Name\", \"?\")}#{k.get(\"name\", \"?\")}: ComputeShader kernel has no uniqueVariants field; skipping this kernel', flush=True)\n"
- + indent + " continue\n"
- + indent + "for i, u in enumerate(unique_variants):\n"
- )
- path.write_text("".join(lines))
- print("Patched Unity compute shader scanning to skip kernels without uniqueVariants.")
- PY
- # BUILD UNSIGNED IPA
- - name: Build unsigned IPA
- shell: bash
- run: |
- set -euo pipefail
- WORK="$RUNNER_TEMP/nxnative"
- GAME_DIR="$RUNNER_TEMP/GameFiles"
- OUTPUT="$RUNNER_TEMP/NxNative.ipa"
- python scripts/build_ipa_from_nsp.py \
- "$GAME_DIR" \
- -k "$WORK/prod.keys" \
- -o "$OUTPUT"
- if [[ ! -s "$OUTPUT" ]]; then
- echo "::error::IPA build did not produce an output file."
- exit 1
- fi
- echo "IPA successfully created: $OUTPUT"
- ls -lh "$OUTPUT"
- # UPLOAD IPA
- - name: Upload IPA
- if: success()
- uses: actions/upload-artifact@v4
- with:
- name: NxNative-IPA-${{ inputs.device == 'iPhone 15 or older (BC fallback)' && 'BC-Fallback' || 'Standard' }}
- path: ${{ runner.temp }}/NxNative.ipa
- if-no-files-found: error
- retention-days: 7
- # GENERATE BUILD DIAGNOSTICS
- - name: Generate build diagnostics
- if: always()
- env:
- DEVICE: ${{ inputs.device }}
- CONTENT_SELECTION: ${{ inputs.content }}
- BUILD_STATUS: ${{ job.status }}
- shell: bash
- run: |
- mkdir -p "$RUNNER_TEMP/NxNative-Diagnostics/build-logs"
- REPORT="$RUNNER_TEMP/NxNative-Diagnostics/diagnostics.txt"
- GAME_DIR="$RUNNER_TEMP/GameFiles"
- WORK="$RUNNER_TEMP/nxnative"
- {
- echo "=========================================="
- echo "NXNATIVE BUILD DIAGNOSTICS"
- echo "=========================================="
- echo ""
- echo "BUILD INFORMATION"
- echo "-----------------"
- echo "Status: $BUILD_STATUS"
- echo "Device: $DEVICE"
- echo "Content selection: $CONTENT_SELECTION"
- echo "Profile selection: Automatic (nxnative profile lookup / survey fallback)"
- echo "Detected title ID: ${DETECTED_TITLE_ID:-Not detected}"
- echo "Runner OS: $RUNNER_OS"
- echo "Runner architecture: $(uname -m)"
- echo "Date: $(date -u)"
- echo ""
- echo "SOURCE INFORMATION"
- echo "------------------"
- echo "Repository: $(git remote get-url origin 2>/dev/null || echo unknown)"
- echo "Commit: $(git rev-parse HEAD 2>/dev/null || echo unknown)"
- echo "Branch: $(git branch --show-current 2>/dev/null || echo unknown)"
- echo ""
- echo "TOOL VERSIONS"
- echo "-------------"
- python --version 2>&1
- dotnet --version 2>&1 || true
- xcodebuild -version 2>&1 || true
- brew list --versions 2>/dev/null | grep -E \
- 'xcodegen|zstd|spirv-cross|unar' || true
- echo ""
- echo "CACHE INFORMATION"
- echo "-----------------"
- echo "Cache key: ${{ steps.cache-key.outputs.key }}"
- echo "Cache hit: ${{ steps.download-cache.outputs.cache-hit }}"
- echo ""
- echo "DOWNLOADED FILES"
- echo "----------------"
- for f in \
- "$WORK/game.download" \
- "$WORK/update.download" \
- "$WORK/dlc.download"; do
- if [[ -s "$f" ]]; then
- echo "$(basename "$f"): $(stat -f%z "$f") bytes"
- shasum -a 256 "$f"
- else
- echo "$(basename "$f"): Missing or not configured"
- fi
- done
- if [[ -s "$WORK/prod.keys" ]]; then
- echo "prod.keys: Present (contents excluded)"
- echo "Key file size: $(stat -f%z "$WORK/prod.keys") bytes"
- else
- echo "prod.keys: Missing"
- fi
- echo ""
- echo "PREPARED PACKAGES"
- echo "-----------------"
- if [[ -d "$GAME_DIR" ]]; then
- find "$GAME_DIR" -maxdepth 1 -type f \
- -exec sh -c '
- for f do
- echo "$(basename "$f"): $(stat -f%z "$f") bytes"
- shasum -a 256 "$f"
- done
- ' sh {} +
- else
- echo "GameFiles directory was not created."
- fi
- echo ""
- echo "IPA OUTPUT"
- echo "----------"
- if [[ -s "$RUNNER_TEMP/NxNative.ipa" ]]; then
- ls -lh "$RUNNER_TEMP/NxNative.ipa"
- shasum -a 256 "$RUNNER_TEMP/NxNative.ipa"
- else
- echo "IPA was not created."
- fi
- echo ""
- echo "DISK SPACE"
- echo "----------"
- df -h "$RUNNER_TEMP" 2>&1 || true
- echo ""
- echo "DIAGNOSTICS COMPLETE"
- echo "=========================================="
- } > "$REPORT" 2>&1
- if [[ -d "$GAME_DIR" ]]; then
- find "$GAME_DIR" -type d \
- \( -name logs -o -name '*_survey' -o -name profile \) \
- -print0 |
- while IFS= read -r -d '' dir; do
- destination="$RUNNER_TEMP/NxNative-Diagnostics/build-logs/$(basename "$(dirname "$dir")")-$(basename "$dir")"
- mkdir -p "$destination"
- cp -R "$dir/." "$destination/" 2>/dev/null || true
- done
- fi
- echo "Diagnostics report generated."
- cat "$REPORT"
- # ALWAYS UPLOAD DIAGNOSTICS
- - name: Upload build diagnostics
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: NxNative-Build-Diagnostics
- path: ${{ runner.temp }}/NxNative-Diagnostics/
- if-no-files-found: warn
- retention-days: 14
- # CLEANUP
- - name: Clean up downloaded files and keys
- if: always()
- shell: bash
- run: |
- rm -rf \
- "$RUNNER_TEMP/nxnative" \
- "$RUNNER_TEMP/GameFiles"
Add Comment
Please, Sign In to add comment