Krolyxon
Commit 815d036

Branches

Tags

Refs →

bench51815d036

add release workflow and install scripts

Tagging vX.Y.Z builds Linux (x86_64, aarch64), Windows and macOS (arm64,
x86_64) archives and publishes them with SHA256SUMS and the CHANGELOG
section as notes, then checks the one-line installers against the new
release on every OS. PRs touching this get a dry run without publishing.

install.sh (Linux, macOS) and install.ps1 (Windows) download the right
archive, verify its checksum, install both binaries, and add a menu
entry; both can install from a local archive and uninstall.
Author
krolyxon
Date
2026-11-01 00:05 +0530
Parent
3ec5af1
Commit
815d036f1feb3a6b98fe78756b4ad5b73e495f7d

+457 −0

3 files changed

  1. A .github/workflows/release.yml +178 −0
  2. A install.ps1 +112 −0
  3. A install.sh +167 −0
A .github/workflows/release.yml View
@@ -0,0 +1,178 @@
1+name: Release
2+
3+# Pushing a tag vX.Y.Z (matching the version in Cargo.toml) builds bench51
4+# for every target, publishes a GitHub release with the archives, checksums
5+# and the CHANGELOG.md section as notes, then installs it with the one-line
6+# installers on each OS. Pull requests that touch the release machinery get
7+# a dry run: everything except publishing.
8+
9+on:
10+ push:
11+ tags: ['v*']
12+ pull_request:
13+ paths:
14+ - .github/workflows/release.yml
15+ - install.sh
16+ - install.ps1
17+ workflow_dispatch:
18+
19+permissions:
20+ contents: read
21+
22+env:
23+ CARGO_TERM_COLOR: always
24+
25+jobs:
26+ build:
27+ name: ${{ matrix.target }}
28+ strategy:
29+ fail-fast: false
30+ matrix:
31+ include:
32+ # 22.04 keeps the glibc requirement low for older distributions.
33+ - { target: x86_64-unknown-linux-gnu, os: ubuntu-22.04 }
34+ - { target: aarch64-unknown-linux-gnu, os: ubuntu-22.04-arm }
35+ - { target: x86_64-pc-windows-msvc, os: windows-latest }
36+ - { target: aarch64-apple-darwin, os: macos-latest }
37+ # Cross-compiled on Apple silicon, so it can't be run here.
38+ - { target: x86_64-apple-darwin, os: macos-latest, cross: true }
39+ runs-on: ${{ matrix.os }}
40+ steps:
41+ - uses: actions/checkout@v7
42+
43+ - name: Check the tag matches Cargo.toml
44+ if: startsWith(github.ref, 'refs/tags/v')
45+ shell: bash
46+ run: |
47+ version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)
48+ if [ "$GITHUB_REF_NAME" != "v$version" ]; then
49+ echo "::error::tag $GITHUB_REF_NAME does not match version $version in Cargo.toml"
50+ exit 1
51+ fi
52+
53+ - uses: dtolnay/rust-toolchain@stable
54+ with:
55+ targets: ${{ matrix.target }}
56+
57+ - name: Install Linux GUI dependencies
58+ if: runner.os == 'Linux'
59+ run: |
60+ sudo apt-get update
61+ sudo apt-get install -y libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
62+
63+ - uses: Swatinem/rust-cache@v2
64+ with:
65+ key: ${{ matrix.target }}
66+
67+ - name: Build
68+ run: cargo build --release --locked --target ${{ matrix.target }} -p bench51 -p bench51-gui
69+
70+ - name: Package
71+ shell: bash
72+ run: |
73+ name=bench51-${{ matrix.target }}
74+ ext=
75+ if [ "$RUNNER_OS" = Windows ]; then ext=.exe; fi
76+ mkdir -p "dist/$name"
77+ cp "target/${{ matrix.target }}/release/bench51$ext" "target/${{ matrix.target }}/release/bench51-gui$ext" "dist/$name/"
78+ cp README.md LICENSE CHANGELOG.md "dist/$name/"
79+ if [ "$RUNNER_OS" = Linux ]; then
80+ mkdir -p "dist/$name/share/applications" "dist/$name/share/icons/hicolor/scalable/apps"
81+ cp crates/bench51-gui/assets/bench51-gui.desktop "dist/$name/share/applications/"
82+ cp crates/bench51-gui/assets/bench51-gui.svg "dist/$name/share/icons/hicolor/scalable/apps/"
83+ fi
84+ cd dist
85+ if [ "$RUNNER_OS" = Windows ]; then
86+ 7z a -tzip "$name.zip" "$name" > /dev/null
87+ else
88+ tar -czf "$name.tar.gz" "$name"
89+ fi
90+ rm -rf "$name"
91+ ls -l
92+
93+ - name: Install the archive and run it
94+ if: runner.os != 'Windows' && !matrix.cross
95+ shell: bash
96+ run: |
97+ sh install.sh --archive "dist/bench51-${{ matrix.target }}.tar.gz" --dir "$RUNNER_TEMP/bin" --no-desktop
98+ "$RUNNER_TEMP/bin/bench51-gui" --version
99+ printf 'ORG 0\nloop: MOV A,#1\n SJMP loop\n END\n' > "$RUNNER_TEMP/t.asm"
100+ "$RUNNER_TEMP/bin/bench51" --assemble "$RUNNER_TEMP/t.asm"
101+ test -s "$RUNNER_TEMP/t.hex"
102+
103+ - name: Install the archive and run it
104+ if: runner.os == 'Windows'
105+ shell: pwsh
106+ run: |
107+ ./install.ps1 -Archive "dist/bench51-${{ matrix.target }}.zip" -InstallDir "$env:RUNNER_TEMP\bench51" -NoShortcut
108+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
109+ # The GUI has no console; check its exit code instead of its output.
110+ $gui = Start-Process -FilePath "$env:RUNNER_TEMP\bench51\bench51-gui.exe" -ArgumentList '--version' -Wait -PassThru
111+ if ($gui.ExitCode -ne 0) { exit 1 }
112+
113+ - uses: actions/upload-artifact@v7
114+ with:
115+ name: bench51-${{ matrix.target }}
116+ path: dist/*
117+ if-no-files-found: error
118+
119+ publish:
120+ needs: build
121+ if: startsWith(github.ref, 'refs/tags/v')
122+ runs-on: ubuntu-latest
123+ permissions:
124+ contents: write
125+ steps:
126+ - uses: actions/checkout@v7
127+
128+ - uses: actions/download-artifact@v8
129+ with:
130+ path: dist
131+ merge-multiple: true
132+
133+ - name: Checksums
134+ run: |
135+ cd dist
136+ sha256sum -- * > SHA256SUMS
137+ cat SHA256SUMS
138+
139+ - name: Release notes from CHANGELOG.md
140+ run: |
141+ version=${GITHUB_REF_NAME#v}
142+ awk -v v="$version" '/^## \[/ { if (on) exit; on = index($0, "[" v "]") } on' CHANGELOG.md | tail -n +2 > notes.md
143+ if [ ! -s notes.md ]; then
144+ echo "::error::CHANGELOG.md has no section for $version"
145+ exit 1
146+ fi
147+ cat notes.md
148+
149+ - name: Publish
150+ env:
151+ GH_TOKEN: ${{ github.token }}
152+ run: gh release create "$GITHUB_REF_NAME" dist/* --verify-tag --title "bench51 $GITHUB_REF_NAME" --notes-file notes.md
153+
154+ # Installs the published release exactly as the README tells people to.
155+ verify:
156+ name: install on ${{ matrix.os }}
157+ needs: publish
158+ strategy:
159+ fail-fast: false
160+ matrix:
161+ os: [ubuntu-latest, macos-latest, windows-latest]
162+ runs-on: ${{ matrix.os }}
163+ steps:
164+ - name: One-line install
165+ if: runner.os != 'Windows'
166+ run: |
167+ curl -fsSL "https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$GITHUB_REF_NAME/install.sh" | sh
168+ "$HOME/.local/bin/bench51" --version | grep -F "${GITHUB_REF_NAME#v}"
169+ "$HOME/.local/bin/bench51-gui" --version | grep -F "${GITHUB_REF_NAME#v}"
170+
171+ - name: One-line install
172+ if: runner.os == 'Windows'
173+ shell: pwsh
174+ run: |
175+ irm "https://raw.githubusercontent.com/$env:GITHUB_REPOSITORY/$env:GITHUB_REF_NAME/install.ps1" | iex
176+ $version = & "$env:LOCALAPPDATA\Programs\bench51\bench51.exe" --version
177+ if ($version -notmatch [regex]::Escape($env:GITHUB_REF_NAME.TrimStart('v'))) { exit 1 }
178+ if (-not (Test-Path "$([Environment]::GetFolderPath('Programs'))\bench51.lnk")) { exit 1 }
A install.ps1 View
@@ -0,0 +1,112 @@
1+# Installs bench51 (the bench51-gui desktop app and the bench51 terminal app)
2+# from the GitHub releases, on Windows.
3+#
4+# irm https://raw.githubusercontent.com/krolyxon/bench51/master/install.ps1 | iex
5+#
6+# With options:
7+#
8+# & ([scriptblock]::Create((irm https://raw.githubusercontent.com/krolyxon/bench51/master/install.ps1))) -Version v0.2.0
9+#
10+# Options: -Version <tag> (default latest), -InstallDir <path> (default
11+# %LOCALAPPDATA%\Programs\bench51), -Archive <bench51-<target>.zip>,
12+# -NoShortcut, -Uninstall. BENCH51_VERSION and BENCH51_INSTALL_DIR set the
13+# defaults for -Version and -InstallDir when piping into iex.
14+
15+param(
16+ [string]$Version = $(if ($env:BENCH51_VERSION) { $env:BENCH51_VERSION } else { 'latest' }),
17+ [string]$InstallDir = $(if ($env:BENCH51_INSTALL_DIR) { $env:BENCH51_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'Programs\bench51' }),
18+ [string]$Archive,
19+ [switch]$NoShortcut,
20+ [switch]$Uninstall
21+)
22+
23+$ErrorActionPreference = 'Stop'
24+# Invoke-WebRequest is much slower with the progress bar on.
25+$ProgressPreference = 'SilentlyContinue'
26+[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
27+
28+$Repo = 'krolyxon/bench51'
29+$Asset = 'bench51-x86_64-pc-windows-msvc.zip'
30+$Shortcut = Join-Path ([Environment]::GetFolderPath('Programs')) 'bench51.lnk'
31+
32+function Write-Step([string]$Message) {
33+ Write-Host "bench51: $Message"
34+}
35+
36+# Adds or removes $Dir in the user's PATH.
37+function Edit-UserPath([string]$Dir, [bool]$Add) {
38+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
39+ $parts = @(($current -split ';') | Where-Object { $_ -and ($_.TrimEnd('\') -ne $Dir.TrimEnd('\')) })
40+ if ($Add) {
41+ $parts += $Dir
42+ }
43+ [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User')
44+}
45+
46+if ($Uninstall) {
47+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $InstallDir
48+ Remove-Item -Force -ErrorAction SilentlyContinue $Shortcut
49+ Edit-UserPath $InstallDir $false
50+ Write-Step "removed bench51 from $InstallDir"
51+ return
52+}
53+
54+$Temp = Join-Path ([IO.Path]::GetTempPath()) ("bench51-" + [guid]::NewGuid())
55+New-Item -ItemType Directory -Path $Temp | Out-Null
56+try {
57+ if (-not $Archive) {
58+ if ($Version -eq 'latest') {
59+ $Base = "https://github.com/$Repo/releases/latest/download"
60+ } else {
61+ if (-not $Version.StartsWith('v')) {
62+ $Version = "v$Version"
63+ }
64+ $Base = "https://github.com/$Repo/releases/download/$Version"
65+ }
66+ Write-Step "downloading $Asset ($Version)"
67+ $Archive = Join-Path $Temp $Asset
68+ $Sums = Join-Path $Temp 'SHA256SUMS'
69+ Invoke-WebRequest -UseBasicParsing -Uri "$Base/$Asset" -OutFile $Archive
70+ Invoke-WebRequest -UseBasicParsing -Uri "$Base/SHA256SUMS" -OutFile $Sums
71+ $line = Get-Content $Sums | Where-Object { $_ -match ('\s\*?' + [regex]::Escape($Asset) + '$') } | Select-Object -First 1
72+ if (-not $line) {
73+ throw "$Asset is not listed in SHA256SUMS"
74+ }
75+ $expected = ($line -split '\s+')[0].ToLowerInvariant()
76+ $actual = (Get-FileHash -Algorithm SHA256 -Path $Archive).Hash.ToLowerInvariant()
77+ if ($actual -ne $expected) {
78+ throw "checksum mismatch for $Asset"
79+ }
80+ }
81+ if (-not (Test-Path $Archive)) {
82+ throw "no such archive: $Archive"
83+ }
84+
85+ $Extracted = Join-Path $Temp 'x'
86+ Expand-Archive -Path $Archive -DestinationPath $Extracted -Force
87+ $gui = Get-ChildItem -Path $Extracted -Recurse -Filter 'bench51-gui.exe' | Select-Object -First 1
88+ if (-not $gui) {
89+ throw 'the archive has no bench51-gui.exe'
90+ }
91+
92+ New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
93+ Copy-Item -Path (Join-Path $gui.DirectoryName '*') -Destination $InstallDir -Recurse -Force
94+ Edit-UserPath $InstallDir $true
95+ $env:Path = "$env:Path;$InstallDir"
96+ Write-Step "installed bench51 and bench51-gui to $InstallDir"
97+
98+ if (-not $NoShortcut) {
99+ $shell = New-Object -ComObject WScript.Shell
100+ $link = $shell.CreateShortcut($Shortcut)
101+ $link.TargetPath = Join-Path $InstallDir 'bench51-gui.exe'
102+ $link.WorkingDirectory = $env:USERPROFILE
103+ $link.Description = 'Intel 8051 assembler, simulator and debugger'
104+ $link.Save()
105+ Write-Step 'added bench51 to the Start menu'
106+ }
107+
108+ & (Join-Path $InstallDir 'bench51.exe') --version
109+ Write-Step 'open a new terminal to use bench51 and bench51-gui from PATH'
110+} finally {
111+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $Temp
112+}
A install.sh View
@@ -0,0 +1,167 @@
1+#!/bin/sh
2+# Installs bench51 (the bench51-gui desktop app and the bench51 terminal app)
3+# from the GitHub releases, on Linux and macOS.
4+#
5+# curl -fsSL https://raw.githubusercontent.com/krolyxon/bench51/master/install.sh | sh
6+# curl -fsSL https://raw.githubusercontent.com/krolyxon/bench51/master/install.sh | sh -s -- --dir /usr/local/bin
7+#
8+# Run with --help for all options. On Windows, use install.ps1.
9+
10+set -eu
11+
12+REPO=krolyxon/bench51
13+VERSION=${BENCH51_VERSION:-latest}
14+DIR=${BENCH51_INSTALL_DIR:-$HOME/.local/bin}
15+ARCHIVE=
16+DESKTOP=1
17+UNINSTALL=0
18+
19+DATA=${XDG_DATA_HOME:-$HOME/.local/share}
20+APPS=$DATA/applications
21+ICONS=$DATA/icons/hicolor/scalable/apps
22+
23+usage() {
24+ cat <<EOF
25+Install bench51 from https://github.com/$REPO/releases
26+
27+Usage: install.sh [options]
28+
29+ --version <tag> release to install, e.g. v0.2.0 (default: latest)
30+ --dir <path> where to put the binaries (default: ~/.local/bin)
31+ --archive <file> install from a downloaded bench51-<target>.tar.gz
32+ --no-desktop on Linux, skip the application menu entry and icon
33+ --uninstall remove bench51 from --dir and the menu
34+ -h, --help show this help
35+
36+The environment variables BENCH51_VERSION and BENCH51_INSTALL_DIR set the
37+defaults for --version and --dir.
38+EOF
39+}
40+
41+say() { printf 'bench51: %s\n' "$*"; }
42+die() {
43+ printf 'bench51: error: %s\n' "$*" >&2
44+ exit 1
45+}
46+
47+while [ $# -gt 0 ]; do
48+ case $1 in
49+ --version) [ $# -ge 2 ] || die "--version needs a value"; VERSION=$2; shift 2 ;;
50+ --version=*) VERSION=${1#*=}; shift ;;
51+ --dir) [ $# -ge 2 ] || die "--dir needs a value"; DIR=$2; shift 2 ;;
52+ --dir=*) DIR=${1#*=}; shift ;;
53+ --archive) [ $# -ge 2 ] || die "--archive needs a value"; ARCHIVE=$2; shift 2 ;;
54+ --archive=*) ARCHIVE=${1#*=}; shift ;;
55+ --no-desktop) DESKTOP=0; shift ;;
56+ --uninstall) UNINSTALL=1; shift ;;
57+ -h | --help) usage; exit 0 ;;
58+ *) die "unknown option: $1 (see --help)" ;;
59+ esac
60+done
61+
62+refresh_menu() {
63+ if command -v update-desktop-database >/dev/null 2>&1; then
64+ update-desktop-database -q "$APPS" 2>/dev/null || true
65+ fi
66+}
67+
68+if [ "$UNINSTALL" = 1 ]; then
69+ rm -f "$DIR/bench51" "$DIR/bench51-gui" "$APPS/bench51-gui.desktop" "$ICONS/bench51-gui.svg"
70+ refresh_menu
71+ say "removed bench51 from $DIR"
72+ exit 0
73+fi
74+
75+# ---- Which build fits this machine ----
76+
77+os=$(uname -s)
78+arch=$(uname -m)
79+case $os in
80+Linux) os=unknown-linux-gnu ;;
81+Darwin) os=apple-darwin ;;
82+*) die "no prebuilt binaries for $os; on Windows use install.ps1" ;;
83+esac
84+case $arch in
85+x86_64 | amd64) arch=x86_64 ;;
86+aarch64 | arm64) arch=aarch64 ;;
87+*) die "no prebuilt binaries for $arch; build from source with: cargo install --git https://github.com/$REPO bench51-gui" ;;
88+esac
89+TARGET=$arch-$os
90+NAME=bench51-$TARGET.tar.gz
91+
92+download() {
93+ if command -v curl >/dev/null 2>&1; then
94+ curl --proto '=https' --tlsv1.2 -fsSL -o "$2" "$1"
95+ elif command -v wget >/dev/null 2>&1; then
96+ wget -q --https-only -O "$2" "$1"
97+ else
98+ die "neither curl nor wget is installed"
99+ fi
100+}
101+
102+sha256() {
103+ if command -v sha256sum >/dev/null 2>&1; then
104+ sha256sum "$1" | cut -d ' ' -f 1
105+ elif command -v shasum >/dev/null 2>&1; then
106+ shasum -a 256 "$1" | cut -d ' ' -f 1
107+ else
108+ die "neither sha256sum nor shasum is installed to verify the download"
109+ fi
110+}
111+
112+TMP=$(mktemp -d 2>/dev/null || mktemp -d -t bench51)
113+trap 'rm -rf "$TMP"' EXIT
114+trap 'exit 130' INT TERM
115+
116+# ---- Download and verify ----
117+
118+if [ -z "$ARCHIVE" ]; then
119+ if [ "$VERSION" = latest ]; then
120+ BASE=https://github.com/$REPO/releases/latest/download
121+ else
122+ case $VERSION in v*) ;; *) VERSION=v$VERSION ;; esac
123+ BASE=https://github.com/$REPO/releases/download/$VERSION
124+ fi
125+ say "downloading $NAME ($VERSION)"
126+ download "$BASE/$NAME" "$TMP/$NAME" || die "could not download $BASE/$NAME"
127+ download "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "could not download the checksums"
128+ expected=$(grep " [* ]*$NAME\$" "$TMP/SHA256SUMS" | cut -d ' ' -f 1)
129+ [ -n "$expected" ] || die "$NAME is not listed in SHA256SUMS"
130+ [ "$(sha256 "$TMP/$NAME")" = "$expected" ] || die "checksum mismatch for $NAME"
131+ ARCHIVE=$TMP/$NAME
132+fi
133+[ -f "$ARCHIVE" ] || die "no such archive: $ARCHIVE"
134+
135+mkdir "$TMP/x"
136+tar -xzf "$ARCHIVE" -C "$TMP/x"
137+SRC=$(find "$TMP/x" -type f -name bench51-gui | head -n 1)
138+[ -n "$SRC" ] || die "the archive has no bench51-gui"
139+SRC=$(dirname "$SRC")
140+[ -f "$SRC/bench51" ] || die "the archive has no bench51"
141+
142+# ---- Install ----
143+
144+mkdir -p "$DIR"
145+for bin in bench51 bench51-gui; do
146+ cp "$SRC/$bin" "$DIR/$bin.tmp"
147+ chmod 755 "$DIR/$bin.tmp"
148+ mv -f "$DIR/$bin.tmp" "$DIR/$bin"
149+done
150+say "installed bench51 and bench51-gui to $DIR"
151+
152+if [ "$DESKTOP" = 1 ] && [ "$(uname -s)" = Linux ] && [ -f "$SRC/share/applications/bench51-gui.desktop" ]; then
153+ mkdir -p "$APPS" "$ICONS"
154+ # Launchers don't always see ~/.local/bin on PATH: point at the binary.
155+ exec_path=$(printf '%s' "$DIR/bench51-gui" | sed 's/[\\&|]/\\&/g')
156+ sed "s|^Exec=bench51-gui|Exec=\"$exec_path\"|" \
157+ "$SRC/share/applications/bench51-gui.desktop" >"$APPS/bench51-gui.desktop"
158+ cp "$SRC/share/icons/hicolor/scalable/apps/bench51-gui.svg" "$ICONS/bench51-gui.svg"
159+ refresh_menu
160+ say "added bench51 to the application menu"
161+fi
162+
163+"$DIR/bench51" --version
164+case ":$PATH:" in
165+*":$DIR:"*) say "run bench51-gui for the desktop app, or bench51 in a terminal" ;;
166+*) say "$DIR is not on your PATH; add it, e.g. in ~/.profile: export PATH=\"$DIR:\$PATH\"" ;;
167+esac