diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 00000000..8dae1c77 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,233 @@ +# Windows one-click app, built on GitHub's Windows runners so no PC is needed (4 October 2026). +# +# parse: every .ps1 under the Windows folders through the Windows PowerShell 5.1 parser (powershell.exe, the PowerShell +# on the PCs; 5.1 rejects "$name: text" and that class broke two launchers on 4 October), PSScriptAnalyzer as +# warnings, and a parenthesis check of every .bat/.cmd (the bare ")" class of 3 October). Required: build +# needs it. +# build: the engine (app/igneum-app, cargo on the MSVC target, so one fewer input), the window host exactly as +# app\windows\BUILD-APP.bat does it (MSVC, WebView2 SDK from NuGet, static loader, host.rc with the coin icon), +# the payload with packaging/windows/make-payload.sh in Git Bash, the installer with build-installer.ps1 +# (Inno Setup, rcedit), a smoke run of both exes (--version, --help), the launcher's DRY_RUN, then the installer, +# the payload zip and the host as artifacts (90 days). +# +# Inputs that are not in git (igneumd.exe, igneum-miner.exe from the node fork; the prebuilt GPU workers with NVIDIA's +# NVRTC DLLs) come from payload-inputs.zip on the downloads host, published by packaging/windows/push-inputs.sh on the +# Mac; the DL_TOKEN repository secret is the path token (gh secret set DL_TOKEN < ~/.config/igneum/dl-token). +# The Mac side of the loop is packaging/windows/fetch-ci-artifacts.sh (gh run download into the downloads folder). +name: windows-ci +on: + push: + branches: [master] + paths: + - 'app/**' + - 'packaging/windows/**' + - 'packaging/mac/packaged-config.sh' + - 'proto-cuda/windows-app/**' + - 'proto-cuda/windows-miner/**' + - 'proto-cuda/windows-node/**' + - 'proto-cuda/nvrtc/**' + - 'proto-cuda/build.bat' + - 'proto-opencl/**' + - 'proving/windows-wsl2/**' + - 'relay/clients/**' + - 'brand/icons/**' + - 'tools/ci/windows/**' + - '.github/workflows/windows.yml' + workflow_dispatch: + +concurrency: + group: windows-${{ github.ref }} + cancel-in-progress: true + +jobs: + parse: + name: PowerShell 5.1 parse, PSScriptAnalyzer, batch parentheses + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Windows PowerShell 5.1 parse of every .ps1 (with the negative self-test) + shell: powershell + run: | + $PSVersionTable.PSVersion.ToString() + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: parentheses in every .bat and .cmd (with the negative self-test) + shell: powershell + run: | + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-bat.ps1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: PSScriptAnalyzer (warnings only, never fails the job) + shell: powershell + continue-on-error: true + run: | + try { + if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -AllowClobber + } + Import-Module PSScriptAnalyzer + $folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node', 'proving/windows-wsl2', 'relay/clients', 'packaging/windows', 'tools/ci/windows') + $total = 0 + foreach ($f in $folders) { + $results = Invoke-ScriptAnalyzer -Path $f -Recurse -Severity Warning, Error -ExcludeRule PSAvoidUsingWriteHost, PSUseShouldProcessForStateChangingFunctions, PSUseSingularNouns, PSAvoidUsingPositionalParameters + foreach ($r in $results) { + $total += 1 + $file = ($r.ScriptPath -replace '\\', '/') + Write-Host ("::warning file={0},line={1}::{2}: {3}" -f $file, $r.Line, $r.RuleName, $r.Message) + } + } + Write-Host "PSScriptAnalyzer: $total warnings (informational)" + } catch { + Write-Host "::warning::PSScriptAnalyzer could not run: $_" + } + + build: + name: engine, window host, payload, installer, smoke run + needs: parse + runs-on: windows-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: versions + shell: bash + run: | + set -euo pipefail + v="$(sed -n 's/^version = "\(.*\)"/\1/p' app/igneum-app/Cargo.toml | head -1)" + echo "APP_VERSION=$v" >> "$GITHUB_ENV" + echo "app version $v" + rustc --version; cargo --version + git --version; bash --version | head -1; perl --version | sed -n 2p; 7z 2>/dev/null | head -2 | tail -1 || true + + - name: engine (app/igneum-app, cargo build --release on the MSVC target) + shell: bash + working-directory: app/igneum-app + run: | + set -euo pipefail + cargo build --release --locked + ls -la target/release/igneum-app.exe + + - name: payload inputs (payload-inputs.zip from the downloads host, sha256 checked) + shell: bash + env: + DL_TOKEN: ${{ secrets.DL_TOKEN }} + run: | + set -euo pipefail + if [ -z "${DL_TOKEN:-}" ]; then + echo "::error::the DL_TOKEN repository secret is not set. On the Mac: tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum" + exit 1 + fi + base="https://dl.igneum.network/dl/$DL_TOKEN" + mkdir -p build/inputs "$HOME/.config/igneum" + printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token" # make-payload.sh reads it for the update manifest URL + curl -fsSL --retry 3 -o build/payload-inputs.json "$base/payload-inputs.json" + curl -fsSL --retry 3 -o build/payload-inputs.sha256 "$base/payload-inputs.sha256" + curl -fsSL --retry 3 -o build/payload-inputs.zip "$base/payload-inputs.zip" + echo "$(tr -d '[:space:]' < build/payload-inputs.sha256) build/payload-inputs.zip" | sha256sum -c - + 7z x -y -bso0 -bsp0 -obuild/inputs-unpacked build/payload-inputs.zip + mv build/inputs-unpacked/payload-inputs/* build/inputs/ + echo "inputs manifest:"; cat build/payload-inputs.json + echo "inputs:"; ls -la build/inputs + for f in igneumd.exe igneum-miner.exe; do [ -f "build/inputs/$f" ] || { echo "::error::payload-inputs.zip has no $f"; exit 1; }; done + + - name: window host (app\windows\BUILD-APP.bat, exactly as on the PC) + shell: cmd + working-directory: app\windows + run: call BUILD-APP.bat < nul + + - name: payload (packaging/windows/make-payload.sh, as on the Mac, in Git Bash) + shell: bash + run: | + set -euo pipefail + export IGNEUM_WIN_RELEASE="$PWD/build/inputs" + export IGNEUM_WORKERS_DIR="$PWD/build/inputs" + export IGNEUM_APP_EXE="$PWD/app/igneum-app/target/release/igneum-app.exe" + packaging/windows/make-payload.sh "$PWD/packaging/windows/dist/igneum-windows-app.zip" + test -f "packaging/windows/igneum-windows-app/Igneum Miner.exe" || { echo "::error::the window host did not land in the payload"; exit 1; } + + - name: installer (packaging/windows/build-installer.ps1 in Windows PowerShell 5.1, Inno Setup, rcedit) + shell: powershell + working-directory: packaging\windows + run: | + $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" + if (-not (Test-Path $iscc)) { choco install innosetup -y --no-progress | Out-Null } + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build-installer.ps1 -NoWinget -Version $env:APP_VERSION + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Get-ChildItem dist | Format-Table Name, Length + + - name: smoke run (igneum-app.exe --version, Igneum Miner.exe --version and --help) + shell: powershell + run: | + $payload = Resolve-Path 'packaging\windows\igneum-windows-app' + function Run-Capture([string]$exe, [string]$flag) { + $out = Join-Path $env:RUNNER_TEMP ('smoke-' + [IO.Path]::GetRandomFileName() + '.txt') + $p = Start-Process -FilePath $exe -ArgumentList $flag -Wait -NoNewWindow -PassThru -RedirectStandardOutput $out + $text = if (Test-Path $out) { (Get-Content $out -Raw) } else { '' } + Write-Host ("{0} {1} -> exit {2}: {3}" -f (Split-Path -Leaf $exe), $flag, $p.ExitCode, $text.Trim()) + if ($p.ExitCode -ne 0) { throw "$exe $flag exited $($p.ExitCode)" } + return $text + } + $v = $env:APP_VERSION + $a = Run-Capture (Join-Path $payload 'igneum-app.exe') '--version' + if ($a -notmatch "igneum-app $([regex]::Escape($v))") { throw "igneum-app --version did not print 'igneum-app $v'" } + $b = Run-Capture (Join-Path $payload 'Igneum Miner.exe') '--version' + if ($b -notmatch "Igneum Miner $([regex]::Escape($v))") { throw "Igneum Miner.exe --version did not print 'Igneum Miner $v' (version.h and Cargo.toml differ?)" } + $c = Run-Capture (Join-Path $payload 'Igneum Miner.exe') '--help' + if ($c -notmatch 'Usage') { throw 'Igneum Miner.exe --help did not print the usage line' } + $info = (Get-Item (Join-Path $payload 'Igneum Miner.exe')).VersionInfo + Write-Host ("host version block: {0} {1} {2}" -f $info.ProductName, $info.ProductVersion, $info.FileDescription) + if ($info.ProductName -ne 'Igneum Miner') { throw 'the host exe carries no Igneum Miner version block (host.rc)' } + + - name: launcher dry run (proto-cuda/windows-app, DRY_RUN=1, Windows PowerShell 5.1 via the .ps1 and the .bat) + shell: powershell + run: | + $stage = Join-Path $env:RUNNER_TEMP 'launcher' + New-Item -ItemType Directory -Force -Path $stage | Out-Null + Copy-Item -Path 'proto-cuda\windows-app\*' -Destination $stage -Recurse -Force + foreach ($f in @('igneumd.exe', 'igneum-miner.exe', 'igneum-worker-cuda.exe', 'igneum-worker-opencl.exe')) { + if (Test-Path "build\inputs\$f") { Copy-Item "build\inputs\$f" $stage } + } + Get-ChildItem 'build\inputs' -Filter 'nvrtc*.dll' | Copy-Item -Destination $stage + $env:DRY_RUN = '1' + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $stage 'start-igneum.ps1') + if ($LASTEXITCODE -ne 0) { throw "start-igneum.ps1 DRY_RUN=1 exited $LASTEXITCODE" } + $bat = cmd /c "cd /d `"$stage`" && START-IGNEUM.bat < nul 2>&1" | Out-String + Write-Host $bat + if ($bat -notmatch 'dry run done') { throw 'START-IGNEUM.bat with DRY_RUN=1 did not reach the end of the plan' } + + - name: sizes + shell: bash + run: | + set -euo pipefail + { + echo "## Windows build $APP_VERSION" + echo + echo "| file | bytes |" + echo "|---|---:|" + for f in packaging/windows/dist/Igneum-Miner-Setup-*.exe packaging/windows/dist/igneum-windows-app.zip "app/windows/dist/Igneum Miner.exe" app/igneum-app/target/release/igneum-app.exe; do + printf '| %s | %s |\n' "$(basename "$f")" "$(stat -c %s "$f")" + done + echo + echo "inputs: $(tr -d '\n' < build/payload-inputs.json | head -c 400)" + } | tee -a "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: igneum-windows-installer + path: packaging/windows/dist/Igneum-Miner-Setup-*.exe + retention-days: 90 + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: igneum-windows-payload + path: packaging/windows/dist/igneum-windows-app.zip + retention-days: 90 + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: igneum-windows-host + path: app/windows/dist/Igneum Miner.exe + retention-days: 90 + if-no-files-found: error diff --git a/app/windows/host.cpp b/app/windows/host.cpp index fa376055..9dfaded0 100644 --- a/app/windows/host.cpp +++ b/app/windows/host.cpp @@ -8,8 +8,12 @@ // Untested on a real PC at the time of writing (written on a Mac): BUILD-APP.bat is the first run. #define WIN32_LEAN_AND_MEAN +#ifndef UNICODE #define UNICODE +#endif +#ifndef _UNICODE #define _UNICODE +#endif #include #include #include @@ -18,6 +22,7 @@ #include #include #include "WebView2.h" +#include "version.h" using namespace Microsoft::WRL; @@ -341,7 +346,40 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { return DefWindowProcW(hwnd, msg, wp, lp); } +// --version and --help print one line and exit, for the CI smoke run and support scripts. A windows-subsystem exe has +// no console of its own: the text goes to the inherited stdout when there is one (a pipe or a file), else to the +// parent's console. +static bool handleCliFlags() { + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (!argv) return false; + std::wstring text; + for (int i = 1; i < argc; i++) { + std::wstring a = argv[i]; + if (a == L"--version" || a == L"-V") text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n"; + else if (a == L"--help" || a == L"-h" || a == L"/?") + text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n" + L"Usage: \"Igneum Miner.exe\" [--version | --help]\r\n" + L"Without flags it starts igneum-app.exe --wrapper next to it and shows the dashboard in a WebView2 window.\r\n"; + } + LocalFree(argv); + if (text.empty()) return false; + HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE); + if (out == nullptr || out == INVALID_HANDLE_VALUE) { + if (AttachConsole(ATTACH_PARENT_PROCESS)) out = GetStdHandle(STD_OUTPUT_HANDLE); + } + if (out && out != INVALID_HANDLE_VALUE) { + int n = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0, nullptr, nullptr); + std::string utf8(n, 0); + WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), &utf8[0], n, nullptr, nullptr); + DWORD written = 0; + WriteFile(out, utf8.data(), (DWORD)utf8.size(), &written, nullptr); + } + return true; +} + int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { + if (handleCliFlags()) return 0; HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumMinerWindow"); if (GetLastError() == ERROR_ALREADY_EXISTS) { HWND other = FindWindowW(L"IgneumMinerWindow", nullptr); diff --git a/app/windows/host.rc b/app/windows/host.rc index 055c7f5d..e14e9c16 100644 --- a/app/windows/host.rc +++ b/app/windows/host.rc @@ -1,12 +1,14 @@ // Resources for Igneum Miner.exe (the window host): the coin icon and the version block. -// Compiled by BUILD-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i). +// Compiled by BUILD-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i). The version comes +// from version.h, shared with host.cpp. #include +#include "version.h" 1 ICON "igneum.ico" 1 VERSIONINFO -FILEVERSION 0,3,0,0 -PRODUCTVERSION 0,3,0,0 +FILEVERSION IGNEUM_HOST_VERSION_RC +PRODUCTVERSION IGNEUM_HOST_VERSION_RC FILEFLAGSMASK 0x3fL FILEFLAGS 0x0L FILEOS VOS_NT_WINDOWS32 @@ -19,12 +21,12 @@ BEGIN BEGIN VALUE "CompanyName", "Igneum" VALUE "FileDescription", "Igneum Miner" - VALUE "FileVersion", "0.3.0" + VALUE "FileVersion", IGNEUM_HOST_VERSION_STR VALUE "InternalName", "Igneum Miner" VALUE "LegalCopyright", "Igneum. Nothing is bought or sold." VALUE "OriginalFilename", "Igneum Miner.exe" VALUE "ProductName", "Igneum Miner" - VALUE "ProductVersion", "0.3.0" + VALUE "ProductVersion", IGNEUM_HOST_VERSION_STR END END BLOCK "VarFileInfo" diff --git a/app/windows/version.h b/app/windows/version.h new file mode 100644 index 00000000..3e5a0661 --- /dev/null +++ b/app/windows/version.h @@ -0,0 +1,8 @@ +// The version of "Igneum Miner.exe" (the window host). One place for host.rc (the version block Explorer shows) and +// host.cpp (--version). Keep it equal to app/igneum-app/Cargo.toml and the AppVersion default in +// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too. +#ifndef IGNEUM_HOST_VERSION_H +#define IGNEUM_HOST_VERSION_H +#define IGNEUM_HOST_VERSION_STR "0.3.0" +#define IGNEUM_HOST_VERSION_RC 0,3,0,0 +#endif diff --git a/packaging/windows/README.md b/packaging/windows/README.md index 062913a4..87bacfd7 100644 --- a/packaging/windows/README.md +++ b/packaging/windows/README.md @@ -1,9 +1,70 @@ # Igneum Miner for Windows (packaging/windows) +[![windows-ci](https://github.com/igneum-network/igneum/actions/workflows/windows.yml/badge.svg?branch=master)](https://github.com/igneum-network/igneum/actions/workflows/windows.yml) + Inno Setup installer around the app payload. Version 0.3.0 (4 October 2026): the app (engine + window host) instead of the console launchers of 0.1.0. -## On the Mac +## Built on GitHub, not on a PC (4 October 2026) + +`.github/workflows/windows.yml` builds the whole Windows app on a hosted Windows runner on every push to master that +touches `app/`, `packaging/windows/`, `proto-cuda/windows-*`, `proto-cuda/nvrtc/`, `proto-opencl/`, +`proving/windows-wsl2/` or `relay/clients/` (and on `gh workflow run windows.yml`). Nobody runs `BUILD-APP.bat` or +`BUILD-INSTALLER.bat` on a PC any more; both files stay for a hand build and the workflow runs them as they are. + +Two jobs: + +1. `parse` (required): every `.ps1` under the Windows folders through the Windows PowerShell 5.1 parser + (`tools/ci/windows/check-ps51.ps1`, run with `powershell.exe`, the PowerShell on the PCs; it refuses to pass unless + the fixture `tools/ci/windows/fixtures/bad-drive-ref.ps1.txt`, a `"$x: y"` drive-qualified reference, FAILS, so a + green run proves the check bites), PSScriptAnalyzer as warnings, and a parenthesis check of every `.bat` and `.cmd` + (`check-bat.ps1`: caret escapes, quotes and `for /f ('...')` strings ignored, depth never below zero, zero at the + end; its fixture `bad-bare-paren.bat.txt` must be flagged). Both scripts run on a PC too: + `powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1`. +2. `build`: the engine with `cargo build --release` on the MSVC target (so the Mac cross-build is no longer an input), + the window host with `app\windows\BUILD-APP.bat` as it is (MSVC from the runner's Visual Studio, WebView2 SDK from + NuGet, `host.rc` with the coin icon; `version.h` carries the host version), the payload with `make-payload.sh` in + Git Bash, the installer with `build-installer.ps1` (Inno Setup from the runner image or chocolatey, rcedit), then a + smoke run (`igneum-app.exe --version`, `Igneum Miner.exe --version` and `--help`, the version block of the host), + the launcher `proto-cuda/windows-app/start-igneum.ps1` with `DRY_RUN=1` (prints the node command and the GPU plan, + starts nothing) through the `.ps1` and through `START-IGNEUM.bat`, and three artifacts kept 90 days: + `igneum-windows-installer` (`Igneum-Miner-Setup-.exe`), `igneum-windows-payload` + (`igneum-windows-app.zip`), `igneum-windows-host` (`Igneum Miner.exe`). + +### The inputs the runner cannot build + +`igneumd.exe` and `igneum-miner.exe` come from the node fork in `vendor/` (not in git, 20 to 55 minutes to build) and +the prebuilt GPU workers need NVIDIA's NVRTC DLLs (90 MB, not in git). They travel as `payload-inputs.zip` on the +downloads host: + + packaging/windows/push-inputs.sh # on the Mac, after each node or worker cross-build + +collects `igneumd.exe`, `igneum-miner.exe`, the three mingw DLLs, `igneum-worker-cuda.exe`, `nvrtc*.dll`, the licence +texts, `igneum-worker-opencl.exe` and an `inputs.json` (sha256 and bytes of each, the commits, the date), zips them +into `dl//payload-inputs.zip` with `payload-inputs.sha256` and `payload-inputs.json` next to it in the +downloads folder (`~/.config/igneum/dlsite-dir` names it; `IGNEUM_DLSITE` overrides), and deploys the folder with the +Vercel CLI (`--no-deploy` to skip). The workflow downloads the three with the `DL_TOKEN` repository secret and checks +the sha256. The secret was set once from the Mac and never printed: + + tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum + +### The outputs, back to the downloads host + +The runner cannot deploy the downloads project (it is deployed by CLI from a folder outside the repo with the Igneum +Vercel login), so the Mac pulls the artifacts: + + packaging/windows/fetch-ci-artifacts.sh [--deploy] [run-id] + +downloads the installer and the payload zip from the latest green run on master (`gh run download`, as igneum-labs), +copies them into `dl//` next to the Mac-built packages, writes `igneum-windows-ci.json` (run URL, time), and +prints the deploy command, or deploys with `--deploy`. + +### What still needs a human + +A code-signing certificate. Until Igneum has one, the installer and the exes are unsigned and SmartScreen shows +"Windows protected your PC" (More info, Run anyway). Everything else in this folder runs without a PC. + +## On the Mac (hand build, kept for reference) packaging/windows/make-payload.sh [out.zip] @@ -17,7 +78,7 @@ worker sources for the fallback build (`proto-cuda\`, `proto-opencl\`), `igneum- token from `~/.config/igneum/dl-token`, log intake, live page), `stop-igneum.ps1`, and `app\windows\` (the window host sources and `BUILD-APP.bat`, since WebView2 cannot be linked from the Mac). -## On the PC +## On the PC (hand build, kept for reference; CI does all of this) 1. Extract the zip next to `packaging\windows` (or anywhere: `build-installer.ps1 -Payload `; without a folder it downloads `dl.igneum.network/igneum-windows-app.zip`). diff --git a/packaging/windows/fetch-ci-artifacts.sh b/packaging/windows/fetch-ci-artifacts.sh new file mode 100755 index 00000000..8570d37b --- /dev/null +++ b/packaging/windows/fetch-ci-artifacts.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Pulls the Windows installer and payload from the latest green run of .github/workflows/windows.yml on master and +# copies them into the downloads folder (dl//), where the other packages live. Run on the Mac: +# +# packaging/windows/fetch-ci-artifacts.sh [--deploy] [run-id] +# +# Without --deploy it prints the deploy command for the main session to run; with --deploy it deploys the folder with +# the Vercel CLI itself. A run id (gh run list) picks a specific run instead of the latest green one. +# Reads ~/.config/igneum/dl-token, ~/.config/igneum/dlsite-dir (IGNEUM_DLSITE overrides) and the gh login, which must +# be igneum-labs (gh auth switch --user igneum-labs). +set -euo pipefail +REPO="igneum-network/igneum" +DEPLOY=0 +RUN_ID="" +for a in "$@"; do + case "$a" in --deploy) DEPLOY=1 ;; *) RUN_ID="$a" ;; esac +done +TOKEN_FILE="$HOME/.config/igneum/dl-token" +DLSITE="${IGNEUM_DLSITE:-}" +[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true +[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE" >&2; exit 1; } +TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +DEST="$DLSITE/dl/$TOKEN" +[ -n "$DLSITE" ] && [ -d "$DEST" ] || { echo "no downloads folder: set IGNEUM_DLSITE or ~/.config/igneum/dlsite-dir (must hold dl//)" >&2; exit 1; } +gh auth status 2>&1 | grep -q 'Active account: true' || { echo "gh is not logged in" >&2; exit 1; } +gh auth status 2>&1 | grep -B1 'Active account: true' | grep -q 'igneum-labs' || { echo "gh active account is not igneum-labs: run gh auth switch --user igneum-labs" >&2; exit 1; } + +if [ -z "$RUN_ID" ]; then + RUN_ID="$(gh run list --repo "$REPO" --workflow windows.yml --branch master --status success --limit 1 --json databaseId --jq '.[0].databaseId')" + [ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ] || { echo "no green windows.yml run on master yet" >&2; exit 1; } +fi +gh run view "$RUN_ID" --repo "$REPO" --json headSha,displayTitle,updatedAt,url,conclusion --jq '"run \(.url)\n\(.displayTitle)\n\(.headSha[0:12]) \(.updatedAt) \(.conclusion)"' + +TMP="$(mktemp -d)" +gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-installer --name igneum-windows-payload --dir "$TMP" +SETUP="$(find "$TMP" -name 'Igneum-Miner-Setup-*.exe' | head -1)" +PAYLOAD="$(find "$TMP" -name 'igneum-windows-app.zip' | head -1)" +[ -n "$SETUP" ] && [ -n "$PAYLOAD" ] || { echo "the run has no installer or payload artifact" >&2; ls -R "$TMP"; exit 1; } +cp "$SETUP" "$DEST/" +cp "$PAYLOAD" "$DEST/igneum-windows-app.zip" +cat > "$DEST/igneum-windows-ci.json" <&1 | grep -v "$TOKEN" || true) + echo "live: https://dl.igneum.network/dl//$(basename "$SETUP")" +else + echo "deploy: cd $DLSITE && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes" +fi diff --git a/packaging/windows/make-payload.sh b/packaging/windows/make-payload.sh index 6e2668da..6b26ee93 100755 --- a/packaging/windows/make-payload.sh +++ b/packaging/windows/make-payload.sh @@ -14,12 +14,18 @@ # proto-cuda\, proto-opencl\ the worker sources and build.bat for that fallback # igneum-app.json the update manifest URL (token from ~/.config/igneum/dl-token, never in the repo), the log intake # stop-igneum.ps1 what the installer runs before an upgrade and on uninstall -# app\windows\ host.cpp, host.rc, BUILD-APP.bat (the window host is built on the PC) +# app\windows\ host.cpp, host.rc, version.h, BUILD-APP.bat (the window host is built on the PC or by CI) +# Igneum Miner.exe the window host, when app/windows/dist/ holds one (BUILD-APP.bat ran before this script) +# On GitHub (.github/workflows/windows.yml) the same script runs in Git Bash with IGNEUM_WIN_RELEASE and +# IGNEUM_WORKERS_DIR pointing at the unpacked payload-inputs.zip (push-inputs.sh) and IGNEUM_APP_EXE at the engine +# built on the runner; the mingw DLLs are taken from the release folder there, zip falls back to 7z. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" -VERSION="0.3.0" +VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "$ROOT/app/igneum-app/Cargo.toml" | head -1)" OUT="${1:-$HOME/Desktop/igneum-windows-app.zip}" +case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac +mkdir -p "$(dirname "$OUT")" REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}" ENGINE="${IGNEUM_APP_EXE:-$ROOT/app/igneum-app/target/x86_64-pc-windows-gnu/release/igneum-app.exe}" MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32 @@ -37,7 +43,8 @@ cp "$REL/igneumd.exe" "$STAGE/igneumd.exe" cp "$REL/igneum-miner.exe" "$STAGE/igneum-miner.exe" for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do name="$(basename "$dll")" - if [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true + if [ -f "$REL/$name" ]; then cp "$REL/$name" "$STAGE/" # push-inputs.sh ships them next to the exes (CI) + elif [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true else echo "note: $name not found in the mingw toolchain (the node exe is linked -static, so it may not need it)"; fi done @@ -63,9 +70,10 @@ else echo "note: no prebuilt igneum-worker-cuda.exe / igneum-worker-opencl.exe f # the worker sources for the fallback build cp "$ROOT/proto-cuda/host.cu" "$ROOT/proto-cuda/build.bat" "$ROOT/proto-cuda/README.md" "$STAGE/proto-cuda/" cp "$ROOT/proto-opencl/host.c" "$ROOT/proto-opencl/build.bat" "$ROOT/proto-opencl/README.md" "$STAGE/proto-opencl/" -# the window host sources, built on the PC -cp "$ROOT/app/windows/host.cpp" "$ROOT/app/windows/host.rc" "$ROOT/app/windows/BUILD-APP.bat" "$STAGE/app/windows/" +# the window host sources, built on the PC or by CI; the built host when BUILD-APP.bat already ran here +cp "$ROOT/app/windows/host.cpp" "$ROOT/app/windows/host.rc" "$ROOT/app/windows/version.h" "$ROOT/app/windows/BUILD-APP.bat" "$STAGE/app/windows/" mkdir -p "$STAGE/app/windows/art" && cp "$ROOT/brand/icons/igneum.ico" "$STAGE/app/windows/art/" +if [ -f "$ROOT/app/windows/dist/Igneum Miner.exe" ]; then cp "$ROOT/app/windows/dist/Igneum Miner.exe" "$STAGE/"; echo "window host: Igneum Miner.exe from app/windows/dist"; fi cp "$HERE/stop-igneum.ps1" "$STAGE/stop-igneum.ps1" # the packaged configuration: the download token stays out of the repo @@ -96,7 +104,10 @@ for f in "$STAGE"/*.bat "$STAGE/README.txt" "$STAGE/stop-igneum.ps1" "$STAGE/app [ -f "$f" ] && perl -pi -e 's/\r?\n/\r\n/' "$f" done rm -f "$OUT" -(cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store') +if command -v zip >/dev/null 2>&1; then (cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store') +elif command -v 7z >/dev/null 2>&1; then (cd "$HERE" && 7z a -tzip -bso0 -bsp0 "$OUT" "igneum-windows-app" '-xr!.DS_Store') +else echo "neither zip nor 7z is on PATH" >&2; exit 1; fi echo "staged $STAGE" ls -la "$OUT" -unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d' | head -40 +if command -v unzip >/dev/null 2>&1; then unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d' | head -40 +else (cd "$STAGE" && find . -type f | sort | head -40); fi diff --git a/packaging/windows/push-inputs.sh b/packaging/windows/push-inputs.sh new file mode 100755 index 00000000..27e49efb --- /dev/null +++ b/packaging/windows/push-inputs.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Publishes payload-inputs.zip: the pieces of the Windows app that the GitHub build (.github/workflows/windows.yml) +# cannot make on a hosted runner, because they come from the node fork (vendor/, not in git) or from NVIDIA's +# redistributables (large, not in git). Run it on the Mac after every node or worker cross-build: +# +# packaging/windows/push-inputs.sh [--no-deploy] +# +# What goes in (flat): igneumd.exe, igneum-miner.exe (vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/ +# release, IGNEUM_WIN_RELEASE overrides), the three mingw runtime DLLs, igneum-worker-cuda.exe with nvrtc64_*_0.dll, +# nvrtc-builtins64_*.dll and the licence texts (proto-cuda/nvrtc, fetch-redist.sh + build-windows.sh), +# igneum-worker-opencl.exe (proto-opencl), and inputs.json (sha256 and size of each file, the commits, the date). +# The zip, its .sha256 and the .json land in the downloads folder (dl//) and the folder is deployed with the +# Vercel CLI, exactly as the other packages are. The workflow fetches them with the DL_TOKEN repository secret and +# refuses a zip whose sha256 does not match. +# +# Where things are read from (never in the repo): the token in ~/.config/igneum/dl-token, the downloads folder in +# ~/.config/igneum/dlsite-dir (one line, the path of the dlsite directory; IGNEUM_DLSITE overrides), the Vercel login +# in ~/.config/igneum/vercel. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}" +MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32 +NVRTC_DIR="$ROOT/proto-cuda/nvrtc" +CL_WORKER="$ROOT/proto-opencl/igneum-worker-opencl.exe" +TOKEN_FILE="$HOME/.config/igneum/dl-token" +DLSITE="${IGNEUM_DLSITE:-}" +[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true +DEPLOY=1 +[ "${1:-}" = "--no-deploy" ] && DEPLOY=0 + +[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE (the downloads token)" >&2; exit 1; } +TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +[ -n "$DLSITE" ] && [ -d "$DLSITE/dl/$TOKEN" ] || { echo "no downloads folder: set IGNEUM_DLSITE or write the dlsite path to ~/.config/igneum/dlsite-dir (it must hold dl//)" >&2; exit 1; } +[ -f "$REL/igneumd.exe" ] || { echo "no $REL/igneumd.exe; cross-compile the node first (proto-cuda/windows-node/cross-build.sh)" >&2; exit 1; } +[ -f "$REL/igneum-miner.exe" ] || { echo "no $REL/igneum-miner.exe; cross-compile the miner first" >&2; exit 1; } + +STAGE="$(mktemp -d)/payload-inputs" +mkdir -p "$STAGE" +cp "$REL/igneumd.exe" "$REL/igneum-miner.exe" "$STAGE/" +for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do + name="$(basename "$dll")" + if [ -f "$MINGW/$dll" ]; then cp "$MINGW/$dll" "$STAGE/"; x86_64-w64-mingw32-strip "$STAGE/$name" 2>/dev/null || true + else echo "note: $name not in the mingw toolchain (the node exe is linked -static, so it may not need it)"; fi +done +if [ -f "$NVRTC_DIR/igneum-worker-cuda.exe" ]; then + cp "$NVRTC_DIR/igneum-worker-cuda.exe" "$STAGE/" + for f in "$NVRTC_DIR"/redist/bin/nvrtc*.dll "$NVRTC_DIR"/redist/LICENSE-*.txt "$NVRTC_DIR/THIRD-PARTY.md"; do [ -f "$f" ] && cp "$f" "$STAGE/"; done + ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh)" +else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-windows.sh); the app will build the CUDA worker on the PC"; fi +[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER" + +# the manifest: what is in the zip, from where, when +NODE_COMMIT="$(git -C "$ROOT/vendor/igneum-node-v4" rev-parse --short HEAD 2>/dev/null || git -C "$ROOT/vendor/igneum-node" rev-parse --short HEAD 2>/dev/null || echo unknown)" +REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" +{ + echo '{' + echo " \"built_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," + echo " \"node_source_commit\": \"$NODE_COMMIT\"," + echo " \"repo_commit\": \"$REPO_COMMIT\"," + echo ' "files": {' + first=1 + for f in "$STAGE"/*; do + name="$(basename "$f")" + sum="$(shasum -a 256 "$f" | cut -d' ' -f1)" + bytes="$(stat -f %z "$f")" + [ $first = 1 ] || echo ',' + first=0 + printf ' "%s": { "sha256": "%s", "bytes": %s }' "$name" "$sum" "$bytes" + done + echo + echo ' }' + echo '}' +} > "$STAGE/inputs.json" + +DEST="$DLSITE/dl/$TOKEN" +OUT="$DEST/payload-inputs.zip" +rm -f "$OUT" +(cd "$(dirname "$STAGE")" && zip -qr "$OUT" "payload-inputs" -x '*.DS_Store') +shasum -a 256 "$OUT" | awk '{print $1}' > "$DEST/payload-inputs.sha256" +cp "$STAGE/inputs.json" "$DEST/payload-inputs.json" +echo "payload-inputs.zip: $(stat -f %z "$OUT") bytes, sha256 $(cat "$DEST/payload-inputs.sha256")" +cat "$DEST/payload-inputs.json" +rm -rf "$(dirname "$STAGE")" + +if [ "$DEPLOY" = 1 ]; then + echo "deploying $DLSITE" + (cd "$DLSITE" && npx vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | grep -v "$TOKEN" || true) + code="$(curl -s -o /dev/null -w '%{http_code}' "https://dl.igneum.network/dl/$TOKEN/payload-inputs.json")" + echo "https://dl.igneum.network/dl//payload-inputs.json -> HTTP $code" + [ "$code" = 200 ] || { echo "the manifest is not reachable yet; check the deploy output" >&2; exit 1; } +else + echo "not deployed (--no-deploy): cd $DLSITE && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes" +fi +echo "Next: the GitHub build picks it up on the next push to master (or: gh workflow run windows.yml --repo igneum-network/igneum)." diff --git a/proto-cuda/windows-app/start-igneum.ps1 b/proto-cuda/windows-app/start-igneum.ps1 index 90c9778f..a6472af0 100644 --- a/proto-cuda/windows-app/start-igneum.ps1 +++ b/proto-cuda/windows-app/start-igneum.ps1 @@ -23,6 +23,17 @@ Set-NodeTarget $rpcHost $rpcPort # the miners always dial the node on this PC Log "igneum launcher $packageVersion ($chainVersion) on $machine (run $stamp): node $rpcListen, p2p $p2pListen, peers $($peerNodes -join ' '), appdir $appDir, unsynced mining $(if ($unsyncedMining) { 'on' } else { 'off' }), identities $minersPerVendor per vendor" Log "If Windows Firewall asks about igneumd.exe: tick Private networks and click Allow access (the Mac node dials this PC on port $p2pPort). If you clicked Cancel once, run ALLOW-FIREWALL.bat." +# DRY_RUN=1 (CI, .github/workflows/windows.yml; or a quick check on a PC): print the plan and start nothing. +if ($env:DRY_RUN -eq '1') { + Log 'DRY_RUN=1: printing the plan, starting nothing' + Log "node command: `"$nodeExe`" $((Get-NodeArgs) -join ' ')" + Log "node binary present: $(Test-Path $nodeExe); miner binary present: $(Test-Path $miner)" + $plan = Find-Hardware + Log "miners to start: $(if ($plan.Count) { $plan -join ', ' } else { 'none' }) ($minersPerVendor identities per vendor, payout $(if ($payoutEvm) { $payoutEvm } else { 'derived from the PC name' }))" + Log 'dry run done' + exit 0 +} + # ---- 1. the node ---------------------------------------------------------------------------------------------------------- $script:node.on = $true diff --git a/tools/ci/windows/check-bat.ps1 b/tools/ci/windows/check-bat.ps1 new file mode 100644 index 00000000..bdabb332 --- /dev/null +++ b/tools/ci/windows/check-bat.ps1 @@ -0,0 +1,69 @@ +# Static check of every .bat and .cmd under the given folders for the parenthesis bug class seen on 3 October 2026: +# a bare ")" that closes nothing, or an "if (...) (" block left open. cmd.exe has no dry-parse mode, so this mirrors +# what its parser does with parentheses: caret escapes (^( and ^)) are not parentheses, nothing inside double quotes +# or a for /f ('...') command string counts, rem and :: lines are comments. The depth is tracked across lines; it must +# never go below zero and must be zero at the end of the file. Prints file:line:message and exits 1 on any finding. +# +# A built-in negative test checks fixtures\bad-bare-paren.bat.txt first and refuses to pass unless it is flagged. +# +# powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-bat.ps1 [-Root ] +param( + [string]$Root = '', + [string[]]$Folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node', + 'proving/windows-wsl2', 'relay/clients', 'packaging/windows', 'app/windows', + 'proto-cuda', 'proto-opencl') +) +$ErrorActionPreference = 'Stop' +if (-not $Root) { $Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path } + +function Check-Batch([string]$path) { + $findings = @() + $depth = 0 + $n = 0 + foreach ($raw in [IO.File]::ReadAllLines($path)) { + $n += 1 + $line = $raw.TrimEnd("`r") + if ($line -match '^\s*(@?rem\b|::)') { continue } + $s = $line -replace '\^.', '' # caret escapes: ^( ^) ^> ^< ^& ^| + $s = $s -replace '"[^"]*"', '' # double-quoted strings + $s = $s -replace "'[^']*'", '' # for /f ('command') strings + foreach ($ch in $s.ToCharArray()) { + if ($ch -eq '(') { $depth += 1 } + elseif ($ch -eq ')') { + $depth -= 1 + if ($depth -lt 0) { $findings += @{ line = $n; msg = "')' closes nothing (bare parenthesis)" }; $depth = 0 } + } + } + } + if ($depth -gt 0) { $findings += @{ line = $n; msg = "$depth unclosed '(' at the end of the file" } } + return ,$findings +} + +# ---- 1. the negative test ---- +$fixture = Join-Path $PSScriptRoot 'fixtures\bad-bare-paren.bat.txt' +$fx = Check-Batch $fixture +if ($fx.Count -eq 0) { Write-Host "::error::self-test failed: $fixture was not flagged"; exit 2 } +Write-Host ("self-test: fixture bad-bare-paren.bat.txt is flagged as expected at line {0}: {1}" -f $fx[0].line, $fx[0].msg) + +# ---- 2. every .bat and .cmd ---- +$files = @() +foreach ($f in $Folders) { + $dir = Join-Path $Root $f + if (-not (Test-Path $dir)) { Write-Host "note: no folder $f"; continue } + $files += Get-ChildItem -Path $dir -Recurse -Include '*.bat', '*.cmd' -File | Where-Object { $_.FullName -notmatch '\\igneum-windows-app\\|\\build\\|\\dist\\|\\packs\\|\\emu\\|\\nvrtc\\redist\\' } +} +$files = $files | Sort-Object FullName -Unique +$bad = 0 +foreach ($file in $files) { + $rel = $file.FullName.Substring($Root.Length).TrimStart('\', '/') + $found = Check-Batch $file.FullName + if ($found.Count -eq 0) { Write-Host "ok $rel"; continue } + foreach ($x in $found) { + $bad += 1 + Write-Host "FAIL ${rel}:$($x.line): $($x.msg)" + Write-Host "::error file=$($rel -replace '\\', '/'),line=$($x.line)::$($x.msg)" + } +} +Write-Host ("{0} batch files checked, {1} findings" -f $files.Count, $bad) +if ($bad -gt 0) { exit 1 } +exit 0 diff --git a/tools/ci/windows/check-ps51.ps1 b/tools/ci/windows/check-ps51.ps1 new file mode 100644 index 00000000..86d0bc14 --- /dev/null +++ b/tools/ci/windows/check-ps51.ps1 @@ -0,0 +1,68 @@ +# Parses every .ps1 under the given folders with the PowerShell parser of the PowerShell that runs this script and +# fails on any parse error, printed as file:line:message. Run it under Windows PowerShell 5.1 (powershell.exe), the +# PowerShell on the project lead's PCs: 5.1 rejects "$name: text" (a drive-qualified variable reference) where newer parsers may +# not, and that class caused the two launcher failures of 4 October 2026 (commits 82275e6 and 018f739). +# +# A built-in negative test parses fixtures\bad-drive-ref.ps1.txt first and refuses to pass unless that file FAILS with +# the 5.1 message, so a green run proves the check bites. +# +# powershell -NoProfile -ExecutionPolicy Bypass -File tools\ci\windows\check-ps51.ps1 [-Root ] [-AnyVersion] +param( + [string]$Root = '', + [string[]]$Folders = @('proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node', + 'proving/windows-wsl2', 'relay/clients', 'packaging/windows', 'app/windows', 'tools/ci/windows'), + [switch]$AnyVersion +) +$ErrorActionPreference = 'Stop' +if (-not $Root) { $Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path } +$v = $PSVersionTable.PSVersion +Write-Host "PowerShell $v ($($PSVersionTable.PSEdition))" +if ($v.Major -ne 5 -and -not $AnyVersion) { + Write-Host "::error::this check must run under Windows PowerShell 5.1 (powershell.exe), not PowerShell $v; pass -AnyVersion to override" + exit 2 +} + +function Parse-File([string]$path) { + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors) + return ,@($errors) +} + +# ---- 1. the negative test: the fixture with "$x: y" must fail to parse ---- +$fixture = Join-Path $PSScriptRoot 'fixtures\bad-drive-ref.ps1.txt' +$fixtureErrors = Parse-File $fixture +if ($fixtureErrors.Count -eq 0) { + Write-Host "::error::self-test failed: $fixture parsed without errors, so this parser does not catch the 5.1 drive-qualified variable bug" + exit 2 +} +$msg = $fixtureErrors[0].Message +Write-Host ("self-test: fixture bad-drive-ref.ps1.txt fails as expected at line {0}: {1}" -f $fixtureErrors[0].Extent.StartLineNumber, $msg) +if ($msg -notmatch 'Variable reference is not valid') { + Write-Host "::error::self-test: the fixture failed, but not with the 'Variable reference is not valid' message (got: $msg)" + exit 2 +} + +# ---- 2. every .ps1 under the folders ---- +$files = @() +foreach ($f in $Folders) { + $dir = Join-Path $Root $f + if (-not (Test-Path $dir)) { Write-Host "note: no folder $f"; continue } + $files += Get-ChildItem -Path $dir -Recurse -Filter '*.ps1' -File | Where-Object { $_.FullName -notmatch '\\igneum-windows-app\\|\\build\\|\\dist\\' } +} +$bad = 0 +foreach ($file in ($files | Sort-Object FullName)) { + $rel = $file.FullName.Substring($Root.Length).TrimStart('\', '/') + $errs = Parse-File $file.FullName + if ($errs.Count -eq 0) { Write-Host "ok $rel"; continue } + foreach ($e in $errs) { + $bad += 1 + $line = $e.Extent.StartLineNumber + $col = $e.Extent.StartColumnNumber + Write-Host "FAIL ${rel}:${line}:${col}: $($e.Message)" + Write-Host "::error file=$($rel -replace '\\', '/'),line=$line,col=${col}::$($e.Message)" + } +} +Write-Host ("{0} files parsed, {1} parse errors" -f $files.Count, $bad) +if ($bad -gt 0) { exit 1 } +exit 0 diff --git a/tools/ci/windows/fixtures/bad-bare-paren.bat.txt b/tools/ci/windows/fixtures/bad-bare-paren.bat.txt new file mode 100644 index 00000000..8ffd99a6 --- /dev/null +++ b/tools/ci/windows/fixtures/bad-bare-paren.bat.txt @@ -0,0 +1,10 @@ +@echo off +rem Negative fixture for check-bat.ps1 (named .txt so nothing runs it). The bare ")" on line 9 closes nothing: +rem cmd.exe reports ") was unexpected at this time." and stops. Seen on 3 October 2026 in a launcher bat. +if not exist "%~dp0igneumd.exe" ( + echo igneumd.exe is missing ^(extract the whole zip^). + exit /b 1 +) +echo ready +) +echo never reached diff --git a/tools/ci/windows/fixtures/bad-drive-ref.ps1.txt b/tools/ci/windows/fixtures/bad-drive-ref.ps1.txt new file mode 100644 index 00000000..b23da2f1 --- /dev/null +++ b/tools/ci/windows/fixtures/bad-drive-ref.ps1.txt @@ -0,0 +1,7 @@ +# Negative fixture for check-ps51.ps1 (named .txt so no PowerShell glob picks it up). Windows PowerShell 5.1 must +# refuse this file: "$machine: text" is read as a drive-qualified variable reference, the bug class of commits +# 82275e6 and 018f739 on 4 October 2026. The fix in real code is "${machine}: text". +$machine = $env:COMPUTERNAME +$vendorName = 'nvidia' +$label = "igneum-evm-payout-v0:$machine:$vendorName" +Write-Host "$label: derived"