diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b53fb22..583e9da 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,155 +1,251 @@
name: Release
on:
- push:
- tags: ['v*']
+ pull_request:
+ workflow_dispatch:
permissions:
- contents: write
- id-token: write
- attestations: write
+ contents: read
jobs:
- build:
- name: Package ${{ matrix.target }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: ubuntu-24.04
- target: x86_64-unknown-linux-gnu
- archive: tar
- - os: macos-14
- target: aarch64-apple-darwin
- archive: tar
- - os: windows-2025
- target: x86_64-pc-windows-msvc
- archive: zip
- runs-on: ${{ matrix.os }}
+ metadata:
+ name: Validate release context
+ runs-on: ubuntu-24.04
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ tag: ${{ steps.version.outputs.tag }}
+ steps:
+ - name: Require main for a stable release
+ if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main'
+ run: |
+ echo "Stable releases must be manually dispatched from main." >&2
+ exit 1
+ - uses: actions/checkout@v4
+ - id: version
+ name: Read package version
+ shell: bash
+ run: |
+ version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)"
+ test -n "$version"
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+ echo "tag=v$version" >> "$GITHUB_OUTPUT"
+
+ linux:
+ name: Package Linux x86-64
+ needs: metadata
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ id-token: write
+ attestations: write
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
- targets: ${{ matrix.target }}
+ targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2
with:
- key: ${{ matrix.target }}
- - name: Install Linux build dependencies
- if: runner.os == 'Linux'
+ key: release-x86_64-unknown-linux-gnu
+ - name: Export package version
+ run: echo "BOOTABLE_VERSION=${{ needs.metadata.outputs.version }}" >> "$GITHUB_ENV"
+ - name: Install build and packaging dependencies
run: |
sudo apt-get update
- sudo apt-get install -y libfontconfig1-dev libxkbcommon-x11-dev pkg-config
- - run: cargo test --workspace
- - run: cargo build --release --workspace --target ${{ matrix.target }}
- - name: Package Unix artifacts
- if: matrix.archive == 'tar'
+ sudo apt-get install -y libfontconfig1-dev libxkbcommon-x11-dev pkg-config rpm
+ - name: Download pinned AppImage tools
shell: bash
run: |
- version="${GITHUB_REF_NAME#v}"
- asset="bootable-${version}-${{ matrix.target }}.tar.gz"
- stage="$(mktemp -d)"
- cp "target/${{ matrix.target }}/release/bootable" "$stage/"
- cp "target/${{ matrix.target }}/release/bootable-desktop" "$stage/"
- cp "target/${{ matrix.target }}/release/bootable-helper" "$stage/"
- cp assets/bootable-mark.svg "$stage/bootable.svg"
- if [ "$RUNNER_OS" = "Linux" ]; then
- cp packaging/app.bootable.Bootable.desktop "$stage/"
- cp packaging/app.bootable.write-media.policy "$stage/"
- fi
- cp scripts/install.sh "$stage/"
- cp README.md LICENSE "$stage/"
- tar -C "$stage" -czf "$asset" .
- sha256sum "$asset" > "${asset}.sha256" 2>/dev/null || shasum -a 256 "$asset" > "${asset}.sha256"
- - name: Verify Unix release archive
- if: matrix.archive == 'tar'
+ mkdir -p "$RUNNER_TEMP/appimage-tools"
+ curl -fsSLo "$RUNNER_TEMP/appimage-tools/linuxdeploy" \
+ https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20251107-1/linuxdeploy-x86_64.AppImage
+ echo 'c20cd71e3a4e3b80c3483cef793cda3f4e990aca14014d23c544ca3ce1270b4d '"$RUNNER_TEMP/appimage-tools/linuxdeploy" | sha256sum -c -
+ curl -fsSLo "$RUNNER_TEMP/appimage-tools/appimagetool" \
+ https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
+ echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 '"$RUNNER_TEMP/appimage-tools/appimagetool" | sha256sum -c -
+ chmod 0755 "$RUNNER_TEMP/appimage-tools/linuxdeploy" "$RUNNER_TEMP/appimage-tools/appimagetool"
+ - run: cargo test --workspace
+ - run: cargo build --release --workspace --target x86_64-unknown-linux-gnu
+ - name: Build DEB, RPM, AppImage, and archive
+ env:
+ APPIMAGE_EXTRACT_AND_RUN: '1'
+ LINUXDEPLOY: ${{ runner.temp }}/appimage-tools/linuxdeploy
+ APPIMAGETOOL: ${{ runner.temp }}/appimage-tools/appimagetool
+ run: scripts/package-linux.sh "$BOOTABLE_VERSION"
+ - name: Verify Linux packages
+ run: scripts/verify-linux-packages.sh "$BOOTABLE_VERSION"
+ - uses: actions/attest-build-provenance@v2
+ if: github.event_name == 'workflow_dispatch'
+ with:
+ subject-path: dist/linux/*
+ - uses: actions/upload-artifact@v4
+ with:
+ name: release-linux-x86_64
+ path: dist/linux/*
+ if-no-files-found: error
+
+ macos:
+ name: Package macOS Apple Silicon
+ needs: metadata
+ runs-on: macos-14
+ permissions:
+ contents: read
+ id-token: write
+ attestations: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: aarch64-apple-darwin
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: release-aarch64-apple-darwin
+ - name: Export package version
+ run: echo "BOOTABLE_VERSION=${{ needs.metadata.outputs.version }}" >> "$GITHUB_ENV"
+ - run: cargo test --workspace
+ - run: cargo build --release --workspace --target aarch64-apple-darwin
+ - name: Build DMG and archive
+ run: scripts/package-macos.sh "$BOOTABLE_VERSION"
+ - name: Verify macOS packages
+ run: scripts/verify-macos-packages.sh "$BOOTABLE_VERSION"
+ - uses: actions/attest-build-provenance@v2
+ if: github.event_name == 'workflow_dispatch'
+ with:
+ subject-path: dist/macos/*
+ - uses: actions/upload-artifact@v4
+ with:
+ name: release-macos-aarch64
+ path: dist/macos/*
+ if-no-files-found: error
+
+ windows:
+ name: Package Windows x86-64
+ needs: metadata
+ runs-on: windows-2025
+ permissions:
+ contents: read
+ id-token: write
+ attestations: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: x86_64-pc-windows-msvc
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: release-x86_64-pc-windows-msvc
+ - name: Export package version
shell: bash
- run: |
- set -euo pipefail
- version="${GITHUB_REF_NAME#v}"
- asset="bootable-${version}-${{ matrix.target }}.tar.gz"
- verify="$(mktemp -d)"
- trap 'rm -rf "$verify"' EXIT
- if command -v sha256sum >/dev/null 2>&1; then
- sha256sum -c "${asset}.sha256"
- else
- shasum -a 256 -c "${asset}.sha256"
- fi
- tar -xzf "$asset" -C "$verify"
- for executable in bootable bootable-desktop bootable-helper install.sh; do
- test -x "$verify/$executable"
- done
- for file in README.md LICENSE bootable.svg; do
- test -f "$verify/$file"
- done
- if [ "$RUNNER_OS" = "Linux" ]; then
- test -f "$verify/app.bootable.Bootable.desktop"
- test -f "$verify/app.bootable.write-media.policy"
- fi
- test "$("$verify/bootable" --version)" = "bootable $version"
- "$verify/bootable" --help >/dev/null
- - name: Package Windows artifacts
- if: matrix.archive == 'zip'
+ run: echo "BOOTABLE_VERSION=${{ needs.metadata.outputs.version }}" >> "$GITHUB_ENV"
+ - name: Install pinned cargo-packager
+ run: cargo install cargo-packager --locked --version 0.11.8
+ - run: cargo test --workspace
+ - run: cargo build --release --workspace --target x86_64-pc-windows-msvc
+ - name: Build MSI, NSIS installer, portable executables, and archive
shell: pwsh
- run: |
- $version = $env:GITHUB_REF_NAME.TrimStart('v')
- $asset = "bootable-$version-${{ matrix.target }}.zip"
- New-Item -ItemType Directory -Path stage
- Copy-Item "target/${{ matrix.target }}/release/bootable.exe" stage/
- Copy-Item "target/${{ matrix.target }}/release/bootable-desktop.exe" stage/
- Copy-Item "target/${{ matrix.target }}/release/bootable-helper.exe" stage/
- Copy-Item scripts/install.ps1 stage/
- Copy-Item assets/bootable-mark.svg stage/bootable.svg
- Copy-Item README.md,LICENSE stage/
- Compress-Archive -Path stage/* -DestinationPath $asset
- $hash = (Get-FileHash $asset -Algorithm SHA256).Hash.ToLower()
- "$hash $asset" | Set-Content -NoNewline "$asset.sha256"
- - name: Verify Windows release archive
- if: matrix.archive == 'zip'
+ run: scripts/package-windows.ps1 -Version $env:BOOTABLE_VERSION
+ - name: Verify Windows packages
shell: pwsh
- run: |
- $version = $env:GITHUB_REF_NAME.TrimStart('v')
- $asset = "bootable-$version-${{ matrix.target }}.zip"
- $expectedHash = (Get-Content "$asset.sha256" -Raw).Split(' ')[0]
- $actualHash = (Get-FileHash $asset -Algorithm SHA256).Hash.ToLower()
- if ($actualHash -ne $expectedHash) { throw 'Packaged ZIP checksum does not match its sidecar.' }
- $verify = Join-Path $env:RUNNER_TEMP 'bootable-release-verify'
- Expand-Archive -LiteralPath $asset -DestinationPath $verify
- $required = @(
- 'bootable.exe',
- 'bootable-desktop.exe',
- 'bootable-helper.exe',
- 'install.ps1',
- 'bootable.svg',
- 'README.md',
- 'LICENSE'
- )
- foreach ($name in $required) {
- if (-not (Test-Path -LiteralPath (Join-Path $verify $name) -PathType Leaf)) {
- throw "Release ZIP is missing $name."
- }
- }
- $reportedVersion = & (Join-Path $verify 'bootable.exe') --version
- if ($LASTEXITCODE -ne 0 -or $reportedVersion.Trim() -ne "bootable $version") {
- throw "Release binary reported unexpected version: $reportedVersion"
- }
- & (Join-Path $verify 'bootable.exe') --help | Out-Null
- if ($LASTEXITCODE -ne 0) { throw 'Packaged TUI failed its help smoke test.' }
+ run: scripts/verify-windows-packages.ps1 -Version $env:BOOTABLE_VERSION
- uses: actions/attest-build-provenance@v2
+ if: github.event_name == 'workflow_dispatch'
with:
- subject-path: bootable-*
+ subject-path: dist/windows/*
- uses: actions/upload-artifact@v4
with:
- name: release-${{ matrix.target }}
- path: |
- bootable-*.tar.gz
- bootable-*.zip
- bootable-*.sha256
+ name: release-windows-x86_64
+ path: dist/windows/*
if-no-files-found: error
+ publish-rc:
+ name: Publish release candidate
+ needs: [metadata, linux, macos, windows]
+ if: >-
+ github.event_name == 'pull_request' &&
+ github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: ubuntu-24.04
+ permissions:
+ actions: read
+ contents: write
+ env:
+ GH_TOKEN: ${{ github.token }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ steps:
+ - name: Require the matching CI run to pass
+ shell: bash
+ run: |
+ for attempt in {1..60}; do
+ runs="$(gh api -X GET \
+ "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \
+ -f event=pull_request \
+ -f head_sha="$HEAD_SHA" \
+ -f per_page=20)"
+
+ conclusion="$(jq -r \
+ '[.workflow_runs[] | select(.head_sha == env.HEAD_SHA)] | sort_by(.run_attempt) | last | .conclusion // ""' \
+ <<<"$runs")"
+ status="$(jq -r \
+ '[.workflow_runs[] | select(.head_sha == env.HEAD_SHA)] | sort_by(.run_attempt) | last | .status // ""' \
+ <<<"$runs")"
+
+ if [[ "$status" == completed && "$conclusion" == success ]]; then
+ exit 0
+ fi
+ if [[ "$status" == completed && -n "$conclusion" ]]; then
+ echo "CI finished with conclusion: $conclusion" >&2
+ exit 1
+ fi
+ sleep 20
+ done
+
+ echo "Timed out waiting for CI on $HEAD_SHA" >&2
+ exit 1
+
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: release-*
+ merge-multiple: true
+ path: dist
+
+ - name: Verify the downloaded release set
+ shell: bash
+ run: |
+ test "$(find dist -maxdepth 1 -type f | wc -l)" -eq 22
+ (cd dist && sha256sum --check ./*.sha256)
+
+ - name: Name the release candidate
+ id: rc
+ shell: bash
+ run: echo "tag=v${{ needs.metadata.outputs.version }}-rc.pr${PR_NUMBER}.${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
+
+ - uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.rc.outputs.tag }}
+ target_commitish: ${{ env.HEAD_SHA }}
+ name: Bootable ${{ steps.rc.outputs.tag }}
+ prerelease: true
+ generate_release_notes: true
+ body: |
+ ## Bootable ${{ steps.rc.outputs.tag }}
+
+ Automated release candidate for #${{ env.PR_NUMBER }} after both CI and the native
+ package matrix passed. This build is for testing; stable releases are published only
+ through a human-approved manual workflow run from `main`.
+
+ - Linux x86-64: AppImage, DEB, RPM, and tar.gz
+ - Windows 10+ x86-64: MSI, setup EXE, portable GUI/TUI EXEs, and ZIP
+ - macOS Apple Silicon: DMG and tar.gz
+ - Integrity: SHA-256 sidecars for every asset
+ - Safety: interactive apps remain unelevated; approved removable-media writes use the fixed helper
+ files: dist/*
+
publish:
- name: Publish release
- needs: build
+ name: Publish stable release
+ needs: [metadata, linux, macos, windows]
+ if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
+ permissions:
+ contents: write
steps:
- uses: actions/download-artifact@v4
with:
@@ -158,21 +254,24 @@ jobs:
path: dist
- uses: softprops/action-gh-release@v2
with:
+ tag_name: ${{ needs.metadata.outputs.tag }}
+ name: Bootable ${{ needs.metadata.outputs.tag }}
prerelease: false
generate_release_notes: true
body: |
- ## Bootable ${{ github.ref_name }}
+ ## Bootable ${{ needs.metadata.outputs.tag }}
- Executables are unsigned; the macOS build is not notarized. Use expendable USB or SD
- media. Check the physical target before approving erasure.
+ Bootable ships native installers and portable builds for its matching GUI and TUI.
+ Packages are unsigned and the macOS app is ad-hoc signed, not notarized. Use expendable
+ USB or SD media and check the physical target before approving erasure.
- - Packages: Linux x86-64, Windows 10+ x86-64, macOS Apple Silicon
- - Interfaces: native desktop, mouse-enabled TUI, CLI
- - Raw media: ISO, IMG, RAW, XZ, gzip, Zstandard, bzip2
- - Windows media: GPT or MBR, FAT32, split WIM, boot-tree audit
- - Integrity: publisher checksum when available; raw byte read-back after writing
+ - Linux x86-64: AppImage, DEB, RPM, and tar.gz
+ - Windows 10+ x86-64: MSI, setup EXE, portable GUI/TUI EXEs, and ZIP
+ - macOS Apple Silicon: DMG and tar.gz
+ - Integrity: SHA-256 sidecars and GitHub build provenance for every asset
- Privilege: interactive apps remain unelevated; a fixed helper performs approved writes
- Each archive has a SHA-256 sidecar and GitHub build provenance. Legacy BIOS helpers,
- Linux persistence, Windows To Go, DOS media, and multiboot are not implemented.
+ The DEB/RPM and Windows installers place the protected helper automatically. The macOS
+ DMG includes an explicit helper installer. Legacy BIOS helpers, Linux persistence,
+ Windows To Go, DOS media, and multiboot are not implemented.
files: dist/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6df0e3..c5cb6fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
All notable changes to Bootable are documented here.
+## 0.1.1 — 2026-08-27
+
+- Added native Linux AppImage, DEB, and RPM packages, each containing the desktop app, TUI, and
+ protected write helper.
+- Added a macOS DMG with an application bundle and explicit privileged-helper installer.
+- Added Windows MSI and setup EXE installers plus direct portable GUI and TUI executables.
+- Added package-content, embedded-version, checksum, and installer extraction checks to the release
+ workflow, with SHA-256 sidecars and provenance for every published artifact.
+
## 0.1.0 — 2026-08-27
- Reworked the desktop and terminal interfaces around the same workspace-first flow, shared
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2a426fb..ff94a9d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,6 +18,10 @@ behavior does not count as implementation.
Do not copy code from Rufus, WoeUSB, or other projects whose license is incompatible with this
Apache-2.0 repository. Behavioral research and clean-room implementations are welcome.
+Every same-repository pull request publishes a release candidate only after both CI and the native
+package matrix pass. Stable releases require an explicit manual dispatch from `main`; see
+[release channels](docs/releases.md).
+
Documentation and product copy must follow the same standard as destructive code:
- Lead with the task, constraint, or result.
diff --git a/Cargo.lock b/Cargo.lock
index 540fe66..f60fe3b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -792,7 +792,7 @@ dependencies = [
[[package]]
name = "bootable-core"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"bzip2",
"flate2",
@@ -817,7 +817,7 @@ dependencies = [
[[package]]
name = "bootable-desktop"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"anyhow",
"bootable-core",
@@ -831,7 +831,7 @@ dependencies = [
[[package]]
name = "bootable-helper"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"bootable-core",
"serde_json",
@@ -839,7 +839,7 @@ dependencies = [
[[package]]
name = "bootable-tui"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"anyhow",
"bootable-core",
diff --git a/Cargo.toml b/Cargo.toml
index c7f82bf..2cbb94f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ default-members = [
resolver = "2"
[workspace.package]
-version = "0.1.0"
+version = "0.1.1"
edition = "2024"
license = "Apache-2.0"
rust-version = "1.88"
diff --git a/README.md b/README.md
index 7b81e71..87adf83 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
Create and verify bootable USB and SD drives.
- Linux ·
- Windows ·
- macOS ·
+ Linux AppImage ·
+ Windows installer ·
+ macOS DMG ·
All downloads and checksums
@@ -65,7 +65,8 @@ curl -fsSL https://bootable.palash.dev/install.sh | sh -s -- --tui
curl -fsSL https://bootable.palash.dev/install.sh | sh -s -- --all
```
-Windows: download and extract the ZIP, then run:
+Native packages are also available as DEB/RPM on Linux, MSI on Windows, and DMG on macOS. For the
+portable Windows ZIP, extract it and run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 -Variant All
@@ -92,6 +93,7 @@ Project links
[Architecture](docs/architecture.md) ·
[Validation](docs/validation.md) ·
[GUI/TUI parity](docs/ui-parity.md) ·
+[Release channels](docs/releases.md) ·
[Contributing](CONTRIBUTING.md) ·
[Changelog](CHANGELOG.md) ·
[Report a problem](https://github.com/debpalash/bootable/issues/new?template=bug-report.yml)
diff --git a/assets/bootable-mark.png b/assets/bootable-mark.png
new file mode 100644
index 0000000..f6de642
Binary files /dev/null and b/assets/bootable-mark.png differ
diff --git a/assets/bootable.ico b/assets/bootable.ico
new file mode 100644
index 0000000..0e354c0
Binary files /dev/null and b/assets/bootable.ico differ
diff --git a/docs/releases.md b/docs/releases.md
new file mode 100644
index 0000000..18b9c73
--- /dev/null
+++ b/docs/releases.md
@@ -0,0 +1,29 @@
+# Release channels
+
+Bootable has two GitHub release channels. The rules live in CI and are not optional release
+conventions.
+
+## Release candidates
+
+Every same-repository pull request runs the complete CI and native package matrices. Once both
+workflows pass, CI publishes an automatic GitHub prerelease named
+`v-rc.pr.` with the Linux, macOS, and Windows assets from that exact pull
+request commit.
+
+Pull requests from forks are deliberately excluded from publishing: their code is tested with a
+read-only token, but it is never distributed under the project's GitHub Releases account.
+
+## Stable releases
+
+A stable release is never created by a tag push or merge. A maintainer must explicitly run the
+`Release` workflow from the `main` branch. CI rebuilds and verifies every native package, attests
+the stable artifacts, and publishes the Cargo workspace version as `v`.
+
+Before dispatching a stable release:
+
+1. Confirm the intended version is committed in `Cargo.toml` and `Cargo.lock` on `main`.
+2. Confirm the release-candidate assets were installed or opened on their target platforms.
+3. Open **Actions → Release → Run workflow**, select `main`, and approve the run.
+4. Confirm the resulting release is neither a draft nor a prerelease and contains all 22 assets.
+
+Stable tags and releases are immutable. Corrections use a new patch version.
diff --git a/packaging/Packager.toml b/packaging/Packager.toml
new file mode 100644
index 0000000..b4dfba6
--- /dev/null
+++ b/packaging/Packager.toml
@@ -0,0 +1,36 @@
+name = "bootable"
+product-name = "Bootable"
+identifier = "app.bootable.Bootable"
+version = "0.1.1"
+description = "Create verified boot media from trusted images"
+long-description = "A safety-first boot media writer with matching desktop and terminal interfaces."
+homepage = "https://github.com/debpalash/bootable"
+authors = ["Bootable contributors"]
+publisher = "Bootable contributors"
+category = "Utility"
+license-file = "../LICENSE"
+icons = ["../assets/bootable.ico"]
+resources = ["../README.md", "../LICENSE"]
+formats = ["wix", "nsis"]
+out-dir = "../dist/windows"
+binaries-dir = "../target/x86_64-pc-windows-msvc/release"
+target-triple = "x86_64-pc-windows-msvc"
+
+[[binaries]]
+path = "bootable-desktop"
+main = true
+
+[[binaries]]
+path = "bootable"
+
+[[binaries]]
+path = "bootable-helper"
+
+[nsis]
+installer-mode = "perMachine"
+installer-icon = "../assets/bootable.ico"
+compression = "lzma"
+languages = ["English"]
+
+[wix]
+languages = ["en-US"]
diff --git a/scripts/install.sh b/scripts/install.sh
index f125484..32b8d83 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -2,7 +2,7 @@
set -eu
REPOSITORY="${BOOTABLE_REPOSITORY:-debpalash/bootable}"
-VERSION="${BOOTABLE_VERSION:-0.1.0}"
+VERSION="${BOOTABLE_VERSION:-0.1.1}"
VARIANT="${1:---gui}"
INSTALL_ROOT="${BOOTABLE_INSTALL_ROOT:-${HOME}/.local}"
diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh
new file mode 100755
index 0000000..aec451c
--- /dev/null
+++ b/scripts/package-linux.sh
@@ -0,0 +1,176 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+version="${1:?usage: package-linux.sh VERSION [TARGET] [OUTPUT_DIR]}"
+target="${2:-x86_64-unknown-linux-gnu}"
+output="${3:-dist/linux}"
+root="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
+binary_dir="$root/target/$target/release"
+linuxdeploy="${LINUXDEPLOY:-linuxdeploy-x86_64.AppImage}"
+appimagetool="${APPIMAGETOOL:-appimagetool-x86_64.AppImage}"
+
+for executable in bootable bootable-desktop bootable-helper; do
+ test -x "$binary_dir/$executable" || {
+ echo "missing executable: $binary_dir/$executable" >&2
+ exit 1
+ }
+done
+command -v dpkg-deb >/dev/null
+command -v rpmbuild >/dev/null
+test -x "$linuxdeploy"
+test -x "$appimagetool"
+
+output="$root/$output"
+mkdir -p "$output"
+work="$(mktemp -d "${TMPDIR:-/tmp}/bootable-linux-package.XXXXXX")"
+trap 'rm -rf "$work"' EXIT
+
+install_payload() {
+ local destination="$1"
+ install -D -m 0755 "$binary_dir/bootable" "$destination/usr/bin/bootable"
+ install -D -m 0755 "$binary_dir/bootable-desktop" "$destination/usr/bin/bootable-desktop"
+ install -D -m 0755 "$binary_dir/bootable-helper" "$destination/usr/libexec/bootable-helper"
+ install -D -m 0644 "$root/assets/bootable-mark.svg" \
+ "$destination/usr/share/icons/hicolor/scalable/apps/bootable.svg"
+ install -D -m 0644 "$root/assets/bootable-mark.png" \
+ "$destination/usr/share/icons/hicolor/1024x1024/apps/bootable.png"
+ install -D -m 0644 "$root/packaging/app.bootable.write-media.policy" \
+ "$destination/usr/share/polkit-1/actions/app.bootable.write-media.policy"
+ install -D -m 0644 "$root/README.md" "$destination/usr/share/doc/bootable/README.md"
+ install -D -m 0644 "$root/LICENSE" "$destination/usr/share/doc/bootable/LICENSE"
+ mkdir -p "$destination/usr/share/applications"
+ sed 's|@EXEC@|/usr/bin/bootable-desktop|g' \
+ "$root/packaging/app.bootable.Bootable.desktop" \
+ > "$destination/usr/share/applications/app.bootable.Bootable.desktop"
+ chmod 0644 "$destination/usr/share/applications/app.bootable.Bootable.desktop"
+}
+
+deb_root="$work/deb"
+install_payload "$deb_root"
+mkdir -p "$deb_root/DEBIAN"
+installed_size="$(du -sk "$deb_root/usr" | awk '{print $1}')"
+cat > "$deb_root/DEBIAN/control" <
+Homepage: https://github.com/debpalash/bootable
+Depends: libc6, libfontconfig1, libxkbcommon-x11-0, polkitd | policykit-1
+Description: Safety-first boot media writer
+ Bootable provides matching desktop and terminal interfaces for inspecting,
+ writing, and verifying bootable images on removable media.
+EOF
+deb_asset="$output/bootable_${version}_amd64.deb"
+dpkg-deb --build --root-owner-group "$deb_root" "$deb_asset"
+
+rpm_top="$work/rpmbuild"
+mkdir -p "$rpm_top"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
+install -m 0755 "$binary_dir/bootable" "$rpm_top/SOURCES/bootable"
+install -m 0755 "$binary_dir/bootable-desktop" "$rpm_top/SOURCES/bootable-desktop"
+install -m 0755 "$binary_dir/bootable-helper" "$rpm_top/SOURCES/bootable-helper"
+install -m 0644 "$root/assets/bootable-mark.svg" "$rpm_top/SOURCES/bootable.svg"
+install -m 0644 "$root/assets/bootable-mark.png" "$rpm_top/SOURCES/bootable.png"
+install -m 0644 "$root/packaging/app.bootable.write-media.policy" "$rpm_top/SOURCES/app.bootable.write-media.policy"
+sed 's|@EXEC@|/usr/bin/bootable-desktop|g' \
+ "$root/packaging/app.bootable.Bootable.desktop" \
+ > "$rpm_top/SOURCES/app.bootable.Bootable.desktop"
+install -m 0644 "$root/README.md" "$rpm_top/SOURCES/README.md"
+install -m 0644 "$root/LICENSE" "$rpm_top/SOURCES/LICENSE"
+cat > "$rpm_top/SPECS/bootable.spec" < - $version-1
+- Native installer release.
+EOF
+rpmbuild --define "_topdir $rpm_top" -bb "$rpm_top/SPECS/bootable.spec"
+rpm_asset="$(find "$rpm_top/RPMS" -type f -name '*.rpm' -print -quit)"
+test -n "$rpm_asset"
+cp "$rpm_asset" "$output/bootable-${version}-1.x86_64.rpm"
+
+appdir="$work/Bootable.AppDir"
+install_payload "$appdir"
+# linuxdeploy accepts SVG icons but rejects the 1024px hicolor size that is
+# useful to native packages. Keep the high-resolution PNG in DEB/RPM and let
+# the AppImage use the scalable mark.
+rm -f "$appdir/usr/share/icons/hicolor/1024x1024/apps/bootable.png"
+sed 's|@EXEC@|bootable-desktop|g' \
+ "$root/packaging/app.bootable.Bootable.desktop" \
+ > "$appdir/usr/share/applications/app.bootable.Bootable.desktop"
+# The adjacent helper lets the runtime retain its fixed-name fallback inside the AppImage.
+install -m 0755 "$binary_dir/bootable-helper" "$appdir/usr/bin/bootable-helper"
+"$linuxdeploy" \
+ --appdir "$appdir" \
+ --executable "$appdir/usr/bin/bootable-desktop" \
+ --executable "$appdir/usr/bin/bootable" \
+ --executable "$appdir/usr/bin/bootable-helper" \
+ --desktop-file "$appdir/usr/share/applications/app.bootable.Bootable.desktop" \
+ --icon-file "$appdir/usr/share/icons/hicolor/scalable/apps/bootable.svg"
+cat > "$appdir/AppRun" <<'EOF'
+#!/bin/sh
+set -eu
+appdir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
+if [ "${1:-}" = "--tui" ]; then
+ shift
+ exec "$appdir/usr/bin/bootable" "$@"
+fi
+exec "$appdir/usr/bin/bootable-desktop" "$@"
+EOF
+chmod 0755 "$appdir/AppRun"
+ARCH=x86_64 "$appimagetool" "$appdir" "$output/bootable-${version}-x86_64.AppImage"
+
+archive_stage="$work/archive"
+mkdir -p "$archive_stage"
+install -m 0755 "$binary_dir/bootable" "$archive_stage/bootable"
+install -m 0755 "$binary_dir/bootable-desktop" "$archive_stage/bootable-desktop"
+install -m 0755 "$binary_dir/bootable-helper" "$archive_stage/bootable-helper"
+install -m 0755 "$root/scripts/install.sh" "$archive_stage/install.sh"
+install -m 0644 "$root/assets/bootable-mark.svg" "$archive_stage/bootable.svg"
+install -m 0644 "$root/packaging/app.bootable.Bootable.desktop" "$archive_stage/app.bootable.Bootable.desktop"
+install -m 0644 "$root/packaging/app.bootable.write-media.policy" "$archive_stage/app.bootable.write-media.policy"
+install -m 0644 "$root/README.md" "$root/LICENSE" "$archive_stage/"
+tar -C "$archive_stage" -czf "$output/bootable-${version}-${target}.tar.gz" .
+
+for asset in "$output"/*; do
+ case "$asset" in *.sha256) continue ;; esac
+ (cd "$output" && sha256sum "$(basename "$asset")" > "$(basename "$asset").sha256")
+done
diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh
new file mode 100755
index 0000000..0171f79
--- /dev/null
+++ b/scripts/package-macos.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+version="${1:?usage: package-macos.sh VERSION [TARGET] [OUTPUT_DIR]}"
+target="${2:-aarch64-apple-darwin}"
+output="${3:-dist/macos}"
+root="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
+binary_dir="$root/target/$target/release"
+output="$root/$output"
+
+for executable in bootable bootable-desktop bootable-helper; do
+ test -x "$binary_dir/$executable" || {
+ echo "missing executable: $binary_dir/$executable" >&2
+ exit 1
+ }
+done
+mkdir -p "$output"
+work="$(mktemp -d "${TMPDIR:-/tmp}/bootable-macos-package.XXXXXX")"
+trap 'rm -rf "$work"' EXIT
+
+app="$work/dmg/Bootable.app"
+contents="$app/Contents"
+mkdir -p "$contents/MacOS" "$contents/Resources"
+install -m 0755 "$binary_dir/bootable-desktop" "$contents/MacOS/bootable-desktop"
+install -m 0755 "$binary_dir/bootable" "$contents/MacOS/bootable"
+install -m 0755 "$binary_dir/bootable-helper" "$contents/MacOS/bootable-helper"
+
+iconset="$work/bootable.iconset"
+mkdir -p "$iconset"
+for size in 16 32 128 256 512; do
+ sips -z "$size" "$size" "$root/assets/bootable-mark.png" \
+ --out "$iconset/icon_${size}x${size}.png" >/dev/null
+ double="$((size * 2))"
+ sips -z "$double" "$double" "$root/assets/bootable-mark.png" \
+ --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null
+done
+iconutil -c icns "$iconset" -o "$contents/Resources/bootable.icns"
+
+cat > "$contents/Info.plist" <
+
+
+
+ CFBundleDevelopmentRegionen
+ CFBundleDisplayNameBootable
+ CFBundleExecutablebootable-desktop
+ CFBundleIconFilebootable
+ CFBundleIdentifierapp.bootable.Bootable
+ CFBundleInfoDictionaryVersion6.0
+ CFBundleNameBootable
+ CFBundlePackageTypeAPPL
+ CFBundleShortVersionString$version
+ CFBundleVersion$version
+ LSApplicationCategoryTypepublic.app-category.utilities
+ LSMinimumSystemVersion12.0
+ NSHighResolutionCapable
+
+
+EOF
+plutil -lint "$contents/Info.plist"
+codesign --force --deep --sign - "$app"
+
+cat > "$work/dmg/Install Bootable Helper.command" <<'EOF'
+#!/bin/sh
+set -eu
+bundle="$(CDPATH= cd -- "$(dirname -- "$0")/Bootable.app/Contents/MacOS" && pwd)"
+echo "Bootable installs only its narrow media-write helper with administrator ownership."
+sudo install -d -m 0755 /Library/PrivilegedHelperTools
+sudo install -m 0755 "$bundle/bootable-helper" /Library/PrivilegedHelperTools/app.bootable.helper
+sudo chown root:wheel /Library/PrivilegedHelperTools/app.bootable.helper
+echo "Bootable helper installed. You can now launch Bootable.app."
+EOF
+chmod 0755 "$work/dmg/Install Bootable Helper.command"
+ln -s /Applications "$work/dmg/Applications"
+
+dmg="$output/bootable-${version}-aarch64.dmg"
+hdiutil create -volname "Bootable $version" -srcfolder "$work/dmg" -ov -format UDZO "$dmg"
+
+archive_stage="$work/archive"
+mkdir -p "$archive_stage"
+install -m 0755 "$binary_dir/bootable" "$archive_stage/bootable"
+install -m 0755 "$binary_dir/bootable-desktop" "$archive_stage/bootable-desktop"
+install -m 0755 "$binary_dir/bootable-helper" "$archive_stage/bootable-helper"
+install -m 0755 "$root/scripts/install.sh" "$archive_stage/install.sh"
+install -m 0644 "$root/assets/bootable-mark.svg" "$archive_stage/bootable.svg"
+install -m 0644 "$root/README.md" "$root/LICENSE" "$archive_stage/"
+tar -C "$archive_stage" -czf "$output/bootable-${version}-${target}.tar.gz" .
+
+for asset in "$output"/*; do
+ case "$asset" in *.sha256) continue ;; esac
+ (cd "$output" && shasum -a 256 "$(basename "$asset")" > "$(basename "$asset").sha256")
+done
diff --git a/scripts/package-windows.ps1 b/scripts/package-windows.ps1
new file mode 100644
index 0000000..213d3c7
--- /dev/null
+++ b/scripts/package-windows.ps1
@@ -0,0 +1,54 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Version,
+ [string]$Target = 'x86_64-pc-windows-msvc',
+ [string]$OutputDirectory = 'dist/windows'
+)
+
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$binaryDirectory = Join-Path $root "target/$Target/release"
+$output = Join-Path $root $OutputDirectory
+$config = Join-Path $root 'packaging/Packager.toml'
+
+foreach ($name in @('bootable.exe', 'bootable-desktop.exe', 'bootable-helper.exe')) {
+ if (-not (Test-Path -LiteralPath (Join-Path $binaryDirectory $name) -PathType Leaf)) {
+ throw "Missing executable: $name"
+ }
+}
+$configuredVersion = Select-String -LiteralPath $config -Pattern '^version = "([^\"]+)"$'
+if (-not $configuredVersion -or $configuredVersion.Matches[0].Groups[1].Value -ne $Version) {
+ throw "Packager.toml does not declare release version $Version."
+}
+
+New-Item -ItemType Directory -Path $output -Force | Out-Null
+cargo packager --config $config --formats wix,nsis
+if ($LASTEXITCODE -ne 0) { throw 'cargo-packager failed.' }
+
+$msi = Get-ChildItem -LiteralPath $output -Filter '*.msi' | Select-Object -First 1
+$installer = Get-ChildItem -LiteralPath $output -Filter '*setup.exe' | Select-Object -First 1
+if (-not $msi) { throw 'cargo-packager did not produce an MSI.' }
+if (-not $installer) { throw 'cargo-packager did not produce an NSIS installer.' }
+Move-Item -LiteralPath $msi.FullName -Destination (Join-Path $output "bootable-$Version-x86_64.msi") -Force
+Move-Item -LiteralPath $installer.FullName -Destination (Join-Path $output "bootable-$Version-x86_64-setup.exe") -Force
+Copy-Item -LiteralPath (Join-Path $binaryDirectory 'bootable-desktop.exe') `
+ -Destination (Join-Path $output "bootable-desktop-$Version-x86_64.exe")
+Copy-Item -LiteralPath (Join-Path $binaryDirectory 'bootable.exe') `
+ -Destination (Join-Path $output "bootable-tui-$Version-x86_64.exe")
+
+$stage = Join-Path $env:RUNNER_TEMP 'bootable-windows-archive'
+New-Item -ItemType Directory -Path $stage -Force | Out-Null
+Copy-Item -LiteralPath (Join-Path $binaryDirectory 'bootable.exe') -Destination $stage
+Copy-Item -LiteralPath (Join-Path $binaryDirectory 'bootable-desktop.exe') -Destination $stage
+Copy-Item -LiteralPath (Join-Path $binaryDirectory 'bootable-helper.exe') -Destination $stage
+Copy-Item -LiteralPath (Join-Path $root 'scripts/install.ps1') -Destination $stage
+Copy-Item -LiteralPath (Join-Path $root 'assets/bootable-mark.svg') -Destination (Join-Path $stage 'bootable.svg')
+Copy-Item -LiteralPath (Join-Path $root 'README.md'),(Join-Path $root 'LICENSE') -Destination $stage
+Compress-Archive -Path (Join-Path $stage '*') `
+ -DestinationPath (Join-Path $output "bootable-$Version-$Target.zip") -Force
+
+Get-ChildItem -LiteralPath $output -File | Where-Object Extension -ne '.sha256' | ForEach-Object {
+ $hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLower()
+ "$hash $($_.Name)" | Set-Content -NoNewline "$($_.FullName).sha256"
+}
diff --git a/scripts/verify-linux-packages.sh b/scripts/verify-linux-packages.sh
new file mode 100755
index 0000000..e425905
--- /dev/null
+++ b/scripts/verify-linux-packages.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+version="${1:?usage: verify-linux-packages.sh VERSION [OUTPUT_DIR]}"
+output="${2:-dist/linux}"
+root="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
+output="$root/$output"
+
+deb="$output/bootable_${version}_amd64.deb"
+rpm="$output/bootable-${version}-1.x86_64.rpm"
+appimage="$output/bootable-${version}-x86_64.AppImage"
+archive="$output/bootable-${version}-x86_64-unknown-linux-gnu.tar.gz"
+
+(cd "$output" && sha256sum -c -- *.sha256)
+test "$(dpkg-deb -f "$deb" Version)" = "$version"
+deb_contents="$(dpkg-deb -c "$deb")"
+for path in ./usr/bin/bootable ./usr/bin/bootable-desktop ./usr/libexec/bootable-helper \
+ ./usr/share/applications/app.bootable.Bootable.desktop \
+ ./usr/share/polkit-1/actions/app.bootable.write-media.policy; do
+ grep -Fq "$path" <<<"$deb_contents"
+done
+test "$(rpm -qp --queryformat '%{VERSION}' "$rpm")" = "$version"
+rpm_contents="$(rpm -qlp "$rpm")"
+for path in /usr/bin/bootable /usr/bin/bootable-desktop /usr/libexec/bootable-helper \
+ /usr/share/applications/app.bootable.Bootable.desktop \
+ /usr/share/polkit-1/actions/app.bootable.write-media.policy; do
+ grep -Fxq "$path" <<<"$rpm_contents"
+done
+
+extract="$(mktemp -d "${TMPDIR:-/tmp}/bootable-linux-verify.XXXXXX")"
+trap 'rm -rf "$extract"' EXIT
+(cd "$extract" && "$appimage" --appimage-extract >/dev/null)
+for path in AppRun usr/bin/bootable usr/bin/bootable-desktop usr/bin/bootable-helper; do
+ test -x "$extract/squashfs-root/$path"
+done
+test "$("$extract/squashfs-root/usr/bin/bootable" --version)" = "bootable $version"
+
+mkdir "$extract/archive"
+tar -xzf "$archive" -C "$extract/archive"
+for executable in bootable bootable-desktop bootable-helper install.sh; do
+ test -x "$extract/archive/$executable"
+done
+test "$("$extract/archive/bootable" --version)" = "bootable $version"
+"$extract/archive/bootable" --help >/dev/null
diff --git a/scripts/verify-macos-packages.sh b/scripts/verify-macos-packages.sh
new file mode 100755
index 0000000..408c8df
--- /dev/null
+++ b/scripts/verify-macos-packages.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+version="${1:?usage: verify-macos-packages.sh VERSION [OUTPUT_DIR]}"
+output="${2:-dist/macos}"
+root="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
+output="$root/$output"
+dmg="$output/bootable-${version}-aarch64.dmg"
+archive="$output/bootable-${version}-aarch64-apple-darwin.tar.gz"
+
+(cd "$output" && shasum -a 256 -c ./*.sha256)
+hdiutil verify "$dmg"
+mount="$(mktemp -d "${TMPDIR:-/tmp}/bootable-dmg-verify.XXXXXX")"
+extract="$(mktemp -d "${TMPDIR:-/tmp}/bootable-macos-verify.XXXXXX")"
+cleanup() {
+ hdiutil detach "$mount" -quiet >/dev/null 2>&1 || true
+ rm -rf "$mount" "$extract"
+}
+trap cleanup EXIT
+hdiutil attach "$dmg" -readonly -nobrowse -mountpoint "$mount" -quiet
+test -d "$mount/Bootable.app"
+test -x "$mount/Bootable.app/Contents/MacOS/bootable-desktop"
+test -x "$mount/Bootable.app/Contents/MacOS/bootable"
+test -x "$mount/Bootable.app/Contents/MacOS/bootable-helper"
+test -x "$mount/Install Bootable Helper.command"
+test "$(defaults read "$mount/Bootable.app/Contents/Info" CFBundleShortVersionString)" = "$version"
+codesign --verify --deep --strict "$mount/Bootable.app"
+test "$("$mount/Bootable.app/Contents/MacOS/bootable" --version)" = "bootable $version"
+hdiutil detach "$mount" -quiet
+
+tar -xzf "$archive" -C "$extract"
+for executable in bootable bootable-desktop bootable-helper install.sh; do
+ test -x "$extract/$executable"
+done
+test "$("$extract/bootable" --version)" = "bootable $version"
diff --git a/scripts/verify-windows-packages.ps1 b/scripts/verify-windows-packages.ps1
new file mode 100644
index 0000000..5b4a447
--- /dev/null
+++ b/scripts/verify-windows-packages.ps1
@@ -0,0 +1,62 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Version,
+ [string]$OutputDirectory = 'dist/windows'
+)
+
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$output = Join-Path $root $OutputDirectory
+$expected = @(
+ "bootable-$Version-x86_64.msi",
+ "bootable-$Version-x86_64-setup.exe",
+ "bootable-desktop-$Version-x86_64.exe",
+ "bootable-tui-$Version-x86_64.exe",
+ "bootable-$Version-x86_64-pc-windows-msvc.zip"
+)
+
+foreach ($name in $expected) {
+ $path = Join-Path $output $name
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Missing package: $name" }
+ $sidecar = "$path.sha256"
+ $expectedHash = (Get-Content -LiteralPath $sidecar -Raw).Split(' ')[0]
+ $actualHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLower()
+ if ($expectedHash -ne $actualHash) { throw "Checksum mismatch: $name" }
+}
+
+$reportedVersion = & (Join-Path $output "bootable-tui-$Version-x86_64.exe") --version
+if ($LASTEXITCODE -ne 0 -or $reportedVersion.Trim() -ne "bootable $Version") {
+ throw "Portable TUI reported an unexpected version: $reportedVersion"
+}
+& (Join-Path $output "bootable-tui-$Version-x86_64.exe") --help | Out-Null
+if ($LASTEXITCODE -ne 0) { throw 'Portable TUI failed its help smoke test.' }
+
+$msiExtract = Join-Path $env:RUNNER_TEMP 'bootable-msi-verify'
+$process = Start-Process -FilePath 'msiexec.exe' -ArgumentList @(
+ '/a',
+ "`"$(Join-Path $output "bootable-$Version-x86_64.msi")`"",
+ '/qn',
+ "TARGETDIR=`"$msiExtract`""
+) -Wait -PassThru
+if ($process.ExitCode -ne 0) { throw "MSI administrative extraction failed: $($process.ExitCode)" }
+foreach ($name in @('bootable.exe', 'bootable-desktop.exe', 'bootable-helper.exe')) {
+ if (-not (Get-ChildItem -LiteralPath $msiExtract -Recurse -Filter $name -File)) {
+ throw "MSI is missing $name."
+ }
+}
+
+$listing = & 7z l (Join-Path $output "bootable-$Version-x86_64-setup.exe")
+if ($LASTEXITCODE -ne 0) { throw 'Could not inspect the NSIS installer.' }
+foreach ($name in @('bootable.exe', 'bootable-desktop.exe', 'bootable-helper.exe')) {
+ if (-not ($listing -match [regex]::Escape($name))) { throw "NSIS installer is missing $name." }
+}
+
+$zipExtract = Join-Path $env:RUNNER_TEMP 'bootable-zip-verify'
+Expand-Archive -LiteralPath (Join-Path $output "bootable-$Version-x86_64-pc-windows-msvc.zip") `
+ -DestinationPath $zipExtract -Force
+foreach ($name in @('bootable.exe', 'bootable-desktop.exe', 'bootable-helper.exe', 'install.ps1')) {
+ if (-not (Test-Path -LiteralPath (Join-Path $zipExtract $name) -PathType Leaf)) {
+ throw "ZIP is missing $name."
+ }
+}
diff --git a/site/src/components/Header.astro b/site/src/components/Header.astro
index 36a39a9..5ed5d39 100644
--- a/site/src/components/Header.astro
+++ b/site/src/components/Header.astro
@@ -12,7 +12,7 @@ const homeHref = home ? '#top' : '/';
bootable
- v0.1.0
+ v0.1.1