diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5c35e52 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,113 @@ +################################################################################ +# How to create a tag to launch the workflow +# git tag -a "0.1.0" -m "Release v0.1.0" +# git push origin --tags +################################################################################ +# WARNING: Make sure to set action permissions to max ("Read and write permissions") +name: Build/Release +on: + workflow_dispatch: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+*" + +permissions: + contents: write + +jobs: + synthesis: + runs-on: ubuntu-latest + strategy: + matrix: + core: ["main", "pal", "spc", "sa1gsu", "pal_sa1gsu"] + + steps: + - name: "🧰 Checkout Repository" + uses: actions/checkout@v4 + + - name: "🧰 Checkout Packybara" + uses: actions/checkout@v4 + with: + repository: "agg23/pocketpublish" + path: ".github/publish" + + - name: "🏗️ Compile design" + shell: bash + run: | + map_value() { + case "$1" in + main) + echo "ntsc" + ;; + pal) + echo "pal" + ;; + spc) + echo "ntsc_spc" + ;; + sa1gsu) + echo "ntsc_sa1gsu" + ;; + pal_sa1gsu) + echo "pal_sa1gsu" + ;; + *) + echo "Unknown input" + ;; + esac + } + + build_type=$(map_value "${{ matrix.core }}") + cmd="docker run --rm -v ${{ github.workspace }}:/build raetro/quartus:21.1 quartus_sh -t generate.tcl $build_type" + echo "Running command: $cmd" + eval $cmd + + - name: Upload failure artifact + uses: actions/upload-artifact@v4 + if: failure() + with: + name: failure_output_files + path: projects/output_files/ + + - name: Reverse bitstream + shell: bash + run: | + mkdir artifacts + pip3 install -r .github/publish/requirements.txt + python3 .github/publish/reverse.py projects/output_files/snes_pocket.rbf artifacts/snes_${{ matrix.core }}.rev + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: snes_${{ matrix.core }} + path: artifacts/snes_${{ matrix.core }}.rev + + packaging: + runs-on: ubuntu-latest + needs: synthesis + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: "🧰 Checkout Repository" + uses: actions/checkout@v4 + + - name: "🧰 Checkout Packybara" + uses: actions/checkout@v4 + with: + repository: "agg23/pocketpublish" + path: ".github/publish" + + - name: "Create folders" + run: | + pip3 install -r .github/publish/requirements.txt + python3 .github/publish/create_folders.py + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: staging/Cores/agg23.SNES/ + merge-multiple: true + + - name: "Release" + run: | + python3 .github/publish/release.py diff --git a/.gitignore b/.gitignore index e0e4466..f26401d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.rbf_r *.zip +support/test/roms dist/**/*.rev dist/**/*.bin src/**/*.bin @@ -7,6 +8,7 @@ src/sim/work/ src/sim/*.hex src/sim/*.mem src/sim/*.bin +**/tmp # Quartus directories and files db diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e0f2626 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing + +## Releasing + +There is an automated build workflow at `.github/workflows/build.yml` triggered on tag pushes that start with a version number (it also can be triggered manually). This will build the split bitstream and compile the final user build. A draft release will be created with this build attached. + +To create a build: + +```bash +git tag -a "0.1.0" -m "Release v0.1.0" +git push origin --tags +``` \ No newline at end of file diff --git a/support/test/test.js b/support/test/test.js new file mode 100644 index 0000000..c67f720 --- /dev/null +++ b/support/test/test.js @@ -0,0 +1,168 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; + +const chip32SimDir = "../../../../../chip32-sim"; + +const tests = [ + { + name: "LoROM", + filename: "roms/F-Zero.smc", + expectedCore: 0, + }, + { + name: "HiROM", + filename: "roms/Chrono Trigger.smc", + expectedCore: 0, + }, + { + name: "ExHiROM", + filename: "roms/Tales of Phantasia.smc", + expectedCore: 0, + }, + { + name: "SPC7110", + filename: "roms/Momotarou Dentetsu Happy (Japan).sfc", + expectedCore: 1, + }, + { + name: "SDD1", + filename: "roms/Star Ocean (Japan).sfc", + expectedCore: 1, + }, + // BSX isn't properly supported + // { + // name: "BSX", + // filename: "roms/[ikari] Super Bomberman.bs", + // expectedCore: 1, + // }, + { + name: "PAL LoROM", + filename: "roms/F-Zero (Europe).sfc", + expectedCore: 2, + }, +]; + +const makeDataJson = (testCase) => { + const filename = path.join(import.meta.dirname, testCase.filename); + + const contents = { + data: { + magic: "APF_VER_1", + data_slots: [ + { + name: "SMC", + id: 0, + filename, + required: true, + parameters: "0x109", + extensions: ["smc", "sfc"], + address: "0x10000000", + }, + ], + }, + }; + + return JSON.stringify(contents, null, 2); +}; + +const printChip32Output = (json) => { + console.log("Chip32 output:"); + console.log(` Core: ${json.core}`); + console.log(` File state: ${json.file_state}`); + console.log(` Logs:`); + const logs = json.logs.join("\n "); + console.log(` ${logs}`); +}; + +const runTest = (testCase, testFilePath) => { + console.log(`Running test: ${testCase.name} (File: ${testCase.filename})`); + + try { + const loaderPinPath = path.join(import.meta.dirname, "../loader.bin"); + const stdout = execSync( + `cargo run --quiet -- --data-json "${testFilePath}" --bin "${loaderPinPath}" --json`, + { encoding: "utf8", cwd: chip32SimDir } + ); + + let jsonOutput; + try { + jsonOutput = JSON.parse(stdout); + } catch (error) { + console.error(` ❌ Test failed: Invalid JSON output.\n${error}`); + console.error("Stdout: ", stdout); + return false; + } + + if (jsonOutput.core !== testCase.expectedCore) { + console.error( + ` ❌ Test failed: Expected core ${testCase.expectedCore}, but got ${jsonOutput.core}` + ); + printChip32Output(jsonOutput); + return false; + } + if (jsonOutput.file_state === null || jsonOutput.file_state === undefined) { + console.error(` ❌ Test failed: file_state is null or undefined`); + return false; + } + + console.log(` ✅ Test passed.\n`); + return true; + } catch (error) { + console.error(` ❌ Test failed: Command exited with code ${error.status}`); + if (error.stderr) { + console.error(` Stderr:\n${error.stderr}`); + } + try { + const jsonOutput = JSON.parse(error.stdout.toString()); + printChip32Output(jsonOutput); + } catch { + console.error("Stdout: ", error.stdout.toString()); + } + + console.log("\n"); + return false; + } +}; + +const runAllTests = () => { + let failCount = 0; + + if (!fs.existsSync("tmp")) { + fs.mkdirSync("tmp"); + } + + for (const testCase of tests) { + const singleTestFilePath = path.join( + import.meta.dirname, + "tmp", + `test_roms_${testCase.name}.json` + ); + fs.writeFileSync(singleTestFilePath, makeDataJson(testCase), "utf8"); + + const passed = runTest(testCase, singleTestFilePath); + if (!passed) { + failCount += 1; + } + } + + if (failCount === 0) { + console.log("\nAll tests passed!"); + return true; + } else { + console.error(`\n${failCount} tests failed!`); + return false; + } +}; + +let success = false; + +try { + success = runAllTests(); +} finally { + // fs.rmSync("tmp", { recursive: true, force: true }); +} + +if (!success) { + process.exit(1); +}