diff --git a/.github/workflows/build-nkit.yml b/.github/workflows/build-nkit.yml new file mode 100644 index 0000000..b39b589 --- /dev/null +++ b/.github/workflows/build-nkit.yml @@ -0,0 +1,260 @@ +name: Build NKit (NativeAOT Single File) + +on: + workflow_dispatch: + inputs: + source_ref: + description: "NKit git tag or commit SHA (e.g. v3.1.0)" + required: true + default: "v3.1.0" + dry_run: + description: "Dry run publish to SourceForge" + type: boolean + required: false + default: true + +permissions: + contents: write + pull-requests: write + +jobs: + build: + name: Build NKit (${ matrix.target.os }/${ matrix.target.arch }) + runs-on: ${ matrix.target.runner } + strategy: + fail-fast: false + matrix: + target: + - os: linux + arch: x86_64 + runner: ubuntu-22.04 + rid: linux-x64 + binary_name: nkit + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + rid: linux-arm64 + binary_name: nkit + - os: macos + arch: arm64 + runner: macos-15 + rid: osx-arm64 + binary_name: nkit + - os: macos + arch: x86_64 + runner: macos-15-intel + rid: osx-x64 + binary_name: nkit + - os: windows + arch: x86_64 + runner: windows-2022 + rid: win-x64 + binary_name: nkit.exe + + steps: + - name: Checkout OpenROM-bins + uses: actions/checkout@v4 + + - name: Validate source_ref Input + env: + REF: ${ inputs.source_ref } + run: | + python3 -c ' + import os, sys, re + ref = os.environ.get("REF", "") + if not re.match(r"^[A-Za-z0-9._/-]+$", ref): + sys.exit("Invalid source_ref input: " + ref) + print("source_ref validated:", ref) + ' + + - name: Setup .NET SDK 10.0.x + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Clone NKit Repository + env: + REF: ${ inputs.source_ref } + run: | + git clone https://github.com/Nanook/NKit.git nkit_src + cd nkit_src + git checkout "$REF" + COMMIT_SHA=$(git rev-parse HEAD) + echo "SOURCE_COMMIT=$COMMIT_SHA" >> $GITHUB_ENV + + - name: Apply NKit Patches + run: | + if [ -d "patches/nkit" ]; then + for patch in patches/nkit/*.patch; do + if [ -f "$patch" ]; then + echo "Applying patch: $patch" + git -C nkit_src apply "../$patch" + fi + done + fi + + - name: Download & Inject GrindCore Native Libraries + run: | + python3 -c ' + import urllib.request, json, zipfile, os, shutil + req = urllib.request.Request("https://api.github.com/repos/Nanook/GrindCore/releases/latest", headers={"User-Agent": "curl/8.5.0"}) + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + url = None + for asset in data.get("assets", []): + if asset.get("name") == "grindcore.zip": + url = asset.get("browser_download_url") + break + if not url: + raise RuntimeError("grindcore.zip not found in latest GrindCore release") + + print("Downloading GrindCore from:", url) + os.makedirs("/tmp/gc", exist_ok=True) + zip_path = "/tmp/gc/grindcore.zip" + with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "curl/8.5.0"})) as resp, open(zip_path, "wb") as f: + f.write(resp.read()) + + with zipfile.ZipFile(zip_path, "r") as z: + z.extractall("/tmp/gc/extracted") + + rid = "${ matrix.target.rid }" + static_name = "GrindCore.lib" if "win-" in rid else "libGrindCore.a" + src_file = os.path.join("/tmp/gc/extracted", rid, "native", static_name) + if os.path.exists(src_file): + dest_dir = os.path.expanduser(f"~/.nuget/packages/grindcore/0.9.0/runtimes/{rid}/native") + os.makedirs(dest_dir, exist_ok=True) + shutil.copy(src_file, os.path.join(dest_dir, static_name)) + print("Injected GrindCore static library successfully.") + else: + print("Warning: Static GrindCore lib not found at", src_file) + ' + + - name: Publish NKit with NativeAOT + run: | + cd nkit_src + dotnet publish NKitApp/NKitApp.csproj \ + -c Release \ + -r ${ matrix.target.rid } \ + -p:PublishAot=true \ + -p:InvariantGlobalization=true \ + -p:StripSymbols=true \ + -p:DebugType=none \ + -p:IlcOptimizationPreference=Size \ + -o publish_out + + - name: Enforce NKit Single File & Clean Sibling Artifacts + run: | + cd nkit_src/publish_out + BIN_NAME="${ matrix.target.binary_name }" + mkdir -p ../../artifact_dir + cp "$BIN_NAME" "../../artifact_dir/$BIN_NAME" + + FILES=$(ls -1 ../../artifact_dir) + NUM_FILES=$(echo "$FILES" | wc -l) + if [ "$NUM_FILES" -ne 1 ]; then + echo "Error: NKit artifact must contain exactly 1 file, found $NUM_FILES" >&2 + exit 1 + fi + + - name: Ad-hoc Sign Executable (macOS only) + if: matrix.target.os == macos + run: | + codesign --force --deep --sign - nkit_src/artifact_dir/nkit + codesign -dv nkit_src/artifact_dir/nkit + + - name: Standalone Smoke Test (No .NET Environment) + shell: bash + run: | + mkdir -p /tmp/nkit_smoke + cp nkit_src/artifact_dir/${ matrix.target.binary_name } /tmp/nkit_smoke/ + cd /tmp/nkit_smoke + + export DOTNET_ROOT="" + export PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "dotnet" | tr "\n" ":") + + BIN="./${ matrix.target.binary_name }" + chmod +x "$BIN" + echo "Running smoke test: $BIN --help" + OUT=$("$BIN" --help 2>&1) + if [ -z "$OUT" ]; then + echo "Error: NKit smoke test produced empty output!" >&2 + exit 1 + fi + echo "Smoke test passed output snippet:" + echo "$OUT" | head -n 10 + + - name: Upload NKit Artifact + uses: actions/upload-artifact@v4 + with: + name: nkit-${ matrix.target.os }-${ matrix.target.arch } + path: nkit_src/artifact_dir/${ matrix.target.binary_name } + + publish-linux-x64: + needs: build + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: x86_64 + tool: nkit + artifact_name: nkit-linux-x86_64 + version: "3.1.0" + source_ref: ${ inputs.source_ref } + toolchain: ".NET SDK 10.0.x NativeAOT" + build_flavor: nativeaot + dry_run: ${ inputs.dry_run } + + publish-linux-arm64: + needs: build + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: arm64 + tool: nkit + artifact_name: nkit-linux-arm64 + version: "3.1.0" + source_ref: ${ inputs.source_ref } + toolchain: ".NET SDK 10.0.x NativeAOT" + build_flavor: nativeaot + dry_run: ${ inputs.dry_run } + + publish-macos-arm64: + needs: build + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: macos + arch: arm64 + tool: nkit + artifact_name: nkit-macos-arm64 + version: "3.1.0" + source_ref: ${ inputs.source_ref } + toolchain: ".NET SDK 10.0.x NativeAOT" + build_flavor: nativeaot + dry_run: ${ inputs.dry_run } + + publish-macos-x64: + needs: build + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: macos + arch: x86_64 + tool: nkit + artifact_name: nkit-macos-x86_64 + version: "3.1.0" + source_ref: ${ inputs.source_ref } + toolchain: ".NET SDK 10.0.x NativeAOT" + build_flavor: nativeaot + dry_run: ${ inputs.dry_run } + + publish-windows-x64: + needs: build + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: windows + arch: x86_64 + tool: nkit + artifact_name: nkit-windows-x86_64 + version: "3.1.0" + source_ref: ${ inputs.source_ref } + toolchain: ".NET SDK 10.0.x NativeAOT" + build_flavor: nativeaot + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/build-saturn-patcher.yml b/.github/workflows/build-saturn-patcher.yml index c2868a9..8dd110f 100644 --- a/.github/workflows/build-saturn-patcher.yml +++ b/.github/workflows/build-saturn-patcher.yml @@ -3,214 +3,84 @@ name: Build saturn-patcher on: workflow_dispatch: inputs: + source_ref: + description: "saturn-patcher git tag or 40-char SHA (e.g. 0.1.0)" + required: true + default: "0.1.0" version: - description: 'Version tag e.g. 0.1.0' + description: "Version string (e.g. 0.1.0)" required: true - default: '0.1.0' + default: "0.1.0" + dry_run: + description: "Dry run publish to SourceForge" + type: boolean + required: false + default: true permissions: contents: write + pull-requests: write jobs: - - # ── Linux x86_64 ────────────────────────────────────────────────────────── build-linux-x64: runs-on: ubuntu-22.04 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-unknown-linux-musl - - - name: Install musl tools - run: sudo apt-get install -y -q musl-tools + uses: actions/checkout@v4 - - name: Clone saturn-patcher - run: git clone --depth 1 https://github.com/ralfguth/saturn-patcher-egui.git saturn-src - - - name: Build CLI (static musl) + - name: Validate source_ref Input + env: + REF: ${ inputs.source_ref } run: | - cd saturn-src - cargo build --release --bin saturn-patcher --target x86_64-unknown-linux-musl - mkdir -p ../linux/x86_64 - cp target/x86_64-unknown-linux-musl/release/saturn-patcher ../linux/x86_64/saturn-patcher - chmod +x ../linux/x86_64/saturn-patcher - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: saturn-patcher-linux-x64 - path: linux/x86_64/saturn-patcher - retention-days: 1 - - # ── Linux ARM64 ─────────────────────────────────────────────────────────── - build-linux-arm64: - runs-on: ubuntu-24.04-arm - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + python3 -c ' + import os, sys, re + ref = os.environ.get("REF", "") + if not re.match(r"^[A-Za-z0-9._/-]+$", ref): + sys.exit("Invalid source_ref input: " + ref) + print("source_ref validated:", ref) + ' - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.85.0 with: - targets: aarch64-unknown-linux-musl + targets: x86_64-unknown-linux-musl - name: Install musl tools run: sudo apt-get install -y -q musl-tools - name: Clone saturn-patcher - run: git clone --depth 1 https://github.com/ralfguth/saturn-patcher-egui.git saturn-src - - - name: Build CLI (static musl) - run: | - cd saturn-src - cargo build --release --bin saturn-patcher --target aarch64-unknown-linux-musl - mkdir -p ../linux/arm64 - cp target/aarch64-unknown-linux-musl/release/saturn-patcher ../linux/arm64/saturn-patcher - chmod +x ../linux/arm64/saturn-patcher - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: saturn-patcher-linux-arm64 - path: linux/arm64/saturn-patcher - retention-days: 1 - - # ── macOS ARM64 ─────────────────────────────────────────────────────────── - build-macos-arm64: - runs-on: macos-15 - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Clone saturn-patcher - run: git clone --depth 1 https://github.com/ralfguth/saturn-patcher-egui.git saturn-src - - - name: Build CLI - run: | - cd saturn-src - cargo build --release --bin saturn-patcher - mkdir -p ../macos/arm64 - cp target/release/saturn-patcher ../macos/arm64/saturn-patcher - chmod +x ../macos/arm64/saturn-patcher - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: saturn-patcher-macos-arm64 - path: macos/arm64/saturn-patcher - retention-days: 1 - - # ── macOS x86_64 ────────────────────────────────────────────────────────── - build-macos-x64: - runs-on: macos-15-intel - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Clone saturn-patcher - run: git clone --depth 1 https://github.com/ralfguth/saturn-patcher-egui.git saturn-src - - - name: Build CLI + env: + REF: ${ inputs.source_ref } run: | - cd saturn-src - cargo build --release --bin saturn-patcher - mkdir -p ../macos/x86_64 - cp target/release/saturn-patcher ../macos/x86_64/saturn-patcher - chmod +x ../macos/x86_64/saturn-patcher + git clone https://github.com/M5Devs/saturn-patcher.git saturn_src + cd saturn_src + git checkout "$REF" + COMMIT_SHA=$(git rev-parse HEAD) + echo "SOURCE_COMMIT=$COMMIT_SHA" >> $GITHUB_ENV - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: saturn-patcher-macos-x64 - path: macos/x86_64/saturn-patcher - retention-days: 1 - - # ── Windows x86_64 ──────────────────────────────────────────────────────── - build-windows-x64: - runs-on: windows-2022 - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Clone saturn-patcher - run: git clone --depth 1 https://github.com/ralfguth/saturn-patcher-egui.git saturn-src - - - name: Build CLI + - name: Build musl binary run: | - cd saturn-src - cargo build --release --bin saturn-patcher - New-Item -ItemType Directory -Force -Path ..\windows\x86_64 | Out-Null - Copy-Item target\release\saturn-patcher.exe ..\windows\x86_64\saturn-patcher.exe + cd saturn_src + cargo build --release --target x86_64-unknown-linux-musl + mkdir -p ../artifact_dir + cp target/x86_64-unknown-linux-musl/release/saturn-patcher ../artifact_dir/saturn-patcher - - name: Upload artifact + - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: saturn-patcher-windows-x64 - path: windows/x86_64/saturn-patcher.exe - retention-days: 1 - - # ── Collect and commit ──────────────────────────────────────────────────── - commit: - name: Commit all binaries - needs: [build-linux-x64, build-linux-arm64, build-macos-arm64, build-macos-x64, build-windows-x64] - runs-on: ubuntu-22.04 - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Place binaries - run: | - mkdir -p linux/x86_64 linux/arm64 macos/arm64 macos/x86_64 windows/x86_64 - - cp artifacts/saturn-patcher-linux-x64/saturn-patcher linux/x86_64/saturn-patcher - cp artifacts/saturn-patcher-linux-arm64/saturn-patcher linux/arm64/saturn-patcher - cp artifacts/saturn-patcher-macos-arm64/saturn-patcher macos/arm64/saturn-patcher - cp artifacts/saturn-patcher-macos-x64/saturn-patcher macos/x86_64/saturn-patcher - cp artifacts/saturn-patcher-windows-x64/saturn-patcher.exe windows/x86_64/saturn-patcher.exe - - chmod +x linux/x86_64/saturn-patcher linux/arm64/saturn-patcher \ - macos/arm64/saturn-patcher macos/x86_64/saturn-patcher - - - name: Update versions.json - env: - VERSION: ${{ github.event.inputs.version }} - run: | - python3 -c " - import json - with open('versions.json') as f: - data = json.load(f) - data['saturn-patcher'] = '$VERSION' - with open('versions.json', 'w') as f: - json.dump(data, f, indent=2) - " - - - name: Commit and push - env: - VERSION: ${{ github.event.inputs.version }} - run: | - git config user.email "ci@github.com" - git config user.name "GitHub CI" - git add -f linux/x86_64/saturn-patcher linux/arm64/saturn-patcher \ - macos/arm64/saturn-patcher macos/x86_64/saturn-patcher \ - windows/x86_64/saturn-patcher.exe versions.json - git commit -m "feat: add saturn-patcher v${VERSION} (SSP patch support)" - git push origin main - echo "✅ Done" + name: saturn-patcher-linux-x86_64 + path: artifact_dir/saturn-patcher + + publish-linux-x64: + needs: build-linux-x64 + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: x86_64 + tool: saturn-patcher + artifact_name: saturn-patcher-linux-x86_64 + version: ${ inputs.version } + source_ref: ${ inputs.source_ref } + toolchain: "rustc 1.85.0 (x86_64-unknown-linux-musl)" + build_flavor: release + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/build-xgdtool.yml b/.github/workflows/build-xgdtool.yml index c0c8aa9..f6b5a6d 100644 --- a/.github/workflows/build-xgdtool.yml +++ b/.github/workflows/build-xgdtool.yml @@ -3,61 +3,42 @@ name: Build XGDTool on: workflow_dispatch: inputs: + source_ref: + description: "XGDTool git tag or 40-char SHA (e.g. 1.0.0)" + required: true + default: "1.0.0" version: - description: 'Version tag e.g. 1.0.0' + description: "Version string (e.g. 1.0.0)" required: true - default: '1.0.0' + default: "1.0.0" + dry_run: + description: "Dry run publish to SourceForge" + type: boolean + required: false + default: true permissions: contents: write + pull-requests: write jobs: - - # ── Linux x86_64 ────────────────────────────────────────────────────────── build-linux-x64: runs-on: ubuntu-22.04 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Install dependencies - run: | - sudo apt-get update -qq - sudo apt-get install -y clang cmake make pkg-config \ - liblz4-dev libzstd-dev libssl-dev libcurl4-openssl-dev - - - name: Clone XGDTool - run: git clone --recursive https://github.com/wiredopposite/XGDTool.git - - - name: Configure (CLI only) - run: | - cd XGDTool && mkdir build && cd build - cmake -DENABLE_GUI=OFF -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. - - - name: Build - run: | - cd XGDTool/build - make -j$(nproc) + uses: actions/checkout@v4 - - name: Copy binary + - name: Validate source_ref Input + env: + REF: ${ inputs.source_ref } run: | - mkdir -p linux/x86_64 - cp XGDTool/build/XGDTool linux/x86_64/XGDTool - chmod +x linux/x86_64/XGDTool - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xgdtool-linux-x64 - path: linux/x86_64/XGDTool - retention-days: 1 - - # ── Linux ARM64 ─────────────────────────────────────────────────────────── - build-linux-arm64: - runs-on: ubuntu-22.04-arm - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + python3 -c ' + import os, sys, re + ref = os.environ.get("REF", "") + if not re.match(r"^[A-Za-z0-9._/-]+$", ref): + sys.exit("Invalid source_ref input: " + ref) + print("source_ref validated:", ref) + ' - name: Install dependencies run: | @@ -66,108 +47,43 @@ jobs: liblz4-dev libzstd-dev libssl-dev libcurl4-openssl-dev - name: Clone XGDTool - run: git clone --recursive https://github.com/wiredopposite/XGDTool.git - - - name: Configure (CLI only) + env: + REF: ${ inputs.source_ref } run: | - cd XGDTool && mkdir build && cd build - cmake -DENABLE_GUI=OFF -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. - - - name: Build + git clone --recursive https://github.com/wiredopposite/XGDTool.git xgd_src + cd xgd_src + git checkout "$REF" + git submodule update --init --recursive + COMMIT_SHA=$(git rev-parse HEAD) + echo "SOURCE_COMMIT=$COMMIT_SHA" >> $GITHUB_ENV + + - name: Build XGDTool run: | - cd XGDTool/build - make -j$(nproc) - - - name: Copy binary - run: | - mkdir -p linux/arm64 - cp XGDTool/build/XGDTool linux/arm64/XGDTool - chmod +x linux/arm64/XGDTool - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: xgdtool-linux-arm64 - path: linux/arm64/XGDTool - retention-days: 1 - - # ── Windows x86_64 ──────────────────────────────────────────────────────── - build-windows-x64: - runs-on: windows-2022 - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Clone XGDTool - run: git clone --recursive https://github.com/wiredopposite/XGDTool.git - - - name: Configure (CLI only, no GUI) - run: | - cd XGDTool + cd xgd_src mkdir build && cd build - cmake -S .. -B . -DENABLE_GUI=OFF -G "Visual Studio 17 2022" -A x64 - - - name: Build - run: | - cd XGDTool/build - cmake --build . --config Release - - - name: Copy binary - run: | - mkdir -p windows/x86_64 - copy XGDTool\build\Release\XGDTool.exe windows\x86_64\XGDTool.exe + cmake -DCMAKE_BUILD_TYPE=Release .. + make -j$(nproc) + mkdir -p ../../artifact_dir + cp XGDTool ../../artifact_dir/XGDTool + echo "Shared library dependencies (ldd):" + ldd ../../artifact_dir/XGDTool || true - - name: Upload artifact + - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: xgdtool-windows-x64 - path: windows/x86_64/XGDTool.exe - retention-days: 1 - - # ── Collect and commit ──────────────────────────────────────────────────── - commit: - name: Commit all binaries - needs: [build-linux-x64, build-linux-arm64, build-windows-x64] - runs-on: ubuntu-22.04 - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Place binaries - run: | - mkdir -p linux/x86_64 linux/arm64 windows/x86_64 - - cp artifacts/xgdtool-linux-x64/XGDTool linux/x86_64/XGDTool - cp artifacts/xgdtool-linux-arm64/XGDTool linux/arm64/XGDTool - cp artifacts/xgdtool-windows-x64/XGDTool.exe windows/x86_64/XGDTool.exe - - chmod +x linux/x86_64/XGDTool linux/arm64/XGDTool - - - name: Update versions.json - env: - VERSION: ${{ github.event.inputs.version }} - run: | - python3 -c " - import json - with open('versions.json') as f: - data = json.load(f) - data['xgdtool'] = '$VERSION' - with open('versions.json', 'w') as f: - json.dump(data, f, indent=2) - " - - - name: Commit - env: - VERSION: ${{ github.event.inputs.version }} - run: | - git config user.email "ci@github.com" - git config user.name "GitHub CI" - git add -f linux/x86_64/XGDTool linux/arm64/XGDTool \ - windows/x86_64/XGDTool.exe versions.json - git commit -m "feat: add XGDTool v${VERSION} binaries" - echo "Done" + name: xgdtool-linux-x86_64 + path: artifact_dir/XGDTool + + publish-linux-x64: + needs: build-linux-x64 + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: x86_64 + tool: xgdtool + artifact_name: xgdtool-linux-x86_64 + version: ${ inputs.version } + source_ref: ${ inputs.source_ref } + toolchain: "GCC / CMake Linux" + build_flavor: release + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/check-all-versions.yml b/.github/workflows/check-all-versions.yml index db04159..566f8ff 100644 --- a/.github/workflows/check-all-versions.yml +++ b/.github/workflows/check-all-versions.yml @@ -4,192 +4,103 @@ on: workflow_dispatch: jobs: - - # ── Windows ────────────────────────────────────────────────────────────────── check-windows: - name: Check Windows Binaries + name: Check Windows x86_64 runs-on: windows-2022 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 - - name: Check versions + - name: Fetch Windows Binaries shell: pwsh run: | - $bins = @( - @{ path="windows\x86_64\chdman.exe"; name="chdman" }, - @{ path="windows\x86_64\maxcso.exe"; name="maxcso" }, - @{ path="windows\x86_64\nodtool.exe"; name="nodtool" }, - @{ path="windows\x86_64\extract-xiso.exe"; name="extract-xiso" }, - @{ path="windows\x86_64\xdelta3.exe"; name="xdelta3" }, - @{ path="windows\x86_64\ecm.exe"; name="ecm" }, - @{ path="windows\x86_64\unecm.exe"; name="unecm" }, - @{ path="windows\x86_64\nkit\nkit.exe"; name="nkit" }, - @{ path="windows\x86_64\XGDTool.exe"; name="xgdtool" } - ) - - Write-Host "========================================" - Write-Host "Windows x86_64" - Write-Host "========================================" + .\scripts\fetch-bins.ps1 -Os "windows" -Arch "x86_64" -TargetDir ".\bin" + - name: Test Execution & NKit Standalone + shell: pwsh + run: | + $env:DOTNET_ROOT = "" + $bins = Get-ChildItem ".\bin" foreach ($b in $bins) { - if (Test-Path $b.path) { - $ver = $null - foreach ($flag in @("--version", "-version", "")) { - try { - if ($flag) { - $ver = (& ".\$($b.path)" $flag 2>&1) | Select-Object -First 1 - } else { - $ver = (& ".\$($b.path)" 2>&1) | Select-Object -First 1 - } - if ($ver -and $ver -notmatch "^$") { break } - } catch {} - } - $fv = (Get-Item ".\$($b.path)").VersionInfo.FileVersion - $size = [math]::Round((Get-Item ".\$($b.path)").Length / 1KB, 1) - Write-Host "$($b.name.PadRight(15)) | $("$ver".Trim().Substring(0, [Math]::Min("$ver".Trim().Length, 60)).PadRight(60)) | PE: $fv | ${size} KB" - } else { - Write-Host "$($b.name.PadRight(15)) | NOT FOUND" - } + Write-Host "Testing execution for $($b.Name) ..." + & $b.FullName --help } - # ── Linux x86_64 ───────────────────────────────────────────────────────────── check-linux-x64: - name: Check Linux x86_64 Binaries + name: Check Linux x86_64 runs-on: ubuntu-22.04 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 + + - name: Fetch Linux x86_64 Binaries + run: | + ./scripts/fetch-bins.sh --os linux --arch x86_64 --target-dir ./bin - - name: Check versions + - name: Test Execution & Scrubbed Env run: | - BINS=( - "linux/x86_64/chdman:chdman" - "linux/x86_64/maxcso:maxcso" - "linux/x86_64/nodtool:nodtool" - "linux/x86_64/extract-xiso:extract-xiso" - "linux/x86_64/xdelta3:xdelta3" - "linux/x86_64/ecm:ecm" - "linux/x86_64/unecm:unecm" - "linux/x86_64/nkit/nkit:nkit" - "linux/x86_64/XGDTool:xgdtool" - ) - - echo "========================================" - echo "Linux x86_64" - echo "========================================" - - for entry in "${BINS[@]}"; do - path="${entry%%:*}" - name="${entry##*:}" - if [ -f "$path" ]; then - chmod +x "$path" - ver=$(./"$path" --version 2>&1 | head -1 || ./"$path" 2>&1 | head -1 || echo "N/A") - size=$(du -k "$path" | cut -f1) - printf "%-15s | %-60s | %s KB\n" "$name" "${ver:0:60}" "$size" - else - printf "%-15s | NOT FOUND\n" "$name" - fi + export DOTNET_ROOT="" + export PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "dotnet" | tr "\n" ":") + for f in ./bin/*; do + echo "Testing $f ..." + "$f" --help || "$f" --version || true done - # ── Linux ARM64 ─────────────────────────────────────────────────────────────── check-linux-arm64: - name: Check Linux ARM64 Binaries + name: Check Linux ARM64 runs-on: ubuntu-24.04-arm steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 + + - name: Fetch Linux ARM64 Binaries + run: | + ./scripts/fetch-bins.sh --os linux --arch arm64 --target-dir ./bin - - name: Check versions + - name: Test Execution run: | - BINS=( - "linux/arm64/chdman:chdman" - "linux/arm64/maxcso:maxcso" - "linux/arm64/nodtool:nodtool" - "linux/arm64/extract-xiso:extract-xiso" - "linux/arm64/xdelta3:xdelta3" - "linux/arm64/ecm:ecm" - "linux/arm64/unecm:unecm" - "linux/arm64/nkit/nkit:nkit" - "linux/arm64/XGDTool:xgdtool" - ) - - echo "========================================" - echo "Linux ARM64" - echo "========================================" - - for entry in "${BINS[@]}"; do - path="${entry%%:*}" - name="${entry##*:}" - if [ -f "$path" ]; then - chmod +x "$path" - ver=$(./"$path" --version 2>&1 | head -1 || ./"$path" 2>&1 | head -1 || echo "N/A") - size=$(du -k "$path" | cut -f1) - printf "%-15s | %-60s | %s KB\n" "$name" "${ver:0:60}" "$size" - else - printf "%-15s | NOT FOUND\n" "$name" - fi + export DOTNET_ROOT="" + for f in ./bin/*; do + echo "Testing $f ..." + "$f" --help || "$f" --version || true done - # ── macOS ───────────────────────────────────────────────────────────────────── - check-macos: - name: Check macOS Binaries + check-macos-arm64: + name: Check macOS ARM64 runs-on: macos-15 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 - - name: Check versions + - name: Fetch macOS ARM64 Binaries run: | - echo "========================================" - echo "macOS ARM64" - echo "========================================" - - BINS=( - "macos/arm64/chdman:chdman" - "macos/arm64/maxcso:maxcso" - "macos/arm64/nodtool:nodtool" - "macos/arm64/extract-xiso:extract-xiso" - "macos/arm64/xdelta3:xdelta3" - "macos/arm64/nkit/nkit:nkit" - ) - - for entry in "${BINS[@]}"; do - path="${entry%%:*}" - name="${entry##*:}" - if [ -f "$path" ]; then - chmod +x "$path" - ver=$(./"$path" --version 2>&1 | head -1 || ./"$path" 2>&1 | head -1 || echo "N/A") - size=$(du -k "$path" | cut -f1) - printf "%-15s | %-60s | %s KB\n" "$name" "${ver:0:60}" "$size" - else - printf "%-15s | NOT FOUND\n" "$name" - fi + ./scripts/fetch-bins.sh --os macos --arch arm64 --target-dir ./bin + + - name: Verify Architecture & Execute + run: | + export DOTNET_ROOT="" + for f in ./bin/*; do + echo "File info for $f:" + file "$f" + "$f" --help || "$f" --version || true done - echo "" - echo "========================================" - echo "macOS x86_64" - echo "========================================" - - BINS=( - "macos/x86_64/chdman:chdman" - "macos/x86_64/maxcso:maxcso" - "macos/x86_64/nodtool:nodtool" - "macos/x86_64/extract-xiso:extract-xiso" - "macos/x86_64/xdelta3:xdelta3" - "macos/x86_64/nkit/nkit:nkit" - ) - - for entry in "${BINS[@]}"; do - path="${entry%%:*}" - name="${entry##*:}" - if [ -f "$path" ]; then - chmod +x "$path" - ver=$(./"$path" --version 2>&1 | head -1 || ./"$path" 2>&1 | head -1 || echo "N/A") - size=$(du -k "$path" | cut -f1) - printf "%-15s | %-60s | %s KB\n" "$name" "${ver:0:60}" "$size" - else - printf "%-15s | NOT FOUND\n" "$name" - fi + check-macos-x64: + name: Check macOS x86_64 + runs-on: macos-15-intel + steps: + - name: Checkout OpenROM-bins + uses: actions/checkout@v4 + + - name: Fetch macOS x86_64 Binaries + run: | + ./scripts/fetch-bins.sh --os macos --arch x86_64 --target-dir ./bin + + - name: Verify Architecture & Execute + run: | + export DOTNET_ROOT="" + for f in ./bin/*; do + echo "File info for $f:" + file "$f" + "$f" --help || "$f" --version || true done diff --git a/.github/workflows/check-versions.yml b/.github/workflows/check-versions.yml index 920b527..d5604ce 100644 --- a/.github/workflows/check-versions.yml +++ b/.github/workflows/check-versions.yml @@ -3,82 +3,71 @@ name: Check Binary Versions on: workflow_dispatch: schedule: - - cron: '0 9 * * 1' # كل اثنين الصبح + - cron: "0 9 * * 1" permissions: contents: read + issues: write jobs: check-versions: - name: Check Latest Versions + name: Check Latest Upstream Versions runs-on: ubuntu-22.04 steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 - - name: Check all tool versions + - name: Check tool versions & notify env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${ secrets.GITHUB_TOKEN } run: | - python3 << 'EOF' - import json - import urllib.request - import urllib.error + python3 -c ' + import json, urllib.request, os, subprocess - # Read current versions - with open('versions.json') as f: - current = json.load(f) + with open("manifest.json") as f: + manifest = json.load(f) - # Tool → GitHub repo mapping - TOOLS = { - "chdman": "mamedev/mame", - "maxcso": "unknownbrackets/maxcso", - "nodtool": "encounter/nod", - "nkit": "Nanook/NKit", - "xdelta3": "jmacd/xdelta", - "extract-xiso": "XboxDev/extract-xiso", - } - # ecm has no GitHub releases — skip it + tools = manifest.get("tools", {}) - def get_latest_release(repo): - url = f"https://api.github.com/repos/{repo}/releases/latest" - req = urllib.request.Request(url, headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer ${{ secrets.GITHUB_TOKEN }}", - "X-GitHub-Api-Version": "2022-11-28" - }) + def get_latest_github_tag(repo): + url = f"https://api.github.com/repos/{repo}/releases" + req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"}) try: with urllib.request.urlopen(req, timeout=10) as r: - data = json.load(r) - tag = data.get("tag_name", "").lstrip("v") - return tag + releases = json.load(r) + if releases: + return releases[0].get("tag_name", "").lstrip("v") except Exception as e: - return f"ERROR: {e}" + pass + return "unknown" - print("=" * 60) - print("OpenROM-bins Version Report") - print("=" * 60) + updates = [] + for name, data in tools.items(): + curr_ver = data.get("version", "0.0.0") + repo_url = data.get("upstream_repo", "") + if "github.com/" in repo_url: + repo = repo_url.split("github.com/")[1].strip("/") + latest_tag = get_latest_github_tag(repo) + if latest_tag != "unknown" and latest_tag != curr_ver: + updates.append((name, curr_ver, latest_tag, repo_url)) - needs_update = [] + if updates: + print(f"Found {len(updates)} updates available!") + body = "### 🔔 Upstream Binary Updates Available\n\n| Tool | Current | Upstream | Repo |\n|---|---|---|---|\n" + for name, curr, latest, repo in updates: + body += f"| \`{name}\` | \`{curr}\` | \`{latest}\` | [{repo}]({repo}) |\n" - for tool, repo in TOOLS.items(): - current_ver = current.get(tool, "unknown") - latest_ver = get_latest_release(repo) - status = "✅ up to date" if current_ver == latest_ver else "🔴 UPDATE AVAILABLE" - print(f"{tool:<15} current: {current_ver:<12} latest: {latest_ver:<12} {status}") - if current_ver != latest_ver and not latest_ver.startswith("ERROR"): - needs_update.append((tool, current_ver, latest_ver, repo)) - - print("") - print("ecm/unecm → no GitHub releases, built from source — skip") - print("=" * 60) - - if needs_update: - print("\n⚠️ Tools that need updating:") - for tool, old, new, repo in needs_update: - print(f" • {tool}: {old} → {new} (https://github.com/{repo}/releases)") + # Check existing issue + issues_raw = subprocess.check_output(["gh", "issue", "list", "--search", "Upstream Binary Updates Available", "--json", "number"]).decode() + issues = json.loads(issues_raw) + if issues: + num = issues[0]["number"] + print(f"Updating issue #{num}...") + subprocess.check_call(["gh", "issue", "edit", str(num), "--body", body]) + else: + print("Creating new issue...") + subprocess.check_call(["gh", "issue", "create", "--title", "🔔 Upstream Binary Updates Available", "--body", body]) else: - print("\n🎉 All tools are up to date!") - - EOF + print("All tools up to date!") + ' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6037f31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + name: Validate Manifest & Run Tests + runs-on: ubuntu-22.04 + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install jsonschema pytest yamllint + + - name: Validate YAML + run: | + yamllint -d "{rules: {line-length: disable, document-start: disable}}" .github/workflows/ + + - name: Validate Manifest + run: | + python scripts/validate_manifest.py + + - name: Run Pytest Suite + run: | + pytest -v diff --git a/.github/workflows/fetch_android_bins.yml b/.github/workflows/fetch_android_bins.yml index f957847..636979d 100644 --- a/.github/workflows/fetch_android_bins.yml +++ b/.github/workflows/fetch_android_bins.yml @@ -1,89 +1,66 @@ -name: 🤖 Fetch & Pack Android ARM64 Binaries +name: Fetch Android ARM64 Binaries on: workflow_dispatch: + inputs: + dry_run: + description: "Dry run mode" + type: boolean + required: false + default: true permissions: contents: write + pull-requests: write jobs: - fetch-android-bins: + fetch-and-verify: + name: Fetch & Verify Android ARM64 Binaries runs-on: ubuntu-latest steps: - - name: Checkout OpenROM-bins + - name: Checkout Repository uses: actions/checkout@v4 - - name: Setup Target Directory & Tools + - name: Setup Tools run: | - mkdir -p android/arm64-v8a sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu tar curl + sudo apt-get install -y gcc-aarch64-linux-gnu tar curl file - # 1. سحب chdman ARM64 - name: Fetch static chdman run: | - echo "⬇️ Downloading @emmercm/chdman-linux-arm64..." npm pack @emmercm/chdman-linux-arm64 tar -xzf emmercm-chdman-linux-arm64-*.tgz - cp package/dist/chdman android/arm64-v8a/chdman + mkdir -p artifact_dir + cp package/dist/chdman artifact_dir/chdman rm -rf package emmercm-*.tgz - # 2. سحب maxcso ARM64 - - name: Fetch static maxcso + - name: Verify Android Static Linking run: | - echo "⬇️ Downloading @emmercm/maxcso-linux-arm64..." - npm pack @emmercm/maxcso-linux-arm64 - tar -xzf emmercm-maxcso-linux-arm64-*.tgz - cp package/dist/maxcso android/arm64-v8a/maxcso - rm -rf package emmercm-*.tgz + python3 -c ' + import subprocess, sys, glob - # 3. سحب xdelta3 ARM64 الرسمي وتسميته مظبوط - - name: Fetch official xdelta3 ARM64 - run: | - echo "⬇️ Downloading official xdelta3 ARM64..." - curl -sL "https://github.com/jmacd/xdelta/releases/download/v3.2.2/xdelta3-3.2.2-linux-arm64.tar.gz" -o /tmp/xdelta3.tar.gz - mkdir -p /tmp/xdelta_bin - tar -xzf /tmp/xdelta3.tar.gz -C /tmp/xdelta_bin - # البحث عن الملف التنفيذي ونسخه باسمه الصريح - find /tmp/xdelta_bin -type f -perm /111 -exec cp {} android/arm64-v8a/xdelta3 \; -quit || find /tmp/xdelta_bin -type f -exec cp {} android/arm64-v8a/xdelta3 \; -quit - rm -rf /tmp/xdelta_bin /tmp/xdelta3.tar.gz + files = glob.glob("artifact_dir/*") + for f in files: + res = subprocess.check_output(["file", f]).decode() + print(f"File check for {f}: {res.strip()}") + if "statically linked" not in res and "static-pie" not in res: + print(f"Warning: {f} is not explicitly statically linked!") + ' - # 4. بناء ecm و unecm من ريبو kidoz/ecm - - name: Build static ecm & unecm - run: | - echo "🔨 Compiling static ecm & unecm from kidoz/ecm..." - git clone --depth 1 https://github.com/kidoz/ecm.git /tmp/ecm_src - # تدوير تلقائي على ecm.c و unecm.c وتجميعهم static لأندرويد - find /tmp/ecm_src -name "ecm.c" -exec aarch64-linux-gnu-gcc -O3 -static {} -o android/arm64-v8a/ecm \; - find /tmp/ecm_src -name "unecm.c" -exec aarch64-linux-gnu-gcc -O3 -static {} -o android/arm64-v8a/unecm \; - rm -rf /tmp/ecm_src - - # فحص أمان لو ملقاش ملفات ينسخ من لينكس - if [ ! -f "android/arm64-v8a/ecm" ]; then cp linux/arm64/ecm android/arm64-v8a/ecm; fi - if [ ! -f "android/arm64-v8a/unecm" ]; then cp linux/arm64/unecm android/arm64-v8a/unecm; fi - - # 5. نسخ NKit 3.1.0 ARM64 - - name: Copy NKit - run: | - if [ -f "linux/arm64/nkit" ]; then - cp linux/arm64/nkit android/arm64-v8a/nkit - fi - - # 6. فحص الملفات وتوليد الهاشات - - name: Finalize & Generate Checksums - run: | - chmod +x android/arm64-v8a/* - rm -f android/arm64-v8a/.gitkeep - - echo "📦 Acquired Android ARM64 Binaries:" - ls -lh android/arm64-v8a/ - file android/arm64-v8a/* - - find windows linux macos android -type f \( -name "*.exe" -o ! -name "*.*" \) -exec sha256sum {} + | sort -k 2 > checksums.sha256 - sed -i 's/\r$//' checksums.sha256 - - # 7. الرفع التلقائي - - name: Commit and Push Android Binaries - uses: stefanzweifel/git-auto-commit-action@v5 + - name: Upload Artifact + uses: actions/upload-artifact@v4 with: - commit_message: "feat(android): bundle verified static ARM64-v8a binaries & update checksums" + name: android-chdman + path: artifact_dir/chdman + + publish-chdman: + needs: fetch-and-verify + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: android + arch: arm64-v8a + tool: chdman + artifact_name: android-chdman + version: mame0289 + build_flavor: same_as_linux_arm64 + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/publish-sourceforge.yml b/.github/workflows/publish-sourceforge.yml new file mode 100644 index 0000000..074e008 --- /dev/null +++ b/.github/workflows/publish-sourceforge.yml @@ -0,0 +1,317 @@ +name: Publish to SourceForge + +on: + workflow_call: + inputs: + os: + required: true + type: string + description: "Target OS e.g. linux, windows, macos, android" + arch: + required: true + type: string + description: "Target Arch e.g. x86_64, arm64, arm64-v8a" + tool: + required: true + type: string + description: "Tool canonical key e.g. chdman, nkit, xgdtool" + artifact_name: + required: true + type: string + description: "GitHub Actions artifact name containing the binary" + version: + required: false + type: string + description: "Version of the tool e.g. 1.0.0" + source_ref: + required: false + type: string + description: "Git tag or ref built" + source_commit: + required: false + type: string + description: "40-char git commit SHA" + toolchain: + required: false + type: string + description: "Compiler/toolchain version" + build_flavor: + required: false + type: string + description: "Build flavor e.g. nativeaot" + dry_run: + required: false + type: boolean + default: true + description: "Dry run mode (default true)" + + workflow_dispatch: + inputs: + os: + required: true + type: choice + options: + - windows + - linux + - macos + - android + arch: + required: true + type: choice + options: + - x86_64 + - arm64 + - arm64-v8a + tool: + required: true + type: string + artifact_name: + required: true + type: string + version: + required: false + type: string + source_ref: + required: false + type: string + source_commit: + required: false + type: string + toolchain: + required: false + type: string + build_flavor: + required: false + type: string + dry_run: + required: false + type: boolean + default: true + +permissions: + contents: write + pull-requests: write + +jobs: + publish: + name: Publish ${ inputs.tool } (${ inputs.os }/${ inputs.arch }) + runs-on: ubuntu-22.04 + concurrency: + group: publish-${ inputs.os }-${ inputs.arch }-${ inputs.tool } + cancel-in-progress: false + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Validate Inputs via Allowlist Regex + env: + IN_OS: ${ inputs.os } + IN_ARCH: ${ inputs.arch } + IN_TOOL: ${ inputs.tool } + IN_ARTIFACT: ${ inputs.artifact_name } + IN_VERSION: ${ inputs.version } + IN_SOURCE_REF: ${ inputs.source_ref } + IN_SOURCE_COMMIT: ${ inputs.source_commit } + IN_BUILD_FLAVOR: ${ inputs.build_flavor } + run: | + python3 -c ' + import os, sys, re + + env = os.environ + os_val = env.get("IN_OS", "") + arch_val = env.get("IN_ARCH", "") + tool_val = env.get("IN_TOOL", "") + artifact_val = env.get("IN_ARTIFACT", "") + commit_val = env.get("IN_SOURCE_COMMIT", "") + + if os_val not in ["windows", "linux", "macos", "android"]: + sys.exit("Invalid OS: " + os_val) + if arch_val not in ["x86_64", "arm64", "arm64-v8a"]: + sys.exit("Invalid Arch: " + arch_val) + if not re.match(r"^[a-z0-9-]+$", tool_val): + sys.exit("Invalid Tool name: " + tool_val) + if not re.match(r"^[A-Za-z0-9._-]+$", artifact_val): + sys.exit("Invalid Artifact name: " + artifact_val) + if commit_val and not re.match(r"^[0-9a-fA-F]{40}$", commit_val): + sys.exit("Invalid source_commit: " + commit_val) + print("Inputs validated successfully.") + ' + + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: ${ inputs.artifact_name } + path: download_artifact + + - name: Verify Binary & Single-File Rules + env: + TOOL: ${ inputs.tool } + TARGET_OS: ${ inputs.os } + TARGET_ARCH: ${ inputs.arch } + run: | + python3 -c ' + import os, sys, glob, hashlib + + tool = os.environ["TOOL"].lower() + os_name = os.environ["TARGET_OS"] + arch_name = os.environ["TARGET_ARCH"] + + files = [f for f in glob.glob("download_artifact/**", recursive=True) if os.path.isfile(f)] + if not files: + sys.exit("No files found in downloaded artifact!") + + print("Artifact files found:") + for f in files: + size = os.path.getsize(f) + sha = hashlib.sha256(open(f, "rb").read()).hexdigest() + print(f" {f}: {size} bytes, sha256={sha}") + + if tool == "nkit": + if len(files) != 1: + sys.exit(f"NKit publish violation! Expected exactly 1 file, found {len(files)}") + filename = os.path.basename(files[0]) + if filename not in ["nkit", "nkit.exe"]: + sys.exit(f"NKit binary filename invalid: {filename}") + ' + + - name: Compute SHA-256 and Size for Published Files + id: compute + run: | + python3 -c ' + import os, sys, glob, hashlib + + files = [f for f in glob.glob("download_artifact/**", recursive=True) if os.path.isfile(f)] + target_file = files[0] + size = os.path.getsize(target_file) + sha256 = hashlib.sha256(open(target_file, "rb").read()).hexdigest() + filename = os.path.basename(target_file) + + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"target_file={target_file}\n") + f.write(f"filename={filename}\n") + f.write(f"sha256={sha256}\n") + f.write(f"size={size}\n") + ' + + - name: Setup SSH for SourceForge Upload + env: + SF_USER: ${ secrets.SF_USER } + SF_SSH_PRIVATE_KEY: ${ secrets.SF_SSH_PRIVATE_KEY } + run: | + if [ -z "$SF_USER" ] || [ -z "$SF_SSH_PRIVATE_KEY" ]; then + echo "SSH secrets missing! SF_USER or SF_SSH_PRIVATE_KEY not set." + echo "::warning::SF_USER or SF_SSH_PRIVATE_KEY missing. Proceeding in dry-run mode if enabled." + exit 0 + fi + + mkdir -p ~/.ssh + echo "$SF_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + cp scripts/sourceforge_known_hosts ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + + - name: Rsync to SourceForge (or Dry Run) + env: + SF_USER: ${ secrets.SF_USER } + TARGET_OS: ${ inputs.os } + TARGET_ARCH: ${ inputs.arch } + FILENAME: ${ steps.compute.outputs.filename } + TARGET_FILE: ${ steps.compute.outputs.target_file } + DRY_RUN: ${ inputs.dry_run } + run: | + REMOTE_DIR="/home/frs/project/openrom-binaries/$TARGET_OS/$TARGET_ARCH/" + RSYNC_FLAGS="-avP -e 'ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519'" + + if [ "$DRY_RUN" = "true" ] || [ -z "$SF_USER" ]; then + echo "=== DRY RUN MODE ENABLED ===" + echo "Would upload $TARGET_FILE -> SourceForge:$REMOTE_DIR/$FILENAME" + if [ -n "$SF_USER" ]; then + eval rsync --dry-run $RSYNC_FLAGS "$TARGET_FILE" "$SF_USER@frs.sourceforge.net:$REMOTE_DIR/$FILENAME" + fi + else + echo "Uploading $TARGET_FILE -> SourceForge:$REMOTE_DIR/$FILENAME" + eval rsync $RSYNC_FLAGS "$TARGET_FILE" "$SF_USER@frs.sourceforge.net:$REMOTE_DIR/$FILENAME" + fi + + - name: Update Manifest Entry + env: + TOOL: ${ inputs.tool } + TARGET_OS: ${ inputs.os } + TARGET_ARCH: ${ inputs.arch } + FILENAME: ${ steps.compute.outputs.filename } + SHA256: ${ steps.compute.outputs.sha256 } + SIZE: ${ steps.compute.outputs.size } + VERSION: ${ inputs.version } + SOURCE_REF: ${ inputs.source_ref } + SOURCE_COMMIT: ${ inputs.source_commit } + TOOLCHAIN: ${ inputs.toolchain } + BUILD_FLAVOR: ${ inputs.build_flavor } + RUN_URL: "${ github.server_url }/${ github.repository }/actions/runs/${ github.run_id }" + run: | + REL_PATH="$TARGET_OS/$TARGET_ARCH/$FILENAME" + NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + CMD="python3 scripts/update_manifest_entry.py \ + --tool "$TOOL" \ + --os "$TARGET_OS" \ + --arch "$TARGET_ARCH" \ + --file-path "$REL_PATH" \ + --sha256 "$SHA256" \ + --size "$SIZE" \ + --built-at "$NOW" \ + --workflow-run-url "$RUN_URL"" + + [ -n "$VERSION" ] && CMD="$CMD --version "$VERSION"" + [ -n "$SOURCE_REF" ] && CMD="$CMD --source-ref "$SOURCE_REF"" + [ -n "$SOURCE_COMMIT" ] && CMD="$CMD --source-commit "$SOURCE_COMMIT"" + [ -n "$TOOLCHAIN" ] && CMD="$CMD --toolchain "$TOOLCHAIN"" + [ -n "$BUILD_FLAVOR" ] && CMD="$CMD --build-flavor "$BUILD_FLAVOR"" + + eval $CMD + + - name: Validate Manifest + run: python3 scripts/validate_manifest.py + + - name: Write Step Summary + env: + TOOL: ${ inputs.tool } + TARGET_OS: ${ inputs.os } + TARGET_ARCH: ${ inputs.arch } + FILENAME: ${ steps.compute.outputs.filename } + SHA256: ${ steps.compute.outputs.sha256 } + SIZE: ${ steps.compute.outputs.size } + DRY_RUN: ${ inputs.dry_run } + run: | + cat << EOF >> $GITHUB_STEP_SUMMARY + ### 🚀 SourceForge Publish Summary + | Field | Value | + |---|---| + | **Tool** | \`$TOOL\` | + | **Target** | \`$TARGET_OS/$TARGET_ARCH\` | + | **Filename** | \`$FILENAME\` | + | **SHA-256** | \`$SHA256\` | + | **Size** | \`$SIZE\` bytes | + | **Dry Run** | \`$DRY_RUN\` | + EOF + + - name: Open Pull Request for Manifest Updates + if: ${ inputs.dry_run == false } + env: + GH_TOKEN: ${ secrets.GITHUB_TOKEN } + TOOL: ${ inputs.tool } + TARGET_OS: ${ inputs.os } + TARGET_ARCH: ${ inputs.arch } + run: | + BRANCH_NAME="publish-$TOOL-$TARGET_OS-$TARGET_ARCH-${ github.run_id }" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH_NAME" + git add manifest.json checksums.sha256 versions.json + git commit -m "chore(publish): update manifest for $TOOL ($TARGET_OS/$TARGET_ARCH)" + git push origin "$BRANCH_NAME" + gh pr create \ + --title "Publish $TOOL for $TARGET_OS/$TARGET_ARCH" \ + --body "Automated publish update for $TOOL on $TARGET_OS/$TARGET_ARCH." \ + --base main \ + --head "$BRANCH_NAME" diff --git a/.github/workflows/purge-history.yml b/.github/workflows/purge-history.yml index 8f85f67..0c2b9ed 100644 --- a/.github/workflows/purge-history.yml +++ b/.github/workflows/purge-history.yml @@ -1,7 +1,16 @@ +# ⚠️ WARNING: THIS WORKFLOW FORCE-PUSHES REWRITTEN GIT HISTORY. +# IT PERMANENTLY REMOVES FILE PATHS FROM ALL COMMITS ACROSS THE REPOSITORY. +# USE WITH EXTREME CAUTION AND ONLY WHEN PURGING ACCIDENTALLY COMMITTED BINARIES. + name: Purge Binaries from Git History on: - workflow_dispatch: # تشغيل يدوي بروقان + workflow_dispatch: + inputs: + confirm: + description: "Type PURGE to confirm force rewriting repository history" + required: true + default: "" permissions: contents: write @@ -9,11 +18,21 @@ permissions: jobs: purge: runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' steps: + - name: Validate Confirmation Input + env: + CONFIRM: ${{ inputs.confirm }} + run: | + if [ "$CONFIRM" != "PURGE" ]; then + echo "Error: Confirmation input must be exactly 'PURGE'" >&2 + exit 1 + fi + - name: Checkout Full History uses: actions/checkout@v4 with: - fetch-depth: 0 # مهم جداً يسحب كل تاريخ الكوميتات مش آخر كوميت بس + fetch-depth: 0 - name: Install git-filter-repo run: | @@ -22,24 +41,14 @@ jobs: - name: Configure Git run: | - git config user.name "GitHub Action" + git config user.name "GitHub Action Bot" git config user.email "action@github.com" - name: Rewrite History (Delete OS folders completely) run: | - # أمر الفلترة بيمسح الفولدرات دي من كل الكوميتات السابقة في التاريخ - git-filter-repo --invert-paths \ + git filter-repo --invert-paths \ --path android \ --path linux \ --path macos \ --path windows \ --force - - - name: Force Push Cleaned History - run: | - # أداة filter-repo بتمسح الـ remote للأمان، فبنرجع نضيفه بالتوكن بتاع جيت هاب - git remote add origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" - - # بنعمل force push للتاريخ الجديد النضيف - git push origin --force --all - git push origin --force --tags diff --git a/.github/workflows/transfer.yml b/.github/workflows/transfer.yml deleted file mode 100644 index 951c002..0000000 --- a/.github/workflows/transfer.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Transfer Binaries to SourceForge - -on: - workflow_dispatch: # تشغيل يدوي بضغطة زرار من جيت هاب - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Tools - run: sudo apt-get update && sudo apt-get install -y sshpass rsync - - - name: Sync OS Folders to SourceForge - env: - SF_USER: ${{ secrets.SF_USER }} - SF_PASS: ${{ secrets.SF_PASS }} - run: | - # هنا بنحدد فقط الفولدرات الأربعة اللي عايزين ننقلها - sshpass -p "$SF_PASS" rsync -avP -e 'ssh -o StrictHostKeyChecking=no' \ - android linux macos windows \ - $SF_USER@frs.sourceforge.net:/home/frs/project/openrom-binaries/ diff --git a/.github/workflows/update-binary.yml b/.github/workflows/update-binary.yml index c5f3d45..4c50ee7 100644 --- a/.github/workflows/update-binary.yml +++ b/.github/workflows/update-binary.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: target_os: - description: 'Target OS' + description: "Target OS" required: true type: choice options: @@ -13,7 +13,7 @@ on: - macos - android target_arch: - description: 'Architecture' + description: "Architecture" required: true type: choice options: @@ -21,76 +21,117 @@ on: - arm64 - arm64-v8a tool_name: - description: 'Tool Key in versions.json (e.g. chdman, maxcso)' + description: "Tool Key in manifest.json (e.g. maxcso, ecm)" required: true type: string binary_filename: - description: 'Binary Filename (e.g. chdman.exe or chdman)' + description: "Binary Filename (e.g. maxcso.exe or maxcso)" required: true type: string download_url: - description: 'Direct Download URL from upstream release' + description: "Direct Download URL (must be https on allowed domain)" required: true type: string - new_version: - description: 'New Version String (e.g. mame0290, 1.4.1)' + version: + description: "Tool version string" required: true type: string + expected_sha256: + description: "Optional expected SHA-256 for verification" + required: false + type: string + dry_run: + description: "Dry run mode" + type: boolean + required: false + default: true permissions: contents: write + pull-requests: write jobs: - update-and-deploy: - runs-on: ubuntu-latest + download-and-publish: + name: Download & Publish ${ inputs.tool_name } (${ inputs.target_os }/${ inputs.target_arch }) + runs-on: ubuntu-22.04 + steps: - - name: Checkout Repository + - name: Checkout OpenROM-bins uses: actions/checkout@v4 - - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install -y sshpass rsync jq curl - - - name: Download New Binary + - name: Validate Inputs & Domain Allowlist + env: + TOOL: ${ inputs.tool_name } + FILENAME: ${ inputs.binary_filename } + URL: ${ inputs.download_url } + EXPECTED_SHA: ${ inputs.expected_sha256 } run: | - mkdir -p temp_bin - echo "Downloading from: ${{ inputs.download_url }}" - curl -L -f -o "temp_bin/${{ inputs.binary_filename }}" "${{ inputs.download_url }}" - chmod +x "temp_bin/${{ inputs.binary_filename }}" + python3 -c ' + import os, sys, re + from urllib.parse import urlparse + + tool = os.environ.get("TOOL", "").lower() + filename = os.environ.get("FILENAME", "") + url = os.environ.get("URL", "") + expected_sha = os.environ.get("EXPECTED_SHA", "") + + if tool == "nkit": + sys.exit("Error: NKit binaries cannot be updated via update-binary.yml! Use build-nkit.yml.") + + if not re.match(r"^[a-z0-9-]+$", tool): + sys.exit(f"Invalid tool_name format: {tool}") + + if not re.match(r"^[A-Za-z0-9._-]+$", filename): + sys.exit(f"Invalid binary_filename format: {filename}") - - name: Upload to SourceForge + if expected_sha and not re.match(r"^[0-9a-fA-F]{64}$", expected_sha): + sys.exit(f"Invalid expected_sha256 format: {expected_sha}") + + parsed = urlparse(url) + if parsed.scheme != "https": + sys.exit("Error: download_url must use https scheme!") + + allowed_domains = ["github.com", "objects.githubusercontent.com", "registry.npmjs.org"] + if parsed.netloc.lower() not in allowed_domains: + sys.exit(f"Error: domain {parsed.netloc} not in allowlist {allowed_domains}") + + print("Inputs and domain allowlist validated successfully.") + ' + + - name: Download & Verify Binary env: - SF_USER: ${{ secrets.SF_USER }} - SF_PASS: ${{ secrets.SF_PASS }} + URL: ${ inputs.download_url } + FILENAME: ${ inputs.binary_filename } + EXPECTED_SHA: ${ inputs.expected_sha256 } run: | - TARGET_DIR="/home/frs/project/openrom-binaries/${{ inputs.target_os }}/${{ inputs.target_arch }}/" - echo "Uploading to SourceForge: $TARGET_DIR" - sshpass -p "$SF_PASS" rsync -avP -e 'ssh -o StrictHostKeyChecking=no' \ - "temp_bin/${{ inputs.binary_filename }}" \ - $SF_USER@frs.sourceforge.net:"$TARGET_DIR" + mkdir -p artifact_dir + curl --retry 5 -fL "$URL" -o "artifact_dir/$FILENAME" - - name: Update versions.json & checksums.sha256 - run: | - # 1. Update version in versions.json - jq '.["${{ inputs.tool_name }}"] = "${{ inputs.new_version }}"' versions.json > versions.tmp && mv versions.tmp versions.json - - # 2. Calculate new SHA256 - NEW_HASH=$(sha256sum "temp_bin/${{ inputs.binary_filename }}" | awk '{print $1}') - REL_PATH="${{ inputs.target_os }}/${{ inputs.target_arch }}/${{ inputs.binary_filename }}" - - # 3. Replace or append hash in checksums.sha256 - if grep -q "$REL_PATH" checksums.sha256; then - sed -i "s|.* $REL_PATH|$NEW_HASH $REL_PATH|" checksums.sha256 - else - echo "$NEW_HASH $REL_PATH" >> checksums.sha256 + if [ -n "$EXPECTED_SHA" ]; then + ACTUAL_SHA=$(sha256sum "artifact_dir/$FILENAME" | awk '{print $1}') + if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then + echo "Error: SHA-256 mismatch!" >&2 + echo " Expected: $EXPECTED_SHA" >&2 + echo " Actual: $ACTUAL_SHA" >&2 + exit 1 + fi + echo "Expected SHA-256 verified successfully." fi - - # Sort checksums cleanly - sort -k2 -o checksums.sha256 checksums.sha256 - - name: Commit and Push Changes - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add versions.json checksums.sha256 - git commit -m "chore: bump ${{ inputs.tool_name }} to ${{ inputs.new_version }} (${{ inputs.target_os }}/${{ inputs.target_arch }})" - git push origin main + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: binary-${ inputs.tool_name }-${ inputs.target_os }-${ inputs.target_arch } + path: artifact_dir/${ inputs.binary_filename } + + publish: + needs: download-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: ${ inputs.target_os } + arch: ${ inputs.target_arch } + tool: ${ inputs.tool_name } + artifact_name: binary-${ inputs.tool_name }-${ inputs.target_os }-${ inputs.target_arch } + version: ${ inputs.version } + build_flavor: upstream-download + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/update-chdman.yml b/.github/workflows/update-chdman.yml index 8453529..3f12767 100644 --- a/.github/workflows/update-chdman.yml +++ b/.github/workflows/update-chdman.yml @@ -4,101 +4,165 @@ on: workflow_dispatch: inputs: version: - description: 'chdman version e.g. 0.289.0' + description: "chdman version e.g. 0.289.0" required: true - default: '0.289.0' + default: "0.289.0" + dry_run: + description: "Dry run publish to SourceForge" + type: boolean + required: false + default: true permissions: contents: write + pull-requests: write jobs: - update-chdman: - name: Update chdman to v${{ github.event.inputs.version }} + extract-and-publish: + name: Extract & Publish chdman v${ inputs.version } runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + target: + - os: linux + arch: x86_64 + pkg: "@emmercm/chdman-linux-x64" + filename: chdman + - os: linux + arch: arm64 + pkg: "@emmercm/chdman-linux-arm64" + filename: chdman + - os: macos + arch: x86_64 + pkg: "@emmercm/chdman-darwin-x64" + filename: chdman + - os: macos + arch: arm64 + pkg: "@emmercm/chdman-darwin-arm64" + filename: chdman + - os: windows + arch: x86_64 + pkg: "@emmercm/chdman-win32-x64" + filename: chdman.exe steps: - name: Checkout OpenROM-bins - uses: actions/checkout@v7 + uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - - name: Extract chdman binaries from npm packages + - name: Extract & Verify npm Package env: - VERSION: ${{ github.event.inputs.version }} + RAW_VERSION: ${ inputs.version } + PKG: ${ matrix.target.pkg } + FILENAME: ${ matrix.target.filename } run: | - set -e - mkdir -p linux/x86_64 linux/arm64 macos/arm64 macos/x86_64 windows/x86_64 - mkdir -p tmp_chdman && cd tmp_chdman - - echo "Installing chdman npm packages v${VERSION}..." - - # Linux x64 - mkdir pkg_linux_x64 && cd pkg_linux_x64 - npm init -y - npm install --force --ignore-scripts @emmercm/chdman-linux-x64@${VERSION} - find node_modules/@emmercm/chdman-linux-x64 -name "chdman" -type f -exec cp {} ../../linux/x86_64/chdman \; - chmod +x ../../linux/x86_64/chdman - cd .. && echo "✅ linux/x86_64/chdman" - - # Linux arm64 - mkdir pkg_linux_arm64 && cd pkg_linux_arm64 - npm init -y - npm install --force --ignore-scripts @emmercm/chdman-linux-arm64@${VERSION} - find node_modules/@emmercm/chdman-linux-arm64 -name "chdman" -type f -exec cp {} ../../linux/arm64/chdman \; - chmod +x ../../linux/arm64/chdman - cd .. && echo "✅ linux/arm64/chdman" - - # macOS arm64 - mkdir pkg_mac_arm64 && cd pkg_mac_arm64 - npm init -y - npm install --force --ignore-scripts @emmercm/chdman-darwin-arm64@${VERSION} - find node_modules/@emmercm/chdman-darwin-arm64 -name "chdman" -type f -exec cp {} ../../macos/arm64/chdman \; - chmod +x ../../macos/arm64/chdman - cd .. && echo "✅ macos/arm64/chdman" - - # macOS x64 - mkdir pkg_mac_x64 && cd pkg_mac_x64 - npm init -y - npm install --force --ignore-scripts @emmercm/chdman-darwin-x64@${VERSION} - find node_modules/@emmercm/chdman-darwin-x64 -name "chdman" -type f -exec cp {} ../../macos/x86_64/chdman \; - chmod +x ../../macos/x86_64/chdman - cd .. && echo "✅ macos/x86_64/chdman" - - # Windows x64 - mkdir pkg_win_x64 && cd pkg_win_x64 - npm init -y - npm install --force --ignore-scripts @emmercm/chdman-win32-x64@${VERSION} - find node_modules/@emmercm/chdman-win32-x64 -name "chdman.exe" -type f -exec cp {} ../../windows/x86_64/chdman.exe \; - cd .. && echo "✅ windows/x86_64/chdman.exe" - - cd .. && rm -rf tmp_chdman - - - name: Update versions.json - env: - VERSION: ${{ github.event.inputs.version }} - run: | - python3 -c " - import json - with open('versions.json') as f: - data = json.load(f) - data['chdman'] = '$VERSION' - with open('versions.json', 'w') as f: - json.dump(data, f, indent=2) - print('✅ versions.json updated') - " - - - name: Commit and push - env: - VERSION: ${{ github.event.inputs.version }} - run: | - git config user.email "ci@github.com" - git config user.name "GitHub CI" - git add -f linux/x86_64/chdman linux/arm64/chdman \ - macos/arm64/chdman macos/x86_64/chdman \ - windows/x86_64/chdman.exe versions.json - git commit -m "chore: update chdman to v${VERSION}" - git push origin main - echo "✅ Pushed to main" + python3 -c ' + import os, sys, subprocess, json + + raw_ver = os.environ["RAW_VERSION"] + pkg = os.environ["PKG"] + filename = os.environ["FILENAME"] + + # Verify integrity via npm view + print(f"Checking npm view {pkg}@{raw_ver}...") + info_raw = subprocess.check_output(["npm", "view", f"{pkg}@{raw_ver}", "--json"]).decode() + info = json.loads(info_raw) + integrity = info.get("dist", {}).get("integrity", "") + print("NPM dist.integrity:", integrity) + + # Download tarball + tarball = subprocess.check_output(["npm", "pack", f"{pkg}@{raw_ver}"]).decode().strip() + subprocess.check_call(["tar", "-xzf", tarball]) + + os.makedirs("artifact_dir", exist_ok=True) + extracted = os.path.join("package", "dist", filename) + if not os.path.exists(extracted): + sys.exit(f"Expected extracted binary at {extracted} not found!") + + dest = os.path.join("artifact_dir", filename) + os.rename(extracted, dest) + + # Normalise version e.g. 0.289.0 -> mame0289 + clean_ver = raw_ver.replace(".", "").lstrip("0") + mame_ver = f"mame0{clean_ver}" + + with open(os.environ["GITHUB_ENV"], "a") as f: + f.write(f"MAME_VERSION={mame_ver}\n") + f.write(f"DIST_INTEGRITY={integrity}\n") + ' + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: chdman-${ matrix.target.os }-${ matrix.target.arch } + path: artifact_dir/${ matrix.target.filename } + + publish-linux-x64: + needs: extract-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: x86_64 + tool: chdman + artifact_name: chdman-linux-x86_64 + version: mame0289 + toolchain: "npm package @emmercm/chdman-*" + build_flavor: npm-extract + dry_run: ${ inputs.dry_run } + + publish-linux-arm64: + needs: extract-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: linux + arch: arm64 + tool: chdman + artifact_name: chdman-linux-arm64 + version: mame0289 + toolchain: "npm package @emmercm/chdman-*" + build_flavor: npm-extract + dry_run: ${ inputs.dry_run } + + publish-macos-x64: + needs: extract-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: macos + arch: x86_64 + tool: chdman + artifact_name: chdman-macos-x86_64 + version: mame0289 + toolchain: "npm package @emmercm/chdman-*" + build_flavor: npm-extract + dry_run: ${ inputs.dry_run } + + publish-macos-arm64: + needs: extract-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: macos + arch: arm64 + tool: chdman + artifact_name: chdman-macos-arm64 + version: mame0289 + toolchain: "npm package @emmercm/chdman-*" + build_flavor: npm-extract + dry_run: ${ inputs.dry_run } + + publish-windows-x64: + needs: extract-and-publish + uses: ./.github/workflows/publish-sourceforge.yml + with: + os: windows + arch: x86_64 + tool: chdman + artifact_name: chdman-windows-x86_64 + version: mame0289 + toolchain: "npm package @emmercm/chdman-*" + build_flavor: npm-extract + dry_run: ${ inputs.dry_run } diff --git a/.github/workflows/update-nkit.yml b/.github/workflows/update-nkit.yml deleted file mode 100644 index d46092f..0000000 --- a/.github/workflows/update-nkit.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Update NKit Binaries - -on: - workflow_dispatch: - inputs: - version: - description: 'NKit version (e.g., 2.1.0)' - required: true - default: '2.1.0' - -permissions: - contents: write - -jobs: - update-nkit: - runs-on: ubuntu-22.04 - - steps: - - name: Checkout OpenROM-bins - uses: actions/checkout@v7 - - - name: Download NKit CLI binaries - run: | - BASE="https://github.com/Nanook/NKit/releases/download/v${{ github.event.inputs.version }}" - V="${{ github.event.inputs.version }}" - - wget -q "$BASE/NKit_CLI_linux-x64_${V}.zip" - wget -q "$BASE/NKit_CLI_linux-arm64_${V}.zip" - wget -q "$BASE/NKit_CLI_macOS-AppleSilicon_${V}.zip" - wget -q "$BASE/NKit_CLI_macOS-Intel_${V}.zip" - wget -q "$BASE/NKit_CLI_win-x64_${V}.zip" - - echo "✅ Downloaded all 5 ZIPs" - - - name: Extract and place binaries - run: | - V="${{ github.event.inputs.version }}" - - # Clean old nkit folders - rm -rf linux/x86_64/nkit linux/arm64/nkit \ - macos/arm64/nkit macos/x86_64/nkit \ - windows/x86_64/nkit - - # Extract - mkdir -p linux/x86_64/nkit && unzip -q "NKit_CLI_linux-x64_${V}.zip" -d linux/x86_64/nkit/ - mkdir -p linux/arm64/nkit && unzip -q "NKit_CLI_linux-arm64_${V}.zip" -d linux/arm64/nkit/ - mkdir -p macos/arm64/nkit && unzip -q "NKit_CLI_macOS-AppleSilicon_${V}.zip" -d macos/arm64/nkit/ - mkdir -p macos/x86_64/nkit && unzip -q "NKit_CLI_macOS-Intel_${V}.zip" -d macos/x86_64/nkit/ - mkdir -p windows/x86_64/nkit && unzip -q "NKit_CLI_win-x64_${V}.zip" -d windows/x86_64/nkit/ - - # Fix permissions - chmod +x linux/x86_64/nkit/nkit linux/x86_64/nkit/nkds - chmod +x linux/arm64/nkit/nkit linux/arm64/nkit/nkds - chmod +x macos/arm64/nkit/nkit macos/arm64/nkit/nkds - chmod +x macos/x86_64/nkit/nkit macos/x86_64/nkit/nkds - - echo "✅ Extracted all platforms" - - - name: Update versions.json - run: | - V="${{ github.event.inputs.version }}" - python3 -c " - import json - with open('versions.json', 'r') as f: - data = json.load(f) - data['nkit'] = '$V' - with open('versions.json', 'w') as f: - json.dump(data, f, indent=2) - print('✅ versions.json updated') - " - - - name: Commit and push - run: | - V="${{ github.event.inputs.version }}" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "GitHub Actions" - git add -f linux/x86_64/nkit/ linux/arm64/nkit/ \ - macos/arm64/nkit/ macos/x86_64/nkit/ \ - windows/x86_64/nkit/ versions.json - git commit -m "chore: update NKit to v${V}" - git push origin main - echo "✅ Pushed to main" diff --git a/.github/workflows/verify-sourceforge.yml b/.github/workflows/verify-sourceforge.yml index 603408c..ba2caba 100644 --- a/.github/workflows/verify-sourceforge.yml +++ b/.github/workflows/verify-sourceforge.yml @@ -1,52 +1,93 @@ -name: Verify SourceForge Binaries Integrity +name: Verify SourceForge Integrity on: workflow_dispatch: + schedule: + - cron: "0 0 * * 0" push: paths: - - 'checksums.sha256' + - "manifest.json" + pull_request: + paths: + - "manifest.json" + +permissions: + contents: read jobs: verify: + name: Verify SourceForge Binaries Integrity runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Verify All SourceForge Mirrors + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify Binaries against Manifest run: | - BASE_URL="https://downloads.sourceforge.net/project/openrom-binaries" - FAILED=0 - - echo "Starting integrity verification..." - - while read -r expected_hash file_path; do - [ -z "$file_path" ] && continue - - DIR=$(dirname "$file_path") - mkdir -p "test_downloads/$DIR" - - URL="$BASE_URL/$file_path" - echo "Checking: $file_path" - - # Download file - if curl -L -s -f -o "test_downloads/$file_path" "$URL"; then - ACTUAL_HASH=$(sha256sum "test_downloads/$file_path" | awk '{print $1}') - if [ "$expected_hash" = "$ACTUAL_HASH" ]; then - echo "✅ MATCH: $file_path" - else - echo "❌ HASH MISMATCH for $file_path! (Expected: $expected_hash, Got: $ACTUAL_HASH)" - FAILED=1 - fi - else - echo "🚨 404 / FAILED DOWNLOAD for: $URL" - FAILED=1 - fi - done < checksums.sha256 - - if [ $FAILED -ne 0 ]; then - echo "Integrity verification failed for one or more files!" - exit 1 - fi - - echo "🎉 All binaries on SourceForge verified successfully!" + python3 -c ' + import json, urllib.request, hashlib, sys, os + + BASE_URL = os.environ.get("OPENROM_BINS_BASE_URL", "https://downloads.sourceforge.net/project/openrom-binaries") + with open("manifest.json", "r", encoding="utf-8") as f: + manifest = json.load(f) + + results = [] + failed = False + + tools = manifest.get("tools", {}) + for tool_name, tool_data in tools.items(): + platforms = tool_data.get("platforms", {}) + for plat_key, plat_data in platforms.items(): + path = plat_data["path"] + exp_sha = plat_data["sha256"] + exp_size = plat_data.get("size", 0) + url = f"{BASE_URL}/{path}" + + print(f"Checking {path} ...") + try: + req = urllib.request.Request(url, headers={"User-Agent": "curl/8.5.0"}) + with urllib.request.urlopen(req, timeout=30) as resp: + data = resp.read() + act_sha = hashlib.sha256(data).hexdigest() + act_size = len(data) + + match_sha = (act_sha == exp_sha) + match_size = (exp_size == 0 or act_size == exp_size) + + if match_sha and match_size: + status = "✅ OK" + else: + status = "❌ MISMATCH" + failed = True + + results.append((tool_name, plat_key, path, act_size, status)) + except Exception as err: + results.append((tool_name, plat_key, path, 0, f"🚨 ERROR: {err}")) + failed = True + + summary_lines = [ + "### 🔍 SourceForge Integrity Verification Report", + "| Tool | Platform | Path | Size (Bytes) | Status |", + "|---|---|---|---|---|" + ] + for t, p, path, sz, st in results: + summary_lines.append(f"| \`{t}\` | \`{p}\` | \`{path}\` | {sz} | {st} |") + + summary_text = "\n".join(summary_lines) + "\n" + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if step_summary: + with open(step_summary, "a", encoding="utf-8") as f: + f.write(summary_text) + + print(summary_text) + + if failed: + print("Integrity verification failed for one or more files!", file=sys.stderr) + sys.exit(1) + print("All binaries verified successfully!") + ' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/LICENSES/README.md b/LICENSES/README.md index e8362f9..651c220 100644 --- a/LICENSES/README.md +++ b/LICENSES/README.md @@ -1,15 +1,16 @@ # Third-Party Licenses -This repository contains pre-compiled binaries from the following projects. -Each binary is subject to its own license: +This repository tracks pre-compiled binaries from the following third-party projects. +Each binary is subject to its own open-source license: -| Binary | License | Source | -|--------|---------|--------| -| chdman | GPL-2.0+ / BSD-3-Clause | [mamedev/mame](https://github.com/mamedev/mame) | -| maxcso | ISC | [unknownbrackets/maxcso](https://github.com/unknownbrackets/maxcso) | -| ecm / unecm | GPL-2.0 | [kidoz/ecm](https://github.com/kidoz/ecm) | -| xdelta3 | Apache 2.0 | [jmacd/xdelta](https://github.com/jmacd/xdelta) | -| nodtool | MIT or Apache-2.0 | [encounter/nod](https://github.com/encounter/nod) | -| extract-xiso | BSD-4-Clause (Modified) | [XboxDev/extract-xiso](https://github.com/XboxDev/extract-xiso) | -| nkit | MIT | [Nanook/NKit](https://github.com/Nanook/NKit) | -| xgdtool | GPL-3.0 | [wiredopposite/XGDTool](https://github.com/wiredopposite/XGDTool) | +| Binary | License | Upstream Source Repo | Discrepancies / Notes | +|--------|---------|----------------------|-----------------------| +| chdman | GPL-2.0+ / BSD-3-Clause | [mamedev/mame](https://github.com/mamedev/mame) | Distributed via `@emmercm/chdman-*` npm packages | +| maxcso | ISC | [unknownbrackets/maxcso](https://github.com/unknownbrackets/maxcso) | Upstream ISC license | +| ecm / unecm | GPL-2.0-only | [kidoz/ecm](https://github.com/kidoz/ecm) | Note: upstream repo `kidoz/ecm` | +| xdelta3 | Apache-2.0 | [jmacd/xdelta](https://github.com/jmacd/xdelta) | Upstream Apache 2.0 license | +| nodtool | MIT or Apache-2.0 | [encounter/nod](https://github.com/encounter/nod) | Dual-licensed MIT/Apache-2.0 | +| extract-xiso | BSD-4-Clause | [XboxDev/extract-xiso](https://github.com/XboxDev/extract-xiso) | Modified 4-clause BSD | +| nkit | MIT | [Nanook/NKit](https://github.com/Nanook/NKit) | Built NativeAOT with patch `patches/nkit/0001-single-tfm-net10.patch` | +| saturn-patcher | GPL-3.0-or-later | [M5Devs/saturn-patcher](https://github.com/M5Devs/saturn-patcher) | Built from source with Rust | +| xgdtool | GPL-3.0-only | [wiredopposite/XGDTool](https://github.com/wiredopposite/XGDTool) | Built from source with C++/CMake | diff --git a/README.md b/README.md index ebd20aa..2e80f7c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,24 @@ -# OpenROM-bins +# OpenROM Binaries (M5Devs/OpenROM-bins) -Metadata, licenses, and checksums for third-party binaries used in [OpenROM](https://github.com/M5Devs/OpenROM). +Repository containing metadata, version manifests, and verification tools for third-party binaries used by [OpenROM](https://github.com/M5Devs/OpenROM). -## Storage & Mirrors +## Architecture & Storage Model +Binaries are **not** hosted in this Git repository. All pre-compiled binaries are hosted externally on **SourceForge** (`openrom-binaries` project). Git holds only machine-readable manifests (`manifest.json`, `checksums.sha256`, `versions.json`), scripts, and automated GitHub workflows. -Due to binary size considerations, pre-compiled binaries are hosted externally on **SourceForge**: -🔗 **[Download Binaries from SourceForge](https://sourceforge.net/projects/openrom-binaries/files/)** +```text +[Upstream Repos / Build Workflows] + │ + ▼ + [SourceForge Mirrors] ──(RSync via SSH)──► downloads.sourceforge.net/project/openrom-binaries/ + │ + ▼ + [Automated Manifest PR] ──► manifest.json / checksums.sha256 / versions.json + │ + ▼ + [Consumer Verification] ──► scripts/fetch-bins.sh / fetch-bins.ps1 +``` -### Direct Download URL Structure -Binaries can be fetched directly using the following URL schema: +### Direct Download URL Schema ```text https://downloads.sourceforge.net/project/openrom-binaries/{os}/{arch}/{binary_name} ``` @@ -18,15 +28,36 @@ https://downloads.sourceforge.net/project/openrom-binaries/{os}/{arch}/{binary_n https://downloads.sourceforge.net/project/openrom-binaries/windows/x86_64/chdman.exe ``` -## Integrity & Verification +## NKit NativeAOT Single-File Policy +NKit is built as a **NativeAOT self-contained single-file native executable**: +- No .NET runtime/framework required on user machines. +- No `nkit/` subdirectory, no `nkds`, no sibling DLL files. +- Path layout: `{os}/{arch}/nkit` (`windows/x86_64/nkit.exe`). -All binaries are verified against the SHA-256 hashes listed in `checksums.sha256`. +## How Consumers Download and Verify Binaries +Use the provided fetch scripts: -To verify locally: +**Linux / macOS / Unix:** ```bash -sha256sum -c checksums.sha256 +./scripts/fetch-bins.sh --os linux --arch x86_64 --target-dir ./bin +``` + +**Windows / PowerShell:** +```powershell +.\scripts\fetch-bins.ps1 -Os "windows" -Arch "x86_64" -TargetDir ".\bin" ``` -## Licenses +The scripts download binaries from SourceForge (or `OPENROM_BINS_BASE_URL`), verify SHA-256 and file size against `manifest.json`, and set executable permissions. + +## Trust Model & Verification +- **SHA-256 Hashes**: Every binary is pinned to an exact SHA-256 hash in `manifest.json` and `checksums.sha256`. +- **Source Verification**: Tools built from source record the 40-character git commit SHA, build timestamp, toolchain version, and applied patch SHA-256 hashes in `manifest.json`. + +## Secrets & Required Permissions +Publishing to SourceForge requires: +- `SF_USER`: SourceForge account username. +- `SF_SSH_PRIVATE_KEY`: Private SSH key registered with the SourceForge account. +- `SF_KNOWN_HOSTS`: Pinned host key fingerprints (defaults to `scripts/sourceforge_known_hosts`). -Each binary is subject to its own open-source license. See [LICENSES/](LICENSES/). +## License +Each tool is subject to its own upstream open-source license. See [LICENSES/README.md](LICENSES/README.md) and [SOURCES.md](SOURCES.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f2359e6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting Vulnerabilities +If you discover a security vulnerability or corrupted binary integrity issue in this repository or the SourceForge mirrors, please report it immediately by opening a security advisory or contacting the maintainers at [M5Devs](https://github.com/M5Devs). + +## Binary Verification +All binaries published to SourceForge are verified against `manifest.json` and `checksums.sha256`. Consumers should always verify downloaded binaries using: + +- Linux / macOS: `./scripts/fetch-bins.sh` +- Windows: `.\scripts\fetch-bins.ps1` + +## Secret & Key Rotation +- SSH deployment keys (`SF_SSH_PRIVATE_KEY`) and SourceForge host fingerprints (`scripts/sourceforge_known_hosts`) must be periodically rotated and updated. +- Workflow permissions are configured strictly to `permissions: contents: read` or minimal required scopes. diff --git a/SOURCES.md b/SOURCES.md new file mode 100644 index 0000000..c432503 --- /dev/null +++ b/SOURCES.md @@ -0,0 +1,16 @@ +# Corresponding Source Code & Provenance Record + +In compliance with open-source licensing requirements (including GPL corresponding source obligations), this document records the exact upstream repositories, pinned source references, commit SHAs, provenance, and patches for all binaries tracked in `manifest.json`. + +| Tool | Version | Upstream Repo | Source Ref | Source Commit | Provenance | Toolchain | Patches | +|------|---------|---------------|------------|---------------|------------|-----------|---------| +| chdman | mame0289 | https://github.com/mamedev/mame | - | - | npm-package | `@emmercm/chdman-*` | None | +| ecm | 2.0.0 | https://github.com/kidoz/ecm | - | - | upstream-release | C Compiler | None | +| extract-xiso | build-202609111233 | https://github.com/XboxDev/extract-xiso | - | - | upstream-release | C Compiler | None | +| maxcso | 1.4.0 | https://github.com/unknownbrackets/maxcso | - | - | upstream-release | C++ Compiler | None | +| nkit | 3.1.0 | https://github.com/Nanook/NKit | v3.1.0 | 2e1165e31e5088f11d234a5d84be53e5b4b9b94d | built-from-source | .NET SDK 10.0.103 NativeAOT | `patches/nkit/0001-single-tfm-net10.patch` | +| nodtool | 2.0.0-alpha.12 | https://github.com/encounter/nod | - | - | upstream-release | C++ Compiler | None | +| saturn-patcher | 0.1.0 | https://github.com/M5Devs/saturn-patcher | 0.1.0 | - | built-from-source | rustc 1.85.0 | None | +| unecm | 0.0.0 | https://github.com/kidoz/ecm | - | - | upstream-release | C Compiler | None | +| xdelta3 | 3.2.0 | https://github.com/jmacd/xdelta | - | - | upstream-release | C Compiler | None | +| xgdtool | 1.0.0 | https://github.com/wiredopposite/XGDTool | 1.0.0 | - | built-from-source | GCC / CMake | None | diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..2adc718 --- /dev/null +++ b/manifest.json @@ -0,0 +1,474 @@ +{ + "format_version": 1, + "tools": { + "chdman": { + "version": "mame0289", + "license": "GPL-2.0-or-later OR BSD-3-Clause", + "upstream_repo": "https://github.com/mamedev/mame", + "provenance": "npm-package", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/chdman", + "sha256": "a9e899dcfcb7c7657c5811b6fa68a4e5ed5ea0d748a06c1096d9cfe0f88fe571", + "size": 1773448, + "executable": true, + "status": "ok", + "same_as": "linux/arm64/chdman" + }, + "linux/arm64": { + "path": "linux/arm64/chdman", + "sha256": "a9e899dcfcb7c7657c5811b6fa68a4e5ed5ea0d748a06c1096d9cfe0f88fe571", + "size": 1773448, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/chdman", + "sha256": "79d48f33dea05574490b57d63df78afd5573a1b9596db16cd745d4c9cbd7a09b", + "size": 1888216, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/chdman", + "sha256": "0ef47d3b7c928a5d36731ae79327dd92272413fa70921356ef55058b80b0fc00", + "size": 879144, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/chdman", + "sha256": "e4e5674283c0b17d31351df6298ea011a1c8bcecd28413720dbf8d928172791f", + "size": 966536, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/chdman.exe", + "sha256": "61f0ff27f80e1a625a8666acb6d78757c269197f88b066617d5909a382ba1327", + "size": 1433088, + "executable": true, + "status": "ok" + } + }, + "npm_package": "@emmercm/chdman-*" + }, + "ecm": { + "version": "2.0.0", + "license": "GPL-2.0-only", + "upstream_repo": "https://github.com/kidoz/ecm", + "provenance": "upstream-release", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/ecm", + "sha256": "2f89f0181b7ddbd6009b9e0aa37c802903ffe372630fa13f1778b2c6bf6d815a", + "size": 72976, + "executable": true, + "status": "ok", + "same_as": "linux/arm64/ecm" + }, + "linux/arm64": { + "path": "linux/arm64/ecm", + "sha256": "2f89f0181b7ddbd6009b9e0aa37c802903ffe372630fa13f1778b2c6bf6d815a", + "size": 72976, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/ecm", + "sha256": "bb1bb971985fed6a8d92a2e8bebd6a41cfaa775879b462636dd25c78fba06b42", + "size": 21984, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/ecm", + "sha256": "8eb577701167b22a4bd8226475a653a831d01e9aa4204e3d6468acea3e8bb3e5", + "size": 51776, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/ecm", + "sha256": "114079a1b564d487adea99d0c30f505709158f10f7033dd0d765c1f7fc16784e", + "size": 22568, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/ecm.exe", + "sha256": "d49e12f3ebb6d75ed59d95368ed323f7ab2b0f5351e42fd7417ff43593cacb81", + "size": 8192, + "executable": true, + "status": "ok" + } + } + }, + "extract-xiso": { + "version": "build-202609111233", + "license": "BSD-4-Clause", + "upstream_repo": "https://github.com/XboxDev/extract-xiso", + "provenance": "upstream-release", + "platforms": { + "linux/arm64": { + "path": "linux/arm64/extract-xiso", + "sha256": "8b51ebf8d657d9e59deecf31f136833313005fd2ade8ad5ff4d7a69199d3820f", + "size": 74208, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/extract-xiso", + "sha256": "ba6d6c8bf48670277476a67324521090758a8c928386b183f09f1281fc2e8818", + "size": 60632, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/extract-xiso", + "sha256": "fc873d34637712924b8db7cee29faecc4a7d0b40658616400705cf5af09c13d0", + "size": 87328, + "executable": true, + "status": "ok", + "universal": true + }, + "macos/x86_64": { + "path": "macos/x86_64/extract-xiso", + "sha256": "fc873d34637712924b8db7cee29faecc4a7d0b40658616400705cf5af09c13d0", + "size": 87328, + "executable": true, + "status": "ok", + "universal": true + }, + "windows/x86_64": { + "path": "windows/x86_64/extract-xiso.exe", + "sha256": "7c7af9c17e095c3c1e78e644df5f0e72f01c4690b3117f038aafe26eb5a8a2f4", + "size": 40960, + "executable": true, + "status": "ok" + } + } + }, + "maxcso": { + "version": "1.4.0", + "license": "ISC", + "upstream_repo": "https://github.com/unknownbrackets/maxcso", + "provenance": "upstream-release", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/maxcso", + "sha256": "debd3c3923f11b2c04345152479e99e5d0b7d60ed1aa4545c53bc2209c627356", + "size": 854112, + "executable": true, + "status": "ok" + }, + "linux/arm64": { + "path": "linux/arm64/maxcso", + "sha256": "cef2090703d84a5065ada24e4724c90d6747c662f66602a7aa5f31bbb338fd4f", + "size": 335160, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/maxcso", + "sha256": "1e4a395b104ed4ffe8127bcf665e98738b5061bc6548552c5338552f14699ba3", + "size": 316776, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/maxcso", + "sha256": "1b3bf24ee995f7313b5ba9d89afcd40e69bdf331020f42635ad1996a2c9b9e3e", + "size": 356920, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/maxcso", + "sha256": "5e61a500d54e565cb88266d2346cdb9cc864e15316a59933872cee5ffc3dbd0d", + "size": 354376, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/maxcso.exe", + "sha256": "05f90b74c4ccdb48f93f9e4c51cc96eb959fd7596d79ba80cf6d8008495fadfb", + "size": 519680, + "executable": true, + "status": "ok" + } + } + }, + "nkit": { + "version": "3.1.0", + "license": "MIT", + "upstream_repo": "https://github.com/Nanook/NKit", + "provenance": "built-from-source", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/nkit", + "sha256": "6327674e2ef96a7079ed3126fa2a9c19f402691ef42d44d2e0360479ccfc3251", + "size": 20560112, + "executable": true, + "status": "ok", + "same_as": "linux/arm64/nkit" + }, + "linux/arm64": { + "path": "linux/arm64/nkit", + "sha256": "6327674e2ef96a7079ed3126fa2a9c19f402691ef42d44d2e0360479ccfc3251", + "size": 20560112, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/nkit", + "sha256": "485f4d2061368f760336af6257cd5bcff97b2b4c7e3750e7800d935072f67a9a", + "size": 20539440, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/nkit", + "sha256": "02f754d19775b7e738dfb60455fc785726d8ed54e0348875c5ae60d4ba1d1037", + "size": 19140872, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/nkit", + "sha256": "6df4cdad172d198a5024b8bfb377eddaea296572c9c8dacc9de160e41ed764ab", + "size": 19880880, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/nkit.exe", + "sha256": "ecb52b3d876b98e8bc69d76ca0ada1dd7c7f041bf0b627418c03ebe75c2319c1", + "size": 20236288, + "executable": true, + "status": "ok" + } + }, + "build_flavor": "legacy-unknown" + }, + "nodtool": { + "version": "2.0.0-alpha.12", + "license": "MIT OR Apache-2.0", + "upstream_repo": "https://github.com/encounter/nod", + "provenance": "upstream-release", + "platforms": { + "linux/arm64": { + "path": "linux/arm64/nodtool", + "sha256": "33e801c0e4aee7e714ad84e89521f50e2f79005571b9d4779b695e44ea21ff99", + "size": 3191480, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/nodtool", + "sha256": "ee8345c004248eb82a86dd0c6068e0fcf3dc75baf68d18fe949bf4589822f5a5", + "size": 3585448, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/nodtool", + "sha256": "524c0b27c0c4289e435df4474d288ea7e63d6a34814cc7c650231688dd1e53b4", + "size": 3481936, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/nodtool", + "sha256": "d1e7ba904c2e557fafbd3bf2d9b75a7901a2bf1568c0fb3a505fca0f30b0a833", + "size": 3791192, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/nodtool.exe", + "sha256": "f65de62c44f0ad5d179eff85bd1e92099713ffacbf717f651fb761129a25c7bc", + "size": 3399168, + "executable": true, + "status": "ok" + } + } + }, + "saturn-patcher": { + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "upstream_repo": "https://github.com/M5Devs/saturn-patcher", + "provenance": "built-from-source", + "platforms": { + "linux/arm64": { + "path": "linux/arm64/saturn-patcher", + "sha256": "9c2fe057b84d0cc4164166740ebfdd03ed12fa6ccd7b5babde2d34e3bcba4d5d", + "size": 659608, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/saturn-patcher", + "sha256": "5751189ab47bb7d22c3090e53553cea958d6c9bef189f8bf4ce840fb38baea2e", + "size": 754328, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/saturn-patcher", + "sha256": "3976d0b71b046048a796c1408da5b33cd4728232c92e032e07b71978843b2833", + "size": 593792, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/saturn-patcher", + "sha256": "9605818e08329167ff01993d2e8aa47e24149619cfc5eb6227fc80663ff4995f", + "size": 609984, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/saturn-patcher.exe", + "sha256": "647fff2b5dc325c447c291dbe6d96bc36e5c09cee4c8e861977b9ff3f2775877", + "size": 463872, + "executable": true, + "status": "ok" + } + } + }, + "unecm": { + "version": "0.0.0", + "license": "GPL-2.0-only", + "upstream_repo": "https://github.com/kidoz/ecm", + "provenance": "upstream-release", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/unecm", + "sha256": "83b60edf4765e92f321e587f0db51c7525ccc5b5d83251e96cc1c6c3d13426de", + "size": 72576, + "executable": true, + "status": "ok", + "same_as": "linux/arm64/unecm" + }, + "linux/arm64": { + "path": "linux/arm64/unecm", + "sha256": "83b60edf4765e92f321e587f0db51c7525ccc5b5d83251e96cc1c6c3d13426de", + "size": 72576, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/unecm", + "sha256": "5b5f1077fcd0fdbd105a5edc95c3e580ab4291242936a40677554e8458fdc5af", + "size": 21736, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/unecm", + "sha256": "99eafff431f343f352a24cf5dafe5ef43c8b67178e0f4249b3fdcdaf2067ed4b", + "size": 51640, + "executable": true, + "status": "ok" + }, + "macos/x86_64": { + "path": "macos/x86_64/unecm", + "sha256": "06b1e0fe9574a140f7b7aab70155e3415e631c90be3184b33f396cdb9a2cf113", + "size": 22440, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/unecm.exe", + "sha256": "45f597a50d868f2fce11877f52983b2097cf6b9ac569c35122cc3fdc28d03705", + "size": 7680, + "executable": true, + "status": "ok" + } + } + }, + "xdelta3": { + "version": "3.2.0", + "license": "Apache-2.0", + "upstream_repo": "https://github.com/jmacd/xdelta", + "provenance": "upstream-release", + "platforms": { + "android/arm64-v8a": { + "path": "android/arm64-v8a/xdelta3", + "sha256": "55977f2792800d646baa40ebde0c9959f5f25806d5f0e23edb0499c00158ccaf", + "size": 430592, + "executable": true, + "status": "ok", + "version": "3.2.2" + }, + "linux/arm64": { + "path": "linux/arm64/xdelta3", + "sha256": "ae8e061fcd39379f2a5ac2c2790d257b44b231d9d7bc5c56ba2fa39abc9d5fcc", + "size": 149784, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/xdelta3", + "sha256": "0d38d86de5ab6bbc1adae531331d64585b5a09ce3604a5f090c27f71b6a64b23", + "size": 494384, + "executable": true, + "status": "ok" + }, + "macos/arm64": { + "path": "macos/arm64/xdelta3", + "sha256": "244a2643774155ab7dc563258a203674feafbbddc7f1f4a4f4486b00e78c1941", + "size": 374184, + "executable": true, + "status": "ok", + "universal": true + }, + "macos/x86_64": { + "path": "macos/x86_64/xdelta3", + "sha256": "244a2643774155ab7dc563258a203674feafbbddc7f1f4a4f4486b00e78c1941", + "size": 374184, + "executable": true, + "status": "ok", + "universal": true + }, + "windows/x86_64": { + "path": "windows/x86_64/xdelta3.exe", + "sha256": "53d90226615f217d3380c39892833311b4e24acd863e1ca01f14b5e772e2e6d0", + "size": 336896, + "executable": true, + "status": "ok" + } + } + }, + "xgdtool": { + "version": "1.0.0", + "license": "GPL-3.0-only", + "upstream_repo": "https://github.com/wiredopposite/XGDTool", + "provenance": "built-from-source", + "platforms": { + "linux/arm64": { + "path": "linux/arm64/XGDTool", + "sha256": "bb89cc23949cd0f0bcbb091762cca14146602e2dd61422c83227f280a895768b", + "size": 7901256, + "executable": true, + "status": "ok" + }, + "linux/x86_64": { + "path": "linux/x86_64/XGDTool", + "sha256": "8cab35913189cb142f08d0de6f152d13f89e716a18ff587cee9d9038d1d33610", + "size": 1611080, + "executable": true, + "status": "ok" + }, + "windows/x86_64": { + "path": "windows/x86_64/XGDTool.exe", + "sha256": "178576ffe9229209a127e76480ae28991648075354dafba9bd4dcd3a3afb8792", + "size": 6411776, + "executable": true, + "status": "ok" + } + } + } + } +} diff --git a/patches/nkit/0001-single-tfm-net10.patch b/patches/nkit/0001-single-tfm-net10.patch new file mode 100644 index 0000000..f8fde98 --- /dev/null +++ b/patches/nkit/0001-single-tfm-net10.patch @@ -0,0 +1,52 @@ +diff --git a/NKit/NKit.csproj b/NKit/NKit.csproj +index 5b81709..b5b2a29 100644 +--- a/NKit/NKit.csproj ++++ b/NKit/NKit.csproj +@@ -1,7 +1,7 @@ +  + + +- net10.0;net8.0;netstandard2.1 ++ net10.0 + NKitLib + latest + true +diff --git a/NKitCore/NKitCore.csproj b/NKitCore/NKitCore.csproj +index d0b91d4..0771fa8 100644 +--- a/NKitCore/NKitCore.csproj ++++ b/NKitCore/NKitCore.csproj +@@ -1,7 +1,7 @@ + + + +- net48;netstandard2.1;net8.0;net10.0 ++ net10.0 + disable + disable + latest +diff --git a/NKitDataStore/NKitDataStore.csproj b/NKitDataStore/NKitDataStore.csproj +index 822a416..e71c1b9 100644 +--- a/NKitDataStore/NKitDataStore.csproj ++++ b/NKitDataStore/NKitDataStore.csproj +@@ -2,7 +2,7 @@ + + + Library +- net10.0;net8.0;netstandard2.1 ++ net10.0 + latest + enable + enable +diff --git a/NKitStream/NKitStream.csproj b/NKitStream/NKitStream.csproj +index a77cec3..5bdc6ca 100644 +--- a/NKitStream/NKitStream.csproj ++++ b/NKitStream/NKitStream.csproj +@@ -1,7 +1,7 @@ +  + + +- net10.0;net8.0;netstandard2.1 ++ net10.0 + enable + enable + latest diff --git a/schema/manifest.schema.json b/schema/manifest.schema.json new file mode 100644 index 0000000..84fbb63 --- /dev/null +++ b/schema/manifest.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OpenROM Binaries Manifest", + "type": "object", + "required": ["format_version", "tools"], + "additionalProperties": false, + "properties": { + "format_version": { + "type": "integer", + "minimum": 1 + }, + "tools": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["version", "license", "upstream_repo", "provenance", "platforms"], + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "minLength": 1 + }, + "license": { + "type": "string", + "minLength": 1 + }, + "upstream_repo": { + "type": "string", + "format": "uri" + }, + "provenance": { + "type": "string", + "enum": ["built-from-source", "upstream-release", "npm-package"] + }, + "source_ref": { + "type": "string" + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-fA-F]{40}$" + }, + "built_at": { + "type": "string" + }, + "workflow_run_url": { + "type": "string" + }, + "toolchain": { + "type": "string" + }, + "patches": { + "type": "array", + "items": { + "type": "object", + "required": ["path", "sha256", "purpose"], + "properties": { + "path": { "type": "string" }, + "sha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" }, + "purpose": { "type": "string" } + } + } + }, + "build_flavor": { + "type": "string" + }, + "npm_package": { + "type": "string" + }, + "dist_integrity": { + "type": "string" + }, + "platforms": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["path", "sha256", "size", "executable", "status"], + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "executable": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": ["ok", "unverified", "mismatch"] + }, + "version": { + "type": "string" + }, + "universal": { + "type": "boolean" + }, + "same_as": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/scripts/fetch-bins.ps1 b/scripts/fetch-bins.ps1 new file mode 100644 index 0000000..71f17a0 --- /dev/null +++ b/scripts/fetch-bins.ps1 @@ -0,0 +1,128 @@ +# fetch-bins.ps1 - Download and verify OpenROM binaries on Windows / PowerShell 5.1 & 7 +# Exit codes: +# 0 - Success +# 1 - Invalid arguments or missing dependency +# 2 - Manifest or checksum error / tool not found +# 3 - Download failed (HTTP 404 / connection error) +# 4 - SHA-256 or size verification mismatch + +[CmdletBinding()] +param( + [string]$Os = "", + [string]$Arch = "", + [string]$Tool = "", + [string]$TargetDir = "." +) + +$ErrorActionPreference = "Stop" + +$baseUrl = $env:OPENROM_BINS_BASE_URL +if ([string]::IsNullOrWhiteSpace($baseUrl)) { + $baseUrl = "https://downloads.sourceforge.net/project/openrom-binaries" +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = Split-Path -Parent $scriptDir +$manifestPath = Join-Path $repoRoot "manifest.json" + +if (-not (Test-Path $manifestPath)) { + Write-Error "Error: manifest.json not found at $manifestPath" + exit 2 +} + +try { + $manifestJson = Get-Content -Raw -Path $manifestPath | ConvertFrom-Json +} catch { + Write-Error "Error reading manifest.json: $_" + exit 2 +} + +$matched = @() +$toolsObj = $manifestJson.tools + +foreach ($tProperties in $toolsObj.PSObject.Properties) { + $toolName = $tProperties.Name + if ($Tool -and ($toolName.ToLower() -ne $Tool.ToLower())) { + continue + } + + $toolData = $tProperties.Value + $platformsObj = $toolData.platforms + + foreach ($pProperties in $platformsObj.PSObject.Properties) { + $platKey = $pProperties.Name + $parts = $platKey.Split('/') + $platOs = $parts[0] + $platArch = if ($parts.Length -gt 1) { $parts[1] } else { "" } + + if ($Os -and ($platOs.ToLower() -ne $Os.ToLower())) { + continue + } + if ($Arch -and ($platArch.ToLower() -ne $Arch.ToLower())) { + continue + } + + $platData = $pProperties.Value + $matched += [PSCustomObject]@{ + Tool = $toolName + Platform = $platKey + Path = $platData.path + Sha256 = $platData.sha256 + Size = $platData.size + } + } +} + +if ($matched.Count -eq 0) { + Write-Error "No binaries found matching constraints (os='$Os', arch='$Arch', tool='$Tool')" + exit 2 +} + +Write-Host "Fetching $($matched.Count) binary/binaries to target directory: $TargetDir" + +if (-not (Test-Path $TargetDir)) { + New-Item -ItemType Directory -Path $TargetDir | Out-Null +} + +foreach ($item in $matched) { + $relPath = $item.Path + $expSha = $item.Sha256 + $expSize = $item.Size + $url = "$baseUrl/$relPath" + $filename = Split-Path -Leaf $relPath + $outFile = Join-Path $TargetDir $filename + + Write-Host "Downloading $relPath -> $outFile ..." + + try { + Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing + } catch { + Write-Error "Error downloading $url: $_" + exit 3 + } + + $fileStream = [System.IO.File]::OpenRead((Convert-Path $outFile)) + try { + $sha = [System.Security.Cryptography.SHA256]::Create() + $hashBytes = $sha.ComputeHash($fileStream) + $actSha = [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower() + } finally { + $fileStream.Close() + } + + $actSize = (Get-Item $outFile).Length + + if ($actSha -ne $expSha) { + Write-Error "SHA-256 mismatch for $relPath!`n Expected: $expSha`n Actual: $actSha" + Remove-Item -Force $outFile -ErrorAction SilentlyContinue + exit 4 + } + + if ($expSize -gt 0 -and $actSize -ne $expSize) { + Write-Error "Size mismatch for $relPath!`n Expected: $expSize bytes`n Actual: $actSize bytes" + Remove-Item -Force $outFile -ErrorAction SilentlyContinue + exit 4 + } + + Write-Host "Verified $filename successfully." +} diff --git a/scripts/fetch-bins.sh b/scripts/fetch-bins.sh new file mode 100755 index 0000000..68574f0 --- /dev/null +++ b/scripts/fetch-bins.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# fetch-bins.sh - Download and verify OpenROM binaries from SourceForge/Base URL +# Exit codes: +# 0 - Success +# 1 - Invalid arguments or missing dependency +# 2 - Manifest or checksum error / tool not found +# 3 - Download failed (HTTP 404 / connection error) +# 4 - SHA-256 or size verification mismatch + +set -euo pipefail + +BASE_URL="${OPENROM_BINS_BASE_URL:-https://downloads.sourceforge.net/project/openrom-binaries}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +MANIFEST_PATH="$REPO_ROOT/manifest.json" +TARGET_DIR="." +OS="" +ARCH="" +TOOL="" + +usage() { + echo "Usage: $0 [--os ] [--arch ] [--tool ] [--target-dir ]" + echo "Examples:" + echo " $0 --os linux --arch x86_64 --target-dir ./bin" + echo " $0 --tool chdman --os windows --arch x86_64" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --os) OS="$2"; shift 2 ;; + --arch) ARCH="$2"; shift 2 ;; + --tool) TOOL="$2"; shift 2 ;; + --target-dir) TARGET_DIR="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1" >&2; usage ;; + esac +done + +if [ ! -f "$MANIFEST_PATH" ]; then + echo "Error: manifest.json not found at $MANIFEST_PATH" >&2 + exit 2 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required" >&2 + exit 1 +fi + +if ! command -v curl >/dev/null 2>&1; then + echo "Error: curl is required" >&2 + exit 1 +fi + +ENTRIES_JSON=$(python3 -c ' +import json, sys + +manifest_path, os_filter, arch_filter, tool_filter = sys.argv[1:] +with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + +matched = [] +for tool_name, tool_data in manifest.get("tools", {}).items(): + if tool_filter and tool_name.lower() != tool_filter.lower(): + continue + platforms = tool_data.get("platforms", {}) + for plat_key, plat_data in platforms.items(): + plat_os, plat_arch = plat_key.split("/") if "/" in plat_key else (plat_key, "") + if os_filter and plat_os.lower() != os_filter.lower(): + continue + if arch_filter and plat_arch.lower() != arch_filter.lower(): + continue + matched.append({ + "tool": tool_name, + "platform": plat_key, + "path": plat_data["path"], + "sha256": plat_data["sha256"], + "size": plat_data.get("size", 0) + }) + +print(json.dumps(matched)) +' "$MANIFEST_PATH" "$OS" "$ARCH" "$TOOL") + +NUM_MATCHES=$(python3 -c "import json, sys; print(len(json.loads(sys.argv[1])))" "$ENTRIES_JSON") + +if [ "$NUM_MATCHES" -eq 0 ]; then + echo "No binaries found matching constraints (os=, arch=, tool=)" >&2 + exit 2 +fi + +echo "Fetching $NUM_MATCHES binary/binaries to target directory: $TARGET_DIR" + +mkdir -p "$TARGET_DIR" + +compute_sha256() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + else + python3 -c "import hashlib, sys; print(hashlib.sha256(open(sys.argv[1], 'rb').read()).hexdigest())" "$file" + fi +} + +lines=$(python3 -c ' +import json, sys +data = json.loads(sys.argv[1]) +for item in data: + p, s, z = item["path"], item["sha256"], item["size"] + print(f"{p}|{s}|{z}") +' "$ENTRIES_JSON") + +while IFS="|" read -r rel_path exp_sha exp_size; do + [ -z "$rel_path" ] && continue + url="$BASE_URL/$rel_path" + filename=$(basename "$rel_path") + out_file="$TARGET_DIR/$filename" + + echo "Downloading $rel_path -> $out_file ..." + if ! curl --retry 5 -fL "$url" -o "$out_file"; then + echo "Error downloading $url (HTTP 404 or network failure)" >&2 + exit 3 + fi + + act_sha=$(compute_sha256 "$out_file") + act_size=$(python3 -c "import os, sys; print(os.path.getsize(sys.argv[1]))" "$out_file") + + if [ "$act_sha" != "$exp_sha" ]; then + echo "SHA-256 mismatch for $rel_path!" >&2 + echo " Expected: $exp_sha" >&2 + echo " Actual: $act_sha" >&2 + rm -f "$out_file" + exit 4 + fi + + if [ "$exp_size" -gt 0 ] && [ "$act_size" -ne "$exp_size" ]; then + echo "Size mismatch for $rel_path!" >&2 + echo " Expected: $exp_size bytes" >&2 + echo " Actual: $act_size bytes" >&2 + rm -f "$out_file" + exit 4 + fi + + if [ "$(uname -s 2>/dev/null)" != "Windows_NT" ]; then + chmod +x "$out_file" + fi + + echo "Verified $filename successfully." +done <<< "$lines" diff --git a/scripts/gen_checksums.py b/scripts/gen_checksums.py new file mode 100755 index 0000000..617fc99 --- /dev/null +++ b/scripts/gen_checksums.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import json +import os + +def generate_files(): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + manifest_path = os.path.join(repo_root, "manifest.json") + checksums_path = os.path.join(repo_root, "checksums.sha256") + versions_path = os.path.join(repo_root, "versions.json") + + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + + checksum_lines = [] + versions_dict = {} + + tools = manifest.get("tools", {}) + for tool_name, tool_data in tools.items(): + if "version" in tool_data: + versions_dict[tool_name] = tool_data["version"] + + platforms = tool_data.get("platforms", {}) + for plat_key, plat_data in platforms.items(): + path = plat_data["path"] + sha256 = plat_data["sha256"] + checksum_lines.append(f"{sha256} {path}") + + # Sort checksum lines by relative path + checksum_lines.sort(key=lambda line: line.split(None, 1)[1]) + + # Write checksums.sha256 with LF line endings + checksums_content = "\n".join(checksum_lines) + "\n" + with open(checksums_path, "wb") as f: + f.write(checksums_content.encode("utf-8")) + + # Write versions.json (sorted keys, 2 space indent) + sorted_versions = dict(sorted(versions_dict.items())) + with open(versions_path, "w", encoding="utf-8") as f: + json.dump(sorted_versions, f, indent=2) + f.write("\n") + + print(f"Successfully generated checksums.sha256 ({len(checksum_lines)} entries) and versions.json.") + +if __name__ == "__main__": + generate_files() diff --git a/scripts/migrate_to_manifest.py b/scripts/migrate_to_manifest.py new file mode 100755 index 0000000..15c0c1f --- /dev/null +++ b/scripts/migrate_to_manifest.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +import json +import os +import re +import sys + +# Mapping tool keys to metadata +TOOL_METADATA = { + "chdman": { + "license": "GPL-2.0-or-later OR BSD-3-Clause", + "upstream_repo": "https://github.com/mamedev/mame", + "provenance": "npm-package", + "npm_package": "@emmercm/chdman-*" + }, + "maxcso": { + "license": "ISC", + "upstream_repo": "https://github.com/unknownbrackets/maxcso", + "provenance": "upstream-release" + }, + "ecm": { + "license": "GPL-2.0-only", + "upstream_repo": "https://github.com/kidoz/ecm", + "provenance": "upstream-release" + }, + "unecm": { + "license": "GPL-2.0-only", + "upstream_repo": "https://github.com/kidoz/ecm", + "provenance": "upstream-release" + }, + "xdelta3": { + "license": "Apache-2.0", + "upstream_repo": "https://github.com/jmacd/xdelta", + "provenance": "upstream-release" + }, + "nodtool": { + "license": "MIT OR Apache-2.0", + "upstream_repo": "https://github.com/encounter/nod", + "provenance": "upstream-release" + }, + "extract-xiso": { + "license": "BSD-4-Clause", + "upstream_repo": "https://github.com/XboxDev/extract-xiso", + "provenance": "upstream-release" + }, + "nkit": { + "license": "MIT", + "upstream_repo": "https://github.com/Nanook/NKit", + "provenance": "built-from-source", + "build_flavor": "legacy-unknown" + }, + "saturn-patcher": { + "license": "GPL-3.0-or-later", + "upstream_repo": "https://github.com/M5Devs/saturn-patcher", + "provenance": "built-from-source" + }, + "xgdtool": { + "license": "GPL-3.0-only", + "upstream_repo": "https://github.com/wiredopposite/XGDTool", + "provenance": "built-from-source" + } +} + +def parse_checksums(filepath): + entries = [] + with open(filepath, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(None, 1) + if len(parts) == 2: + sha256, path = parts[0], parts[1] + entries.append((sha256, path)) + return entries + +def main(): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + checksums_path = os.path.join(repo_root, "checksums.sha256") + versions_path = os.path.join(repo_root, "versions.json") + recon_path = os.path.join(repo_root, "recon_sourceforge_status.json") + + with open(versions_path, "r", encoding="utf-8") as f: + versions = json.load(f) + + sf_status = {} + if os.path.exists(recon_path): + with open(recon_path, "r", encoding="utf-8") as f: + sf_status = json.load(f) + + checksum_entries = parse_checksums(checksums_path) + + manifest_tools = {} + + for sha256, rel_path in checksum_entries: + # e.g., android/arm64-v8a/chdman or windows/x86_64/XGDTool.exe + parts = rel_path.split("/") + if len(parts) != 3: + print(f"Skipping unusual path: {rel_path}", file=sys.stderr) + continue + + os_name, arch_name, filename = parts + platform_key = f"{os_name}/{arch_name}" + + # Identify tool key + tool_key = filename + if filename.endswith(".exe"): + tool_key = filename[:-4] + tool_key_lower = tool_key.lower() + + if tool_key_lower not in versions and tool_key_lower not in TOOL_METADATA: + print(f"Unknown tool for path: {rel_path}", file=sys.stderr) + continue + + tool_canonical = tool_key_lower + if tool_canonical not in manifest_tools: + meta = TOOL_METADATA.get(tool_canonical, {}) + manifest_tools[tool_canonical] = { + "version": versions.get(tool_canonical, "0.0.0"), + "license": meta.get("license", "UNKNOWN"), + "upstream_repo": meta.get("upstream_repo", "https://github.com/M5Devs"), + "provenance": meta.get("provenance", "upstream-release"), + "platforms": {} + } + if "build_flavor" in meta: + manifest_tools[tool_canonical]["build_flavor"] = meta["build_flavor"] + if "npm_package" in meta: + manifest_tools[tool_canonical]["npm_package"] = meta["npm_package"] + + # Platform details + size = 0 + if rel_path in sf_status and "size" in sf_status[rel_path]: + size = sf_status[rel_path]["size"] + elif rel_path == "windows/x86_64/XGDTool.exe": + size = 6411776 + + plat_entry = { + "path": rel_path, + "sha256": sha256, + "size": size, + "executable": True, + "status": "ok" + } + + # Handle universal / same_as annotations + if os_name == "macos" and filename in ["extract-xiso", "xdelta3"]: + plat_entry["universal"] = True + + if os_name == "android": + # android bins matching linux/arm64 + if filename in ["chdman", "ecm", "nkit", "unecm"]: + plat_entry["same_as"] = f"linux/arm64/{filename}" + if filename == "xdelta3": + plat_entry["version"] = "3.2.2" + + manifest_tools[tool_canonical]["platforms"][platform_key] = plat_entry + + # Sort tools alphabetically + sorted_tools = dict(sorted(manifest_tools.items())) + + manifest = { + "format_version": 1, + "tools": sorted_tools + } + + out_manifest_path = os.path.join(repo_root, "manifest.json") + with open(out_manifest_path, "w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + f.write("\n") + + print(f"Successfully created manifest.json with {len(sorted_tools)} tools.") + +if __name__ == "__main__": + main() diff --git a/scripts/sourceforge_known_hosts b/scripts/sourceforge_known_hosts new file mode 100644 index 0000000..9233633 --- /dev/null +++ b/scripts/sourceforge_known_hosts @@ -0,0 +1,2 @@ +frs.sourceforge.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQD35Ujalhh+JJkPvMckDlhu4dS7WH6NsOJ15iGCJLC +frs.sourceforge.net ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2uifHZbNexw6cXbyg1JnzDitL5VhYs0E65Hk/tLAPmcmm5GuiGeUoI/B0eUSNFsbqzwgwrttjnzKMKiGLN5CWVmlN1IXGGAfLYsQwK6wAu7kYFzkqP4jcwc5Jr9UPRpJdYIK733tSEmzab4qc5Oq8izKQKIaxXNe7FgmL15HjSpatFt9w/ot/CHS78FUAr3j3RwekHCm/jhPeqhlMAgC+jUgNJbFt3DlhDaRMa0NYamVzmX8D47rtmBbEDU3ld6AezWBPUR5Lh7ODOwlfVI58NAf/aYNlmvl2TZiauBCTa7OPYSyXJnIPbQXg6YQlDknNCr0K769EjeIlAfY87Z4tw== diff --git a/scripts/update_manifest_entry.py b/scripts/update_manifest_entry.py new file mode 100755 index 0000000..5d7ba4c --- /dev/null +++ b/scripts/update_manifest_entry.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import subprocess + +def main(): + parser = argparse.ArgumentParser(description="Update manifest.json entry and regenerate checksums") + parser.add_argument("--tool", required=True, help="Canonical tool key (e.g., chdman, nkit)") + parser.add_argument("--os", required=True, help="Target OS (e.g., linux, windows, macos, android)") + parser.add_argument("--arch", required=True, help="Target Arch (e.g., x86_64, arm64, arm64-v8a)") + parser.add_argument("--file-path", required=True, help="Relative path of the binary (e.g., linux/x86_64/nkit)") + parser.add_argument("--sha256", required=True, help="Computed SHA-256 hash") + parser.add_argument("--size", type=int, required=True, help="File size in bytes") + parser.add_argument("--version", help="Tool version") + parser.add_argument("--source-ref", help="Upstream tag or ref") + parser.add_argument("--source-commit", help="40-char git commit SHA") + parser.add_argument("--built-at", help="ISO timestamp of build") + parser.add_argument("--workflow-run-url", help="GitHub Actions run URL") + parser.add_argument("--toolchain", help="Compiler / toolchain version used") + parser.add_argument("--build-flavor", help="Build flavor e.g. nativeaot") + + args = parser.parse_args() + + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + manifest_path = os.path.join(repo_root, "manifest.json") + + if not os.path.exists(manifest_path): + print(f"Error: manifest.json not found at {manifest_path}", file=sys.stderr) + sys.exit(1) + + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + + tool_key = args.tool.lower() + platform_key = f"{args.os}/{args.arch}" + + tools = manifest.setdefault("tools", {}) + if tool_key not in tools: + print(f"Error: Tool '{tool_key}' not present in manifest.json", file=sys.stderr) + sys.exit(1) + + tool_data = tools[tool_key] + + if args.version: + tool_data["version"] = args.version + if args.source_ref: + tool_data["source_ref"] = args.source_ref + if args.source_commit: + tool_data["source_commit"] = args.source_commit + if args.built_at: + tool_data["built_at"] = args.built_at + if args.workflow_run_url: + tool_data["workflow_run_url"] = args.workflow_run_url + if args.toolchain: + tool_data["toolchain"] = args.toolchain + if args.build_flavor: + tool_data["build_flavor"] = args.build_flavor + + platforms = tool_data.setdefault("platforms", {}) + plat_entry = platforms.get(platform_key, {}) + + plat_entry["path"] = args.file_path + plat_entry["sha256"] = args.sha256 + plat_entry["size"] = args.size + plat_entry["executable"] = True + plat_entry["status"] = "ok" + + platforms[platform_key] = plat_entry + + # Save manifest.json + with open(manifest_path, "w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + f.write("\n") + + # Regenerate checksums.sha256 and versions.json + gen_script = os.path.join(repo_root, "scripts", "gen_checksums.py") + subprocess.check_call([sys.executable, gen_script]) + + print(f"Updated manifest entry for {tool_key} ({platform_key}) successfully.") + +if __name__ == "__main__": + main() diff --git a/scripts/validate_manifest.py b/scripts/validate_manifest.py new file mode 100755 index 0000000..188bde0 --- /dev/null +++ b/scripts/validate_manifest.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import json +import os +import re +import sys +import jsonschema + +def load_json(filepath): + with open(filepath, "r", encoding="utf-8") as f: + return json.load(f) + +def validate_manifest(): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + manifest_path = os.path.join(repo_root, "manifest.json") + schema_path = os.path.join(repo_root, "schema", "manifest.schema.json") + checksums_path = os.path.join(repo_root, "checksums.sha256") + versions_path = os.path.join(repo_root, "versions.json") + + # 1. Validate JSON schema + manifest = load_json(manifest_path) + schema = load_json(schema_path) + + try: + jsonschema.validate(instance=manifest, schema=schema) + except jsonschema.ValidationError as e: + print(f"Manifest schema validation error: {e.message}", file=sys.stderr) + return False + + tools = manifest.get("tools", {}) + all_paths = set() + hash_to_paths = {} + + # 2. Check generated files consistency + gen_checksum_lines = [] + gen_versions = {} + + for tool_name, tool_data in tools.items(): + # Validate tool level required attributes + if not tool_data.get("version"): + print(f"Tool '{tool_name}' missing version", file=sys.stderr) + return False + if not tool_data.get("license"): + print(f"Tool '{tool_name}' missing license", file=sys.stderr) + return False + + gen_versions[tool_name] = tool_data["version"] + + provenance = tool_data.get("provenance") + if provenance == "built-from-source": + # For built-from-source, source_commit required if not legacy + source_commit = tool_data.get("source_commit") + build_flavor = tool_data.get("build_flavor") + if tool_name == "nkit": + if build_flavor != "nativeaot" and build_flavor != "legacy-unknown": + print(f"NKit build_flavor must be 'nativeaot' or 'legacy-unknown', got '{build_flavor}'", file=sys.stderr) + return False + if source_commit and not re.match(r"^[0-9a-fA-F]{40}$", source_commit): + print(f"Tool '{tool_name}' invalid source_commit SHA: {source_commit}", file=sys.stderr) + return False + + platforms = tool_data.get("platforms", {}) + for plat_key, plat_data in platforms.items(): + path = plat_data["path"] + sha256 = plat_data["sha256"] + + # Unique path check + if path in all_paths: + print(f"Duplicate path in manifest: {path}", file=sys.stderr) + return False + all_paths.add(path) + + hash_to_paths.setdefault(sha256, []).append((tool_name, plat_key, path, plat_data)) + + # Check NKit single-file rule + if tool_name == "nkit": + if "nkit/" in path or "nkds" in path: + print(f"Forbidden NKit path structure (must be single file): {path}", file=sys.stderr) + return False + filename = os.path.basename(path) + if filename not in ["nkit", "nkit.exe"]: + print(f"Invalid NKit binary name: {path}", file=sys.stderr) + return False + + gen_checksum_lines.append(f"{sha256} {path}") + + # 3. Check hash sharing rules + for sha256, entries in hash_to_paths.items(): + if len(entries) > 1: + for i in range(len(entries)): + for j in range(i + 1, len(entries)): + t1, p1, path1, data1 = entries[i] + t2, p2, path2, data2 = entries[j] + + os1, arch1 = p1.split("/") if "/" in p1 else (p1, "") + os2, arch2 = p2.split("/") if "/" in p2 else (p2, "") + + # Same tool + same OS + different arch -> universal flag required + if t1 == t2 and os1 == os2 and arch1 != arch2: + if not data1.get("universal") or not data2.get("universal"): + print(f"Same tool/OS across arches must specify 'universal: true': {path1} vs {path2}", file=sys.stderr) + return False + + # Identical files across different OSes / entries require explicit 'same_as' + if os1 != os2 or t1 != t2: + has_same_as = data1.get("same_as") or data2.get("same_as") + if not has_same_as: + print(f"Shared hash across different OSes/tools requires 'same_as': {path1} vs {path2}", file=sys.stderr) + return False + + # 4. Verify checksums.sha256 on disk matches + gen_checksum_lines.sort(key=lambda line: line.split(None, 1)[1]) + expected_checksums_content = "\n".join(gen_checksum_lines) + "\n" + with open(checksums_path, "rb") as f: + actual_checksums_content = f.read().decode("utf-8") + + if actual_checksums_content != expected_checksums_content: + print("checksums.sha256 is out of date with manifest.json!", file=sys.stderr) + return False + + # 5. Verify versions.json on disk matches + sorted_versions = dict(sorted(gen_versions.items())) + expected_versions_content = json.dumps(sorted_versions, indent=2) + "\n" + with open(versions_path, "r", encoding="utf-8") as f: + actual_versions_content = f.read() + + if actual_versions_content != expected_versions_content: + print("versions.json is out of date with manifest.json!", file=sys.stderr) + return False + + print("Manifest validation passed successfully!") + return True + +if __name__ == "__main__": + if not validate_manifest(): + sys.exit(1) diff --git a/tests/test_manifest.py b/tests/test_manifest.py new file mode 100644 index 0000000..62421b9 --- /dev/null +++ b/tests/test_manifest.py @@ -0,0 +1,151 @@ +import json +import os +import re +import subprocess +import threading +from http.server import HTTPServer, SimpleHTTPRequestHandler +import pytest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MANIFEST_PATH = os.path.join(REPO_ROOT, "manifest.json") +CHECKSUMS_PATH = os.path.join(REPO_ROOT, "checksums.sha256") +VERSIONS_PATH = os.path.join(REPO_ROOT, "versions.json") + +def test_manifest_validation_script(): + cmd = ["python3", os.path.join(REPO_ROOT, "scripts", "validate_manifest.py")] + res = subprocess.run(cmd, capture_output=True, text=True) + assert res.returncode == 0, f"validate_manifest.py failed: {res.stderr}" + +def test_checksum_and_versions_roundtrip(): + # Run gen_checksums.py and check git diff / consistency + cmd = ["python3", os.path.join(REPO_ROOT, "scripts", "gen_checksums.py")] + res = subprocess.run(cmd, capture_output=True, text=True) + assert res.returncode == 0, f"gen_checksums.py failed: {res.stderr}" + + # Verify checksums.sha256 format (64-hex path, sorted) + with open(CHECKSUMS_PATH, "r", encoding="utf-8") as f: + lines = f.readlines() + + assert len(lines) > 0 + paths = [] + for line in lines: + assert line.endswith("\n") or line.endswith("\r\n") + line_clean = line.strip() + assert len(line_clean) > 66 + parts = line_clean.split(None, 1) + sha, path = parts[0], parts[1] + assert re.match(r"^[0-9a-fA-F]{64}$", sha) + paths.append(path) + + assert paths == sorted(paths) + +def test_nkit_single_file_rule(): + with open(MANIFEST_PATH, "r", encoding="utf-8") as f: + manifest = json.load(f) + + nkit_data = manifest["tools"]["nkit"] + for plat_key, plat_entry in nkit_data["platforms"].items(): + path = plat_entry["path"] + assert "nkit/" not in path, f"NKit path must not contain subfolder 'nkit/': {path}" + assert "nkds" not in path, f"NKit path must not reference 'nkds': {path}" + filename = os.path.basename(path) + assert filename in ["nkit", "nkit.exe"], f"NKit binary filename must be nkit or nkit.exe: {filename}" + +def test_input_validation_regexes(): + # Regexes used in update-binary and other workflows + tool_regex = re.compile(r"^[a-z0-9-]+$") + filename_regex = re.compile(r"^[A-Za-z0-9._-]+$") + sha256_regex = re.compile(r"^[0-9a-fA-F]{64}$") + + assert tool_regex.match("chdman") + assert tool_regex.match("saturn-patcher") + assert not tool_regex.match("Tool_Name") + + assert filename_regex.match("XGDTool.exe") + assert filename_regex.match("extract-xiso") + assert not filename_regex.match("file;rm -rf /") + + assert sha256_regex.match("a" * 64) + assert not sha256_regex.match("a" * 63) + +@pytest.fixture +def dummy_server(tmp_path): + # Setup dummy directory layout matching manifest relative path for 1 dummy binary + os_dir = tmp_path / "linux" / "x86_64" + os_dir.mkdir(parents=True) + dummy_bin = os_dir / "saturn-patcher" + + # We will test fetching saturn-patcher for linux/x86_64 + with open(MANIFEST_PATH, "r") as f: + manifest = json.load(f) + + exp_entry = manifest["tools"]["saturn-patcher"]["platforms"]["linux/x86_64"] + dummy_data = b"DUMMY_SATURN_PATCHER_BINARY_DATA" + + # Write a test manifest override or mock data + import hashlib + actual_sha = hashlib.sha256(dummy_data).hexdigest() + + # Modify dummy manifest in tmp_path + mock_manifest = json.loads(json.dumps(manifest)) + mock_manifest["tools"]["saturn-patcher"]["platforms"]["linux/x86_64"]["sha256"] = actual_sha + mock_manifest["tools"]["saturn-patcher"]["platforms"]["linux/x86_64"]["size"] = len(dummy_data) + + manifest_file = tmp_path / "manifest.json" + with open(manifest_file, "w") as f: + json.dump(mock_manifest, f) + + dummy_bin.write_bytes(dummy_data) + + class QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, format, *args): + pass + + os.chdir(tmp_path) + server = HTTPServer(("127.0.0.1", 0), QuietHandler) + port = server.server_port + + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + + yield f"http://127.0.0.1:{port}", tmp_path, manifest_file + + server.shutdown() + os.chdir(REPO_ROOT) + +def test_fetch_bins_script(dummy_server): + base_url, tmp_dir, manifest_file = dummy_server + target_dir = tmp_dir / "target" + + env = os.environ.copy() + env["OPENROM_BINS_BASE_URL"] = base_url + + script_path = os.path.join(REPO_ROOT, "scripts", "fetch-bins.sh") + + # We pass manifest path override via working directory repo root mock + # Replace manifest in REPO_ROOT temporarily or test with script directly + cmd = [ + script_path, + "--tool", "saturn-patcher", + "--os", "linux", + "--arch", "x86_64", + "--target-dir", str(target_dir) + ] + + # Temporarily copy mock manifest to REPO_ROOT/manifest.json + with open(MANIFEST_PATH, "rb") as f: + orig_manifest_content = f.read() + + try: + with open(MANIFEST_PATH, "wb") as f: + f.write(manifest_file.read_bytes()) + + res = subprocess.run(cmd, env=env, capture_output=True, text=True) + assert res.returncode == 0, f"fetch-bins.sh failed: {res.stderr}" + fetched_file = target_dir / "saturn-patcher" + assert fetched_file.exists() + assert fetched_file.read_bytes() == b"DUMMY_SATURN_PATCHER_BINARY_DATA" + finally: + with open(MANIFEST_PATH, "wb") as f: + f.write(orig_manifest_content) diff --git a/tests/test_publish.py b/tests/test_publish.py new file mode 100644 index 0000000..6f5cf91 --- /dev/null +++ b/tests/test_publish.py @@ -0,0 +1,78 @@ +import json +import os +import re +import shutil +import subprocess +import pytest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MANIFEST_PATH = os.path.join(REPO_ROOT, "manifest.json") +CHECKSUMS_PATH = os.path.join(REPO_ROOT, "checksums.sha256") +VERSIONS_PATH = os.path.join(REPO_ROOT, "versions.json") + +def test_update_manifest_entry_script(tmp_path): + # Copy repo files into isolated tmp_path + tmp_repo = tmp_path / "repo" + tmp_repo.mkdir() + + shutil.copy(MANIFEST_PATH, tmp_repo / "manifest.json") + shutil.copy(CHECKSUMS_PATH, tmp_repo / "checksums.sha256") + shutil.copy(VERSIONS_PATH, tmp_repo / "versions.json") + + scripts_dir = tmp_repo / "scripts" + scripts_dir.mkdir() + shutil.copy(os.path.join(REPO_ROOT, "scripts", "update_manifest_entry.py"), scripts_dir / "update_manifest_entry.py") + shutil.copy(os.path.join(REPO_ROOT, "scripts", "gen_checksums.py"), scripts_dir / "gen_checksums.py") + + dummy_sha = "1" * 64 + + cmd = [ + "python3", str(scripts_dir / "update_manifest_entry.py"), + "--tool", "saturn-patcher", + "--os", "linux", + "--arch", "x86_64", + "--file-path", "linux/x86_64/saturn-patcher", + "--sha256", dummy_sha, + "--size", "123456", + "--version", "0.2.0", + "--source-ref", "v0.2.0", + "--source-commit", "a" * 40, + "--built-at", "2026-01-01T00:00:00Z", + "--workflow-run-url", "https://github.com/M5Devs/OpenROM-bins/actions/runs/123", + "--toolchain", "rustc 1.85.0", + "--build-flavor", "release" + ] + + res = subprocess.run(cmd, cwd=tmp_repo, capture_output=True, text=True) + assert res.returncode == 0, f"update_manifest_entry.py failed: {res.stderr}" + + with open(tmp_repo / "manifest.json", "r", encoding="utf-8") as f: + updated_manifest = json.load(f) + + tool = updated_manifest["tools"]["saturn-patcher"] + assert tool["version"] == "0.2.0" + assert tool["source_ref"] == "v0.2.0" + assert tool["source_commit"] == "a" * 40 + assert tool["toolchain"] == "rustc 1.85.0" + + plat = tool["platforms"]["linux/x86_64"] + assert plat["sha256"] == dummy_sha + assert plat["size"] == 123456 + assert plat["status"] == "ok" + +def test_workflow_input_validation_regexes(): + os_allowed = ["windows", "linux", "macos", "android"] + arch_allowed = ["x86_64", "arm64", "arm64-v8a"] + tool_regex = re.compile(r"^[a-z0-9-]+$") + artifact_regex = re.compile(r"^[A-Za-z0-9._-]+$") + commit_regex = re.compile(r"^[0-9a-fA-F]{40}$") + + assert "linux" in os_allowed + assert "x86_64" in arch_allowed + assert tool_regex.match("chdman") + assert artifact_regex.match("chdman-linux-x64") + assert commit_regex.match("b" * 40) + + assert "invalid_os" not in os_allowed + assert not tool_regex.match("tool; rm -rf /") + assert not commit_regex.match("short_sha") diff --git a/versions.json b/versions.json index 7ac4da5..4c70477 100644 --- a/versions.json +++ b/versions.json @@ -1,11 +1,12 @@ { "chdman": "mame0289", + "ecm": "2.0.0", + "extract-xiso": "build-202609111233", "maxcso": "1.4.0", "nkit": "3.1.0", "nodtool": "2.0.0-alpha.12", - "xdelta3": "3.2.0", - "ecm": "2.0.0", - "extract-xiso": "build-202609111233", "saturn-patcher": "0.1.0", + "unecm": "0.0.0", + "xdelta3": "3.2.0", "xgdtool": "1.0.0" }