Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
self-hosted-runner:
# Depot-managed GitHub Actions runners (https://depot.dev/docs/github-actions/runner-types)
labels:
- depot-macos-26
- depot-macos-latest
- depot-ubuntu-latest
- depot-ubuntu-24.04-arm
50 changes: 49 additions & 1 deletion .github/workflows/release-binary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ on:
push:
branches:
- main
tags:
- "v*"
pull_request:
workflow_dispatch:

permissions:
contents: write

jobs:
build:
runs-on: macos-26
env:
# Set when the Developer ID secrets are configured; release binaries are
# then notarized instead of ad-hoc signed.
HAVE_SIGNING: ${{ secrets.MACOS_SIGN_P12 != '' }}
steps:
- uses: actions/checkout@v6

Expand All @@ -35,7 +44,35 @@ jobs:
CC_LINUX=/opt/homebrew/bin/aarch64-linux-musl-gcc \
cargo build --release --target aarch64-unknown-linux-musl

- name: Codesign release binary
- name: Import Developer ID certificate
if: env.HAVE_SIGNING == 'true'
env:
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_P12_PASSWORD: ${{ secrets.MACOS_SIGN_P12_PASSWORD }}
run: |
keychain="$RUNNER_TEMP/signing.keychain-db"
keychain_password="$(uuidgen)"
security create-keychain -p "$keychain_password" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
echo "$MACOS_SIGN_P12" | base64 --decode > "$RUNNER_TEMP/signing.p12"
security import "$RUNNER_TEMP/signing.p12" -k "$keychain" \
-P "$MACOS_SIGN_P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$keychain_password" "$keychain"
security list-keychains -d user -s "$keychain" login.keychain

- name: Sign and notarize release binary
if: env.HAVE_SIGNING == 'true'
env:
SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
run: scripts/sign-notarize.sh target/release/lnx

- name: Codesign release binary (ad-hoc fallback)
if: env.HAVE_SIGNING != 'true'
run: codesign --entitlements entitlements.plist --force -s - target/release/lnx

- name: Package artifact
Expand All @@ -53,3 +90,14 @@ jobs:
path: |
lnx-macos-arm64.tar.gz
lnx-macos-arm64.tar.gz.sha256

- name: Attach binary to release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release view "$tag" >/dev/null 2>&1 || \
gh release create "$tag" --title "lnx $tag" --generate-notes
gh release upload "$tag" --clobber \
lnx-macos-arm64.tar.gz lnx-macos-arm64.tar.gz.sha256
57 changes: 46 additions & 11 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,55 @@ on:
workflow_dispatch:

jobs:
test:
# Unit tests do not boot VMs, so they run fine on GitHub-hosted runners.
unit:
runs-on: macos-26
timeout-minutes: 60
env:
CC_LINUX: /opt/homebrew/bin/aarch64-linux-musl-gcc
steps:
- uses: actions/checkout@v6

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-musl

- name: Set up Bun
uses: oven-sh/setup-bun@v2

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: third_party/gvproxy-bridge/go.mod

- name: Install host dependencies
run: brew install FiloSottile/musl-cross/musl-cross llvm

- name: Rust formatting
run: cargo fmt --check

- name: Rust tests
run: bun run test

# The system suite boots VMs, which needs Hypervisor.framework on bare-metal
# macOS. No hosted CI provides that (macOS VMs cannot nest HVF), so this job
# targets a self-hosted Mac and only runs on manual dispatch until one is
# registered.
system:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 180
env:
CC_LINUX: /opt/homebrew/bin/aarch64-linux-musl-gcc
LNX_BASE: ${{ runner.temp }}/lnx-state
LNX_RUN_BROWSER_TEST: "1"
LNX_RUN_PRIVILEGED_INGRESS_TEST: "1"
steps:
- uses: actions/checkout@v6

