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