Release #83
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-sdist: | |
| name: Build sdists | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Build review app frontend | |
| working-directory: web/review-app | |
| run: npm ci && npm run build | |
| - name: Build terminal app frontend | |
| working-directory: web/terminal-app | |
| run: npm ci && npm run build | |
| - name: Build markdown preview app frontend | |
| working-directory: web/markdown-app | |
| run: npm ci && npm run build | |
| - name: Build VNC app frontend | |
| working-directory: web/vnc-app | |
| run: npm ci && npm run build | |
| - name: Build config app frontend | |
| working-directory: web/config-app | |
| run: npm ci && npm run build | |
| - name: Build PDF app frontend | |
| working-directory: web/pdf-app | |
| run: npm ci && npm run build | |
| - name: Build open-shrimp sdist | |
| run: uv build --sdist | |
| - name: Build moonshine-stt sdist | |
| working-directory: moonshine-stt | |
| run: uv build --sdist | |
| - name: Upload open-shrimp sdist | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sdist-openshrimp | |
| path: dist/*.tar.gz | |
| - name: Upload moonshine-stt sdist | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sdist-moonshine-stt | |
| path: moonshine-stt/dist/*.tar.gz | |
| build-binary: | |
| name: Build ${{ matrix.artifact }} | |
| needs: build-sdist | |
| runs-on: ${{ matrix.os }} | |
| # Every step below is a POSIX shell script (realpath, $GITHUB_OUTPUT | |
| # appends, `if [ ... ]`); pwsh is the Windows runner's default, so pin | |
| # bash for all of them — git-bash ships on the Windows image. | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # open-shrimp | |
| - os: ubuntu-latest | |
| artifact: openshrimp-linux-x86_64 | |
| sdist_artifact: sdist-openshrimp | |
| exec_code: "from open_shrimp.main import main; main()" | |
| features: "libvirt" | |
| - os: ubuntu-24.04-arm | |
| artifact: openshrimp-linux-aarch64 | |
| sdist_artifact: sdist-openshrimp | |
| exec_code: "from open_shrimp.main import main; main()" | |
| features: "libvirt" | |
| - os: macos-latest | |
| artifact: openshrimp-macos-aarch64 | |
| sdist_artifact: sdist-openshrimp | |
| exec_code: "from open_shrimp.main import main; main()" | |
| - os: macos-latest | |
| artifact: openshrimp-macos-x86_64 | |
| sdist_artifact: sdist-openshrimp | |
| exec_code: "from open_shrimp.main import main; main()" | |
| rust_target: x86_64-apple-darwin | |
| # The `hcs` extra is what makes the Windows build worth shipping: | |
| # win32more, the binding the HCS sandbox backend drives Windows' | |
| # Host Compute Service through. It is Windows-only, so it appears | |
| # on no other entry. | |
| - os: windows-latest | |
| artifact: openshrimp-windows-x86_64.exe | |
| sdist_artifact: sdist-openshrimp | |
| exec_code: "from open_shrimp.main import main; main()" | |
| features: "hcs" | |
| # moonshine-stt | |
| - os: ubuntu-latest | |
| artifact: moonshine-stt-linux-x86_64 | |
| sdist_artifact: sdist-moonshine-stt | |
| exec_code: "from moonshine_stt.main import main; main()" | |
| - os: ubuntu-24.04-arm | |
| artifact: moonshine-stt-linux-aarch64 | |
| sdist_artifact: sdist-moonshine-stt | |
| exec_code: "from moonshine_stt.main import main; main()" | |
| - os: macos-latest | |
| artifact: moonshine-stt-macos-aarch64 | |
| sdist_artifact: sdist-moonshine-stt | |
| exec_code: "from moonshine_stt.main import main; main()" | |
| - os: macos-latest | |
| artifact: moonshine-stt-macos-x86_64 | |
| sdist_artifact: sdist-moonshine-stt | |
| exec_code: "from moonshine_stt.main import main; main()" | |
| rust_target: x86_64-apple-darwin | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download sdist | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ${{ matrix.sdist_artifact }} | |
| path: dist | |
| - name: Install libvirt development headers | |
| if: matrix.features && contains(matrix.features, 'libvirt') | |
| run: sudo apt-get update && sudo apt-get install -y libvirt-dev | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust_target || '' }} | |
| - name: Cache Cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| key: ${{ runner.os }}-cargo-pyapp-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo-pyapp- | |
| - name: Get sdist filename | |
| id: sdist | |
| run: | | |
| FILE=$(realpath dist/*.tar.gz) | |
| # PyApp is a native program: on Windows it cannot open the | |
| # `/d/a/...` path git-bash's realpath hands back. | |
| if [ "$RUNNER_OS" = "Windows" ]; then FILE=$(cygpath -w "$FILE"); fi | |
| echo "filename=$FILE" >> "$GITHUB_OUTPUT" | |
| - name: Build with PyApp | |
| env: | |
| PYAPP_PROJECT_PATH: ${{ steps.sdist.outputs.filename }} | |
| PYAPP_PROJECT_FEATURES: ${{ matrix.features || '' }} | |
| PYAPP_PYTHON_VERSION: "3.12" | |
| PYAPP_EXEC_CODE: ${{ matrix.exec_code }} | |
| PYAPP_DISTRIBUTION_EMBED: "1" | |
| PYAPP_PASS_LOCATION: "1" | |
| run: | | |
| if [ -n "${{ matrix.rust_target }}" ]; then | |
| cargo install pyapp --force --root dist/pyapp --target ${{ matrix.rust_target }} | |
| else | |
| cargo install pyapp --force --root dist/pyapp | |
| fi | |
| - name: Rename binary | |
| run: | | |
| SRC=dist/pyapp/bin/pyapp | |
| if [ -f "$SRC.exe" ]; then SRC="$SRC.exe"; fi | |
| mv "$SRC" "dist/${{ matrix.artifact }}" | |
| - name: Import signing certificate | |
| if: runner.os == 'macOS' | |
| env: | |
| CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| run: | | |
| KC="$RUNNER_TEMP/build.keychain-db" | |
| echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12" | |
| security create-keychain -p "$KC_PW" "$KC" | |
| security set-keychain-settings -lut 21600 "$KC" | |
| security unlock-keychain -p "$KC_PW" "$KC" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \ | |
| -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC" | |
| security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') | |
| rm "$RUNNER_TEMP/cert.p12" | |
| - name: Sign and notarize binary | |
| if: runner.os == 'macOS' | |
| env: | |
| IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} | |
| API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }} | |
| API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| set -e | |
| BIN="dist/${{ matrix.artifact }}" | |
| codesign --force --timestamp --options runtime \ | |
| --entitlements entitlements.plist \ | |
| --sign "$IDENTITY" "$BIN" | |
| codesign --verify --strict --verbose=2 "$BIN" | |
| KEY="$RUNNER_TEMP/notary_key.p8" | |
| echo -n "$API_KEY_B64" | base64 --decode -o "$KEY" | |
| ZIP="$RUNNER_TEMP/${{ matrix.artifact }}.zip" | |
| ditto -c -k --keepParent "$BIN" "$ZIP" | |
| set +e | |
| SUBMIT_OUT=$(xcrun notarytool submit "$ZIP" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \ | |
| --wait --timeout 30m 2>&1) | |
| SUBMIT_RC=$? | |
| echo "$SUBMIT_OUT" | |
| SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}') | |
| if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then | |
| if [ -n "$SUBMIT_ID" ]; then | |
| echo "::group::Notary log for $SUBMIT_ID" | |
| xcrun notarytool log "$SUBMIT_ID" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true | |
| echo "::endgroup::" | |
| fi | |
| rm -f "$ZIP" "$KEY" | |
| exit 1 | |
| fi | |
| # Single-file binaries can't be stapled; Gatekeeper checks online. | |
| rm "$ZIP" "$KEY" | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: dist/${{ matrix.artifact }} | |
| build-virtiofsd: | |
| name: Build virtiofsd (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| arch: x86_64 | |
| artifact: virtiofsd-linux-x86_64 | |
| - os: ubuntu-24.04-arm | |
| arch: aarch64 | |
| artifact: virtiofsd-linux-aarch64 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install system build dependencies | |
| run: sudo apt-get update && sudo apt-get install -y libseccomp-dev libcap-ng-dev | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo registry | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| key: ${{ runner.os }}-${{ runner.arch }}-cargo-virtiofsd-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-${{ runner.arch }}-cargo-virtiofsd- | |
| - name: Clone virtiofsd | |
| run: git clone --depth 1 --branch v1.13.3 https://gitlab.com/virtio-fs/virtiofsd.git | |
| - name: Apply OpenShrimp virtiofsd patch | |
| working-directory: virtiofsd | |
| run: git apply ../patches/virtiofsd-mountinfo-unescape.patch | |
| - name: Test OpenShrimp virtiofsd patch | |
| working-directory: virtiofsd | |
| run: cargo test mountinfo | |
| - name: Build virtiofsd | |
| working-directory: virtiofsd | |
| run: cargo build --release | |
| - name: Rename binary | |
| run: mv virtiofsd/target/release/virtiofsd ${{ matrix.artifact }} | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: ${{ matrix.artifact }} | |
| build-security-key-helper: | |
| name: Build security-key VM helper | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| - name: Build helper | |
| run: | | |
| CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ | |
| -trimpath \ | |
| -ldflags "-s -w" \ | |
| -o openshrimp-security-key-vm-helper-linux-x86_64 \ | |
| ./src/open_shrimp/security_key/vm_helper_go/main.go | |
| CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \ | |
| -trimpath \ | |
| -ldflags "-s -w" \ | |
| -o openshrimp-security-key-vm-helper-linux-aarch64 \ | |
| ./src/open_shrimp/security_key/vm_helper_go/main.go | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: security-key-vm-helper | |
| path: openshrimp-security-key-vm-helper-linux-* | |
| build-hcs-initrd: | |
| # The control initramfs the HCS sandbox backend boots its guests with: | |
| # busybox plus the static vsock control agent. Its build is Linux work | |
| # (gcc -static, mknod, cpio) even though a Windows host consumes it, so | |
| # the operator downloads this asset instead of standing up a WSL build. | |
| name: Build HCS control initramfs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Build initramfs | |
| run: | | |
| sudo PUBLISH=1 bash scripts/build_hcs_initrd.sh \ | |
| "$PWD/openshrimp-hcs-initrd.img" | |
| - name: Upload initramfs | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hcs-initrd | |
| path: | | |
| openshrimp-hcs-initrd.img | |
| openshrimp-hcs-initrd.img.sha256 | |
| build-hcs-base-rootfs: | |
| # The guest userland the HCS sandbox backend chroots into: an ext4 volume | |
| # labelled `clauderoot`, in VHDX form, which is what a context's | |
| # `base_image` points at. debootstrap, loop mounts and chroot are Linux | |
| # work even though a Windows host consumes the result, so the operator | |
| # downloads this asset instead of standing up a root shell in WSL to bake | |
| # one by hand. | |
| name: Build HCS base rootfs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Build base rootfs | |
| # The ext4 image, the VHDX converted from it and the copy at the | |
| # output path coexist, which the runner's guaranteed free space | |
| # absorbs. Both paths are on the OS disk: the /mnt temp disk is the | |
| # smaller of the two and cannot hold the build. | |
| run: | | |
| sudo PUBLISH=1 WORK="$RUNNER_TEMP/hcs-base" \ | |
| bash scripts/build_hcs_base_rootfs.sh \ | |
| "$PWD/openshrimp-hcs-base-rootfs.vhdx" | |
| - name: Check the image fits a release asset | |
| # A release rejects any file of 2 GiB or more. Failing here names the | |
| # image that outgrew the limit; failing at upload time would not. | |
| run: | | |
| asset=openshrimp-hcs-base-rootfs.vhdx.zst | |
| size=$(stat -c %s "$asset") | |
| echo "$asset: $((size / 1024 / 1024)) MiB" | |
| if [ "$size" -ge 2147483648 ]; then | |
| echo "::error::$asset is 2 GiB or larger; a release will reject it" | |
| exit 1 | |
| fi | |
| - name: Upload base rootfs | |
| # Only the compressed form travels. The raw VHDX is several times | |
| # larger, and both the release asset and the next job take the .zst. | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hcs-base-rootfs | |
| # The artifact only has to outlive the jobs that consume it — the | |
| # computer-use build and the release upload, both minutes later. | |
| # The published release asset is the copy that is kept. | |
| retention-days: 7 | |
| path: | | |
| openshrimp-hcs-base-rootfs.vhdx.zst | |
| openshrimp-hcs-base-rootfs.vhdx.zst.sha256 | |
| build-hcs-gui-rootfs: | |
| # The computer-use variant of the guest rootfs: the base image plus weston | |
| # on its RDP backend, the AF_VSOCK relay and a browser. A context with | |
| # computer use enabled boots this in place of the base image, so the two | |
| # ship together. It builds in its own job rather than after the base in | |
| # one: the images are large enough that each needs a whole runner disk. | |
| name: Build HCS computer-use rootfs | |
| runs-on: ubuntu-latest | |
| needs: build-hcs-base-rootfs | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Reclaim runner disk | |
| # The base image, the grown ext4 image holding the desktop stack, the | |
| # VHDX converted from it and the copy at the output path are all live | |
| # at once — several times what the base rootfs build needs, and close | |
| # enough to the free space a runner guarantees to be worth the margin. | |
| # None of the preinstalled language toolchains are used here. | |
| run: | | |
| sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \ | |
| /usr/local/.ghcup /opt/hostedtoolcache | |
| df -h / | |
| - name: Download base rootfs | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: hcs-base-rootfs | |
| - name: Unpack base rootfs | |
| # The GUI build bakes into a copy of the base image, so it consumes | |
| # exactly the artifact published for operators; verifying the checksum | |
| # here proves the published asset is the one that was built against. | |
| run: | | |
| sha256sum -c openshrimp-hcs-base-rootfs.vhdx.zst.sha256 | |
| zstd -d --rm openshrimp-hcs-base-rootfs.vhdx.zst | |
| rm -f openshrimp-hcs-base-rootfs.vhdx.zst.sha256 | |
| - name: Build computer-use rootfs | |
| run: | | |
| sudo PUBLISH=1 WORK="$RUNNER_TEMP/hcs-gui" \ | |
| bash scripts/build_hcs_gui_rootfs.sh \ | |
| "$PWD/openshrimp-hcs-base-rootfs.vhdx" \ | |
| "$PWD/openshrimp-hcs-gui-rootfs.vhdx" | |
| rm -f openshrimp-hcs-base-rootfs.vhdx | |
| - name: Check the image fits a release asset | |
| # The desktop stack makes this the largest asset the project ships; | |
| # a release rejects any file of 2 GiB or more. | |
| run: | | |
| asset=openshrimp-hcs-gui-rootfs.vhdx.zst | |
| size=$(stat -c %s "$asset") | |
| echo "$asset: $((size / 1024 / 1024)) MiB" | |
| if [ "$size" -ge 2147483648 ]; then | |
| echo "::error::$asset is 2 GiB or larger; a release will reject it" | |
| exit 1 | |
| fi | |
| - name: Upload computer-use rootfs | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hcs-gui-rootfs | |
| # Superseded by the published release asset once the release job | |
| # has collected it. | |
| retention-days: 7 | |
| path: | | |
| openshrimp-hcs-gui-rootfs.vhdx.zst | |
| openshrimp-hcs-gui-rootfs.vhdx.zst.sha256 | |
| build-hcs-rdp-helper: | |
| # The RDP helper for HCS computer use, shipped with every FreeRDP DLL it | |
| # loads. Building it here is what makes `mingw_bin` optional: an operator | |
| # without an MSYS2 toolchain gets this bundle, and the runtime falls back | |
| # to compiling from source only for a source install. | |
| name: Build HCS RDP helper | |
| runs-on: windows-latest | |
| defaults: | |
| run: | |
| shell: msys2 {0} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: MINGW64 | |
| update: true | |
| install: >- | |
| mingw-w64-x86_64-gcc | |
| mingw-w64-x86_64-freerdp | |
| mingw-w64-x86_64-pkgconf | |
| zip | |
| - name: Build helper | |
| # Same compile the runtime fallback performs (sandbox/hcs_rdp.py); | |
| # keep the flags in step with it. | |
| run: | | |
| mkdir -p bundle | |
| gcc src/open_shrimp/sandbox/hcs_rdp_helper.c \ | |
| -o bundle/hcs_rdp_helper.exe -D__STDC_NO_THREADS__ -O2 \ | |
| $(pkgconf --cflags --libs freerdp-client3 freerdp3 winpr3) \ | |
| -lws2_32 | |
| - name: Bundle the FreeRDP DLLs | |
| # ldd resolves the PE's imports transitively, so one pass collects the | |
| # whole closure. Only the /mingw64 ones travel; the rest are Windows' | |
| # own and are already on the target machine. | |
| run: | | |
| cd bundle | |
| ldd hcs_rdp_helper.exe | awk '/mingw64/ {print $3}' | sort -u | \ | |
| while read -r dll; do cp -n "$dll" .; done | |
| ls -l | |
| - name: Verify the bundle needs no toolchain | |
| shell: pwsh | |
| # The whole point of the bundle is that the DLLs beside the exe are | |
| # enough, so run it with MSYS2 off PATH. With a DLL missing the | |
| # loader kills the process (exit -1073741515) before it prints | |
| # anything; with the bundle complete, argc < 3 prints usage and | |
| # exits 2. | |
| run: | | |
| $env:PATH = "$env:SystemRoot\system32;$env:SystemRoot" | |
| $out = & .\bundle\hcs_rdp_helper.exe 2>&1 | Out-String | |
| Write-Host $out | |
| if ($LASTEXITCODE -ne 2 -or $out -notmatch 'usage:') { | |
| throw "RDP helper bundle is not self-contained (exit $LASTEXITCODE)" | |
| } | |
| - name: Package the bundle | |
| # The runtime unpacks this archive whole and expects the exe at its | |
| # top level, next to the DLLs (sandbox/hcs_rdp.py: HELPER_ASSET). | |
| run: | | |
| cd bundle | |
| zip -r ../openshrimp-hcs-rdp-helper-windows-x86_64.zip . | |
| - name: Upload bundle | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: hcs-rdp-helper | |
| path: openshrimp-hcs-rdp-helper-windows-x86_64.zip | |
| build-limactl: | |
| # Builds the bundled `limactl` from upstream Lima at the version | |
| # pinned by `patches/PIN`, with `patches/lima-vznc-display.patch` | |
| # applied. The patch attaches Apple's private `_VZVNCServer` SPI to | |
| # the running `VZVirtualMachine` so OpenShrimp can scrape the | |
| # framebuffer without a `limactl` GUI window. Output tarball name and | |
| # layout match upstream Lima releases so the runtime extractor in | |
| # `lima_helpers.py:_download_lima_sync` works unchanged. | |
| name: Build limactl (${{ matrix.uname_m }}) | |
| runs-on: macos-26 | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goarch: arm64 | |
| uname_m: arm64 | |
| - goarch: amd64 | |
| uname_m: x86_64 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Read pinned LIMA_VERSION | |
| id: pin | |
| run: | | |
| PIN_VER=$(awk -F': *' '$1 == "tag" {sub(/^v/, "", $2); print $2}' patches/PIN) | |
| if [ -z "$PIN_VER" ]; then | |
| echo "::error::no 'tag:' line in patches/PIN" | |
| exit 1 | |
| fi | |
| PY_VER=$(awk -F'"' '/^LIMA_VERSION/ {print $2}' \ | |
| src/open_shrimp/sandbox/lima_helpers.py) | |
| if [ "$PIN_VER" != "$PY_VER" ]; then | |
| echo "::error::LIMA_VERSION skew: patches/PIN says v$PIN_VER but lima_helpers.py says $PY_VER" | |
| exit 1 | |
| fi | |
| echo "version=$PIN_VER" >> "$GITHUB_OUTPUT" | |
| - name: Clone Lima at v${{ steps.pin.outputs.version }} | |
| run: | | |
| git clone --depth 1 \ | |
| --branch v${{ steps.pin.outputs.version }} \ | |
| https://github.com/lima-vm/lima.git lima-src | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| - name: Cache Go modules | |
| # Must run after `git clone lima-src` so hashFiles('lima-src/go.sum') | |
| # actually hashes a file — otherwise the cache key would not | |
| # invalidate on a Lima version bump. | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/Library/Caches/go-build | |
| key: ${{ runner.os }}-${{ matrix.goarch }}-go-limactl-${{ hashFiles('lima-src/go.sum', 'patches/lima-vznc-display.patch', 'vznc/go.mod') }} | |
| restore-keys: ${{ runner.os }}-${{ matrix.goarch }}-go-limactl- | |
| - name: Apply OpenShrimp patch | |
| run: | | |
| cd lima-src | |
| git apply ../patches/lima-vznc-display.patch | |
| # The patch's `replace` line uses `../../vznc`, valid for the dev | |
| # checkout layout but not for the CI workspace. Rewrite it. | |
| go mod edit -replace=github.com/openshrimp/vznc=${GITHUB_WORKSPACE}/vznc | |
| - name: Build binaries (${{ matrix.goarch }}) | |
| working-directory: lima-src | |
| env: | |
| GOOS: darwin | |
| GOARCH: ${{ matrix.goarch }} | |
| CGO_ENABLED: "1" | |
| # Lima's Makefile pulls VERSION from `git describe`. The shallow | |
| # clone above drops tag annotations, so pass it explicitly. | |
| run: make VERSION=v${{ steps.pin.outputs.version }} binaries | |
| - name: Build manpages (host arch only) | |
| # Lima's manpage build runs the just-built limactl binary, so it | |
| # only works on the host arch (macos-26 = arm64). The amd64 | |
| # tarball ships without manpages — the runtime extractor doesn't | |
| # require them. | |
| if: matrix.goarch == 'arm64' | |
| working-directory: lima-src | |
| env: | |
| GOOS: darwin | |
| GOARCH: ${{ matrix.goarch }} | |
| CGO_ENABLED: "1" | |
| run: make VERSION=v${{ steps.pin.outputs.version }} manpages | |
| - name: Verify Code-Hex/vz field-layout test | |
| # Native arch only: the cross-built amd64 binary can't execute | |
| # on the arm64 host. The test reads struct layout via reflect, | |
| # so running it natively is sufficient coverage. | |
| if: matrix.goarch == 'arm64' | |
| working-directory: lima-src | |
| run: go test -run TestVZVMReflectedFields ./pkg/driver/vz/... | |
| - name: Import signing certificate | |
| env: | |
| CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| run: | | |
| KC="$RUNNER_TEMP/build.keychain-db" | |
| echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12" | |
| security create-keychain -p "$KC_PW" "$KC" | |
| security set-keychain-settings -lut 21600 "$KC" | |
| security unlock-keychain -p "$KC_PW" "$KC" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \ | |
| -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC" | |
| security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') | |
| rm "$RUNNER_TEMP/cert.p12" | |
| - name: Sign and notarize limactl | |
| env: | |
| IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} | |
| API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }} | |
| API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| BIN="lima-src/_output/bin/limactl" | |
| codesign --force --timestamp --options runtime \ | |
| --entitlements vznc/vznc.entitlements \ | |
| --sign "$IDENTITY" "$BIN" | |
| codesign --verify --strict --verbose=2 "$BIN" | |
| KEY="$RUNNER_TEMP/notary_key.p8" | |
| echo -n "$API_KEY_B64" | base64 --decode -o "$KEY" | |
| ZIP="$RUNNER_TEMP/limactl-${{ matrix.uname_m }}.zip" | |
| ditto -c -k --keepParent "$BIN" "$ZIP" | |
| set +e | |
| SUBMIT_OUT=$(xcrun notarytool submit "$ZIP" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \ | |
| --wait --timeout 30m 2>&1) | |
| SUBMIT_RC=$? | |
| echo "$SUBMIT_OUT" | |
| SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}') | |
| if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then | |
| if [ -n "$SUBMIT_ID" ]; then | |
| echo "::group::Notary log for $SUBMIT_ID" | |
| xcrun notarytool log "$SUBMIT_ID" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true | |
| echo "::endgroup::" | |
| fi | |
| rm -f "$ZIP" "$KEY" | |
| exit 1 | |
| fi | |
| # Single-file binaries can't be stapled; Gatekeeper checks online. | |
| rm "$ZIP" "$KEY" | |
| - name: Build tarball | |
| working-directory: lima-src | |
| run: | | |
| mkdir -p _artifacts | |
| OUT="_artifacts/lima-${{ steps.pin.outputs.version }}-Darwin-${{ matrix.uname_m }}.tar.gz" | |
| tar -C _output --no-xattrs -czvf "$OUT" ./ | |
| - name: Smoke-test the binary | |
| # Native arch only — confirms codesigning didn't break the | |
| # binary and the entitlement load doesn't reject the helper. | |
| if: matrix.goarch == 'arm64' | |
| run: lima-src/_output/bin/limactl --version | |
| - name: Upload tarball | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: lima-darwin-${{ matrix.uname_m }} | |
| path: lima-src/_artifacts/lima-${{ steps.pin.outputs.version }}-Darwin-${{ matrix.uname_m }}.tar.gz | |
| build-macos-app: | |
| name: Build macOS .app | |
| needs: build-sdist | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Build review app frontend | |
| working-directory: web/review-app | |
| run: npm ci && npm run build | |
| - name: Build terminal app frontend | |
| working-directory: web/terminal-app | |
| run: npm ci && npm run build | |
| - name: Build markdown preview app frontend | |
| working-directory: web/markdown-app | |
| run: npm ci && npm run build | |
| - name: Build VNC app frontend | |
| working-directory: web/vnc-app | |
| run: npm ci && npm run build | |
| - name: Build config app frontend | |
| working-directory: web/config-app | |
| run: npm ci && npm run build | |
| - name: Build PDF app frontend | |
| working-directory: web/pdf-app | |
| run: npm ci && npm run build | |
| - name: Install dependencies | |
| run: | | |
| uv pip install --system py2app setuptools | |
| uv pip install --system '.[macos]' | |
| - name: Build .app bundle | |
| run: | | |
| # ruamel is a PEP 420 namespace package (no __init__.py), which | |
| # py2app's imp.find_module cannot resolve. Add a stub so py2app | |
| # can locate the package. | |
| touch "$(python -c 'import ruamel; print(ruamel.__path__[0])')/__init__.py" | |
| mv pyproject.toml pyproject.toml.bak | |
| python setup_app.py py2app | |
| mv pyproject.toml.bak pyproject.toml | |
| - name: Import signing certificate | |
| env: | |
| CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| run: | | |
| KC="$RUNNER_TEMP/build.keychain-db" | |
| echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12" | |
| security create-keychain -p "$KC_PW" "$KC" | |
| security set-keychain-settings -lut 21600 "$KC" | |
| security unlock-keychain -p "$KC_PW" "$KC" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \ | |
| -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC" | |
| security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') | |
| rm "$RUNNER_TEMP/cert.p12" | |
| - name: Sign .app bundle | |
| env: | |
| IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} | |
| run: | | |
| set -e | |
| APP="dist/OpenShrimp.app" | |
| # Sign every Mach-O file inside the bundle. Extension-based matching | |
| # isn't enough for py2app (the embedded Python interpreter has no | |
| # suffix), so probe each file with `file`. | |
| find "$APP" -type f | while read -r f; do | |
| if file -b "$f" | grep -qE 'Mach-O|dynamically linked shared library'; then | |
| codesign --force --timestamp --options runtime \ | |
| --sign "$IDENTITY" "$f" | |
| fi | |
| done | |
| # Sign embedded bundles (frameworks, nested .app) deepest-first so | |
| # each enclosing CodeResources seal covers already-signed contents. | |
| find "$APP/Contents" -type d \( -name "*.framework" -o -name "*.app" \) | \ | |
| awk -F/ '{ print NF, $0 }' | sort -k1 -rn | cut -d' ' -f2- | \ | |
| while read -r b; do | |
| codesign --force --timestamp --options runtime \ | |
| --sign "$IDENTITY" "$b" | |
| done | |
| # Finally sign the outer bundle with entitlements. | |
| codesign --force --timestamp --options runtime \ | |
| --entitlements entitlements.plist \ | |
| --sign "$IDENTITY" "$APP" | |
| codesign --verify --strict --deep --verbose=2 "$APP" | |
| - name: Create DMG | |
| run: | | |
| brew install create-dmg | |
| create-dmg \ | |
| --volname "OpenShrimp" \ | |
| --window-size 600 400 \ | |
| --icon "OpenShrimp.app" 150 200 \ | |
| --app-drop-link 450 200 \ | |
| --no-internet-enable \ | |
| "dist/OpenShrimp-$(cat VERSION).dmg" \ | |
| "dist/OpenShrimp.app" | |
| - name: Sign, notarize, and staple DMG | |
| env: | |
| IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} | |
| API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }} | |
| API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| set -e | |
| DMG="dist/OpenShrimp-$(cat VERSION).dmg" | |
| codesign --force --timestamp --sign "$IDENTITY" "$DMG" | |
| KEY="$RUNNER_TEMP/notary_key.p8" | |
| echo -n "$API_KEY_B64" | base64 --decode -o "$KEY" | |
| set +e | |
| SUBMIT_OUT=$(xcrun notarytool submit "$DMG" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \ | |
| --wait --timeout 30m 2>&1) | |
| SUBMIT_RC=$? | |
| echo "$SUBMIT_OUT" | |
| SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}') | |
| if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then | |
| if [ -n "$SUBMIT_ID" ]; then | |
| echo "::group::Notary log for $SUBMIT_ID" | |
| xcrun notarytool log "$SUBMIT_ID" \ | |
| --key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true | |
| echo "::endgroup::" | |
| fi | |
| rm -f "$KEY" | |
| exit 1 | |
| fi | |
| rm "$KEY" | |
| xcrun stapler staple "$DMG" | |
| xcrun stapler validate "$DMG" | |
| - name: Upload DMG | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: OpenShrimp-macos-app | |
| path: dist/OpenShrimp-*.dmg | |
| release: | |
| name: Create GitHub Release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: | |
| - build-binary | |
| - build-virtiofsd | |
| - build-security-key-helper | |
| - build-hcs-initrd | |
| - build-hcs-base-rootfs | |
| - build-hcs-gui-rootfs | |
| - build-hcs-rdp-helper | |
| - build-limactl | |
| - build-macos-app | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Create release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| generate_release_notes: true | |
| files: | | |
| artifacts/openshrimp-* | |
| artifacts/moonshine-stt-* | |
| artifacts/openshrimp-security-key-vm-helper-* | |
| artifacts/openshrimp-hcs-initrd.img* | |
| artifacts/openshrimp-hcs-base-rootfs.vhdx.zst* | |
| artifacts/openshrimp-hcs-gui-rootfs.vhdx.zst* | |
| artifacts/openshrimp-hcs-rdp-helper-* | |
| artifacts/virtiofsd-* | |
| artifacts/OpenShrimp-*.dmg | |
| artifacts/*.tar.gz |