- name: Set up Depot CLI
uses: depot/setup-action@v1
# The runner context is not available in job-level env.
- name: Set LNX_BASE
run: echo "LNX_BASE=$RUNNER_TEMP/lnx-state" >> "$GITHUB_ENV"

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -30,16 +66,15 @@ jobs:
- name: Set up Bun
uses: oven-sh/setup-bun@v2

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: third_party/gvproxy-bridge/go.mod

- name: Install host dependencies
run: |
brew install FiloSottile/musl-cross/musl-cross
brew install e2fsprogs zstd

- name: Rust formatting
run: cargo fmt --check

- name: Rust tests
run: cargo test
brew install e2fsprogs zstd podman llvm

- name: Full system tests
run: bun run test:full
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

# Test artifacts
/tmp/
/results/**/*.json
/results/
/.lnx-nested-debug/
/.lnx-nested-kvm*/
/.lnx-chaos/
Expand Down
12 changes: 0 additions & 12 deletions .gitmodules

This file was deleted.

53 changes: 53 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Contributing

Thanks for your interest in lnx!

## Prerequisites

- Apple Silicon Mac
- Rust (stable, 1.85+) with the `aarch64-unknown-linux-musl` target
- [Bun](https://bun.sh) for the build/test scripts
- `brew install FiloSottile/musl-cross/musl-cross podman llvm`

## Building

```sh
bun run build # debug build + codesign
bun run release # release build + codesign
```

Always build through the Bun scripts rather than raw `cargo build`: the
hypervisor entitlement requires the binary to be codesigned, and the scripts
handle that (an unsigned binary fails with `HV_DENIED`). They also build the
Linux helper binary used for nested runs.

## Testing

```sh
bun run test # Rust unit tests
bun run test:system # core integration suite
bun run test:full # everything CI runs
```

See [docs/testing.md](docs/testing.md) for the full suite list and opt-in
tests. Guest kernel and rootfs images are downloaded automatically
(`lnx init --global`); building them from source is only needed when changing
`kernel.config`, `kernel-patches/`, or the Dockerfiles.

Tests must encode intended correct behavior — do not add tests that pass
because a known bug reproduces.

## Layout

- `src/` — host CLI and VM runner (Rust, libkrun)
- `guest-agent/` — static Linux agent, PID-1 staging and exec service
- `lnx-protocol/` — host/guest wire protocol
- `third_party/libkrun` — vendored libkrun with snapshot/restore patches
- `scripts/test/` — integration suites (Bun/TypeScript)
- `docs/` — architecture, security, testing notes

## Pull requests

- Keep changes focused; include tests that prove the new behavior.
- `cargo fmt` before pushing; CI checks formatting.
- Licensing is Apache-2.0; contributions are accepted under the same terms.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name = "lnx"
version = "0.3.0"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
repository = "https://github.com/semistrict/lnx"
description = "Linux VM runner for macOS with warm memory/disk snapshots between commands"

[workspace]
members = [
Expand Down
29 changes: 29 additions & 0 deletions Formula/lnx.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Homebrew formula for the prebuilt lnx binary.
#
# The repo doubles as a tap:
# brew tap semistrict/lnx https://github.com/semistrict/lnx
# brew install semistrict/lnx/lnx
#
# When tagging a release, update `version` and `sha256` below to match the
# lnx-macos-arm64.tar.gz asset produced by .github/workflows/release-binary.yml
# (the .sha256 file is published next to it).
class Lnx < Formula
desc "Linux VMs on macOS that resume with memory and disk state intact"
homepage "https://github.com/semistrict/lnx"
version "0.3.0"
url "https://github.com/semistrict/lnx/releases/download/v#{version}/lnx-macos-arm64.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000" # TODO: set from the release .sha256 asset
license "Apache-2.0"

depends_on :macos
depends_on arch: :arm64
depends_on "podman" # provides gvproxy for guest networking

def install
bin.install "lnx"
end

test do
assert_match "Linux VM runner", shell_output("#{bin}/lnx --help")
end
end
Loading
Loading