Skip to content

Commit b39a536

Browse files
Merge branch 'main' into onboarding
2 parents fcd610d + 72f88e7 commit b39a536

8 files changed

Lines changed: 101 additions & 54 deletions

File tree

.github/workflows/release.yaml

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,27 @@ permissions:
1111

1212
jobs:
1313
build-assets:
14-
runs-on: ubuntu-latest
14+
runs-on: ubuntu-24.04-arm # Native arm64 -- no QEMU, ~5x faster Docker builds
1515
steps:
1616
- uses: actions/checkout@v4
17-
- uses: docker/setup-qemu-action@v3
17+
- uses: docker/setup-buildx-action@v3
18+
19+
- uses: dtolnay/rust-toolchain@stable
20+
with:
21+
targets: aarch64-unknown-linux-musl
22+
components: llvm-tools
23+
24+
- uses: Swatinem/rust-cache@v2
25+
with:
26+
cache-targets: false
27+
key: build-assets
28+
29+
- name: Install b3sum
30+
run: cargo install b3sum --locked
31+
1832
- name: Build VM assets
1933
run: python3 images/build.py
34+
2035
- uses: actions/upload-artifact@v4
2136
with:
2237
name: vm-assets
@@ -33,6 +48,10 @@ jobs:
3348
path: assets/
3449

3550
- uses: dtolnay/rust-toolchain@stable
51+
- uses: Swatinem/rust-cache@v2
52+
with:
53+
key: build-app
54+
3655
- uses: pnpm/action-setup@v4
3756
with:
3857
version: 9
@@ -44,7 +63,7 @@ jobs:
4463
- run: cd frontend && pnpm install --frozen-lockfile
4564

4665
- name: Install cargo-auditable and cargo-sbom
47-
run: cargo install cargo-auditable cargo-sbom
66+
run: cargo install cargo-auditable cargo-sbom --locked
4867

4968
- name: Import Apple certificate
5069
if: env.APPLE_CERTIFICATE != ''

crates/capsem-app/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn main() {
1818
match filename {
1919
"vmlinuz" => println!("cargo:rustc-env=VMLINUZ_HASH={}", hash),
2020
"initrd.img" => println!("cargo:rustc-env=INITRD_HASH={}", hash),
21-
"rootfs.img" | "rootfs.squashfs" => {
21+
"rootfs.squashfs" => {
2222
println!("cargo:rustc-env=ROOTFS_HASH={}", hash);
2323
println!("cargo:rustc-env=ROOTFS_FILENAME={}", filename);
2424
}

crates/capsem-app/src/main.rs

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,15 @@ fn resolve_assets_dir() -> Result<PathBuf> {
9696
}
9797

9898
/// Resolve rootfs path, checking bundled assets first, then ~/.capsem/assets/.
99-
/// Supports both squashfs (new) and raw img (legacy) formats.
10099
fn resolve_rootfs(bundled_assets: &Path) -> Option<PathBuf> {
101-
for name in &["rootfs.squashfs", "rootfs.img"] {
102-
let bundled = bundled_assets.join(name);
103-
if bundled.exists() {
104-
return Some(bundled);
105-
}
100+
let bundled = bundled_assets.join("rootfs.squashfs");
101+
if bundled.exists() {
102+
return Some(bundled);
106103
}
107104
if let Some(download_dir) = asset_manager::default_assets_dir() {
108-
for name in &["rootfs.squashfs", "rootfs.img"] {
109-
let downloaded = download_dir.join(name);
110-
if downloaded.exists() {
111-
return Some(downloaded);
112-
}
105+
let downloaded = download_dir.join("rootfs.squashfs");
106+
if downloaded.exists() {
107+
return Some(downloaded);
113108
}
114109
}
115110
None
@@ -127,7 +122,7 @@ fn create_asset_manager(bundled_assets: &Path) -> Result<AssetManager> {
127122
AssetManager::new(download_dir, base_url, &b3sums_content)
128123
}
129124

130-
/// Find the rootfs filename in the manifest (e.g. "rootfs.squashfs" or "rootfs.img").
125+
/// Find the rootfs filename in the manifest.
131126
fn rootfs_manifest_name(mgr: &AssetManager) -> Result<String> {
132127
mgr.manifest_filenames()
133128
.into_iter()
@@ -540,9 +535,8 @@ fn boot_vm(
540535
let rootfs_path = rootfs_override
541536
.map(|p| p.to_path_buf())
542537
.or_else(|| {
543-
["rootfs.squashfs", "rootfs.img"].iter()
544-
.map(|n| assets.join(n))
545-
.find(|p| p.exists())
538+
Some(assets.join("rootfs.squashfs"))
539+
.filter(|p| p.exists())
546540
});
547541

548542
if let Some(ref rootfs) = rootfs_path {

crates/capsem-core/src/asset_manager.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ mod tests {
372372
const SAMPLE_B3SUMS: &str = "\
373373
a65f925ebe0b0cc76afe0fe4945431473cb1a32c4f47a9e9b1592e92c46c829c vmlinuz
374374
cba052ee1e3fc7de5bb1af0da9f4a6472622b24788051f0e4d4ae6eabb0c3456 initrd.img
375-
b8199dc4a83069b99f41e1eb3829992d12777d09e2ce8295276f9d3a1abb1eee rootfs.img
375+
b8199dc4a83069b99f41e1eb3829992d12777d09e2ce8295276f9d3a1abb1eee rootfs.squashfs
376376
";
377377

378378
// ---- parse_b3sums tests ----
@@ -387,7 +387,7 @@ b8199dc4a83069b99f41e1eb3829992d12777d09e2ce8295276f9d3a1abb1eee rootfs.img
387387
"a65f925ebe0b0cc76afe0fe4945431473cb1a32c4f47a9e9b1592e92c46c829c"
388388
);
389389
assert_eq!(entries[1].filename, "initrd.img");
390-
assert_eq!(entries[2].filename, "rootfs.img");
390+
assert_eq!(entries[2].filename, "rootfs.squashfs");
391391
}
392392

393393
#[test]
@@ -466,7 +466,7 @@ b8199dc4a83069b99f41e1eb3829992d12777d09e2ce8295276f9d3a1abb1eee rootfs.img
466466
)
467467
.unwrap();
468468
assert_eq!(mgr.manifest.len(), 3);
469-
assert_eq!(mgr.manifest_filenames(), vec!["vmlinuz", "initrd.img", "rootfs.img"]);
469+
assert_eq!(mgr.manifest_filenames(), vec!["vmlinuz", "initrd.img", "rootfs.squashfs"]);
470470
}
471471

472472
#[test]

crates/capsem-core/src/vm/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ mod tests {
436436
let kernel = temp_file("vmlinuz-disk-bad");
437437
let err = VmConfig::builder()
438438
.kernel_path(&kernel)
439-
.disk_path("/nonexistent/rootfs.img")
439+
.disk_path("/nonexistent/rootfs.squashfs")
440440
.build();
441441
assert!(matches!(err, Err(ConfigError::MissingDisk(_))));
442442
}

crates/capsem-core/tests/vm_integration.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ fn make_config(assets: &std::path::Path) -> VmConfig {
3232
if assets.join("initrd.img").exists() {
3333
builder = builder.initrd_path(assets.join("initrd.img"));
3434
}
35-
if assets.join("rootfs.img").exists() {
36-
builder = builder.disk_path(assets.join("rootfs.img"));
35+
if assets.join("rootfs.squashfs").exists() {
36+
builder = builder.disk_path(assets.join("rootfs.squashfs"));
3737
}
3838

3939
builder.build().expect("VmConfig should be valid with real assets")

images/build.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#!/usr/bin/env python3
2-
"""Build VM boot assets using Podman.
2+
"""Build VM boot assets using Podman/Docker.
33
44
Extracts vmlinuz + initrd from Debian ARM64, builds a squashfs rootfs
55
(zstd-compressed) with developer tools and AI CLIs pre-installed.
66
Output goes to ../assets/.
77
"""
88

9-
import hashlib
9+
import os
1010
import shutil
1111
import subprocess
1212
import sys
@@ -22,22 +22,42 @@
2222
# Use podman, fall back to docker
2323
RUNTIME = "podman" if shutil.which("podman") else "docker"
2424

25+
# In GitHub Actions with docker, use buildx + GHA cache for faster rebuilds.
26+
CI = bool(os.environ.get("GITHUB_ACTIONS"))
27+
2528

2629
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
2730
print(f" -> {' '.join(cmd)}")
2831
return subprocess.run(cmd, check=True, **kwargs)
2932

3033

34+
def _docker_build(tag: str, dockerfile: str, context: str):
35+
"""Build a container image, using BuildKit GHA cache in CI."""
36+
if CI and RUNTIME == "docker":
37+
run([
38+
"docker", "buildx", "build",
39+
"--platform", "linux/arm64",
40+
"--cache-from", "type=gha,scope=" + tag,
41+
"--cache-to", "type=gha,mode=max,scope=" + tag,
42+
"--load",
43+
"-t", tag,
44+
"-f", dockerfile,
45+
context,
46+
])
47+
else:
48+
run([
49+
RUNTIME, "build",
50+
"--platform", "linux/arm64",
51+
"-t", tag,
52+
"-f", dockerfile,
53+
context,
54+
])
55+
56+
3157
def build_kernel_image():
3258
"""Build the container image that extracts kernel + initrd."""
3359
print(f"Building kernel extraction image with {RUNTIME}...")
34-
run([
35-
RUNTIME, "build",
36-
"--platform", "linux/arm64",
37-
"-t", IMAGE_TAG,
38-
"-f", str(SCRIPT_DIR / "Dockerfile.kernel"),
39-
str(SCRIPT_DIR),
40-
])
60+
_docker_build(IMAGE_TAG, str(SCRIPT_DIR / "Dockerfile.kernel"), str(SCRIPT_DIR))
4161

4262

4363
def extract_assets():
@@ -87,12 +107,11 @@ def build_agent():
87107
], cwd=str(REPO_ROOT))
88108

89109
# Copy binaries to images/ so Dockerfile.rootfs can COPY them.
90-
import shutil as _shutil
91110
release_dir = REPO_ROOT / "target" / "aarch64-unknown-linux-musl" / "release"
92111
for binary_name in ["capsem-pty-agent", "capsem-net-proxy"]:
93112
src = release_dir / binary_name
94113
dst = SCRIPT_DIR / binary_name
95-
_shutil.copy2(str(src), str(dst))
114+
shutil.copy2(str(src), str(dst))
96115
print(f" {binary_name}: {dst} ({dst.stat().st_size} bytes)")
97116

98117

@@ -107,13 +126,7 @@ def create_rootfs():
107126
print(f" capsem-ca.crt: {ca_dst}")
108127

109128
# 1. Build rootfs container (arm64 binaries)
110-
run([
111-
RUNTIME, "build",
112-
"--platform", "linux/arm64",
113-
"-t", ROOTFS_IMAGE_TAG,
114-
"-f", str(SCRIPT_DIR / "Dockerfile.rootfs"),
115-
str(SCRIPT_DIR),
116-
])
129+
_docker_build(ROOTFS_IMAGE_TAG, str(SCRIPT_DIR / "Dockerfile.rootfs"), str(SCRIPT_DIR))
117130

118131
# 2. Export container filesystem as tar
119132
print("Exporting rootfs filesystem...")
@@ -129,7 +142,7 @@ def create_rootfs():
129142
finally:
130143
run([RUNTIME, "rm", cid])
131144

132-
# 3. Create squashfs image from tar (zstd level 19 for best compression)
145+
# 3. Create squashfs image from tar (zstd level 15 for good compression)
133146
print("Creating squashfs rootfs image (zstd compression)...")
134147
abs_assets = str(ASSETS_DIR.resolve())
135148
run([
@@ -138,19 +151,19 @@ def create_rootfs():
138151
"debian:bookworm-slim", "bash", "-c",
139152
"apt-get update && apt-get install -y squashfs-tools zstd && "
140153
"mkdir /rootfs && tar xf /assets/rootfs.tar -C /rootfs && "
141-
"mksquashfs /rootfs /assets/rootfs.img -comp zstd -Xcompression-level 15 -b 64K -noappend",
154+
"mksquashfs /rootfs /assets/rootfs.squashfs -comp zstd -Xcompression-level 15 -b 64K -noappend",
142155
])
143156

144157
# 4. Cleanup tar
145158
tar_path.unlink()
146159

147-
img_path = ASSETS_DIR / "rootfs.img"
148-
print(f" rootfs.img: {img_path} ({img_path.stat().st_size // (1024*1024)} MB)")
160+
img_path = ASSETS_DIR / "rootfs.squashfs"
161+
print(f" rootfs.squashfs: {img_path} ({img_path.stat().st_size // (1024*1024)} MB)")
149162

150163

151164
def generate_checksums():
152165
print("Generating BLAKE3 checksums...")
153-
files = [f for f in ["vmlinuz", "initrd.img", "rootfs.img"]
166+
files = [f for f in ["vmlinuz", "initrd.img", "rootfs.squashfs"]
154167
if (ASSETS_DIR / f).exists()]
155168
result = subprocess.run(
156169
["b3sum"] + files,
@@ -166,6 +179,8 @@ def generate_checksums():
166179

167180
def main():
168181
print(f"Using container runtime: {RUNTIME}")
182+
if CI:
183+
print(" CI mode: Docker BuildKit GHA cache enabled")
169184
build_kernel_image()
170185
extract_assets()
171186
build_agent()

justfile

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,28 +126,47 @@ update-prices:
126126
_ensure-tools:
127127
#!/bin/bash
128128
set -euo pipefail
129+
err=0
130+
# Container runtime (Docker or Podman) -- needed for build-assets
131+
if ! command -v docker &>/dev/null && ! command -v podman &>/dev/null; then
132+
echo "ERROR: docker or podman required (for VM image builds)"
133+
err=1
134+
fi
135+
# Musl target for cross-compiling guest binaries
136+
if ! rustup target list --installed | grep -q aarch64-unknown-linux-musl; then
137+
echo "Installing aarch64-unknown-linux-musl target..."
138+
rustup target add aarch64-unknown-linux-musl
139+
fi
140+
# rust-lld linker (from llvm-tools component) -- needed for musl linking
141+
if ! rustup component list --installed | grep -q llvm-tools; then
142+
echo "Installing llvm-tools (provides rust-lld)..."
143+
rustup component add llvm-tools
144+
fi
145+
# cargo-llvm-cov for coverage
129146
if ! command -v cargo-llvm-cov &>/dev/null; then
130147
echo "Installing cargo-llvm-cov..."
131148
cargo install cargo-llvm-cov
132149
fi
133-
if ! rustup component list --installed | grep -q llvm-tools; then
134-
echo "Installing llvm-tools-preview..."
135-
rustup component add llvm-tools-preview
150+
# b3sum for BLAKE3 checksums
151+
if ! command -v b3sum &>/dev/null; then
152+
echo "Installing b3sum..."
153+
cargo install b3sum --locked
136154
fi
137155
if ! command -v podman &>/dev/null && ! command -v docker &>/dev/null; then
138156
echo "ERROR: Podman or Docker is required to build VM assets."
139157
echo "Install podman: brew install podman && podman machine init && podman machine start"
140-
exit 1
158+
err=1
141159
fi
142160
if ! command -v b3sum &>/dev/null; then
143161
echo "ERROR: b3sum is required for checksum verification."
144162
echo "Install it via brew: brew install b3sum"
145-
exit 1
163+
err=1
146164
fi
147165
if ! cargo tauri --version &>/dev/null; then
148166
echo "Installing Tauri CLI..."
149167
cargo install tauri-cli
150168
fi
169+
if [ "$err" -ne 0 ]; then exit 1; fi
151170

152171
_frontend:
153172
cd frontend && pnpm build
@@ -192,5 +211,5 @@ _pack-initrd:
192211
find . | cpio -o -H newc 2>/dev/null | gzip > "$INITRD"
193212
rm -rf "$WORKDIR"
194213
cd "$ROOT"
195-
(cd "{{assets_dir}}" && b3sum vmlinuz initrd.img rootfs.img > B3SUMS)
214+
(cd "{{assets_dir}}" && b3sum vmlinuz initrd.img rootfs.squashfs > B3SUMS)
196215
echo "initrd repacked (with agent + net-proxy + mcp-server + fs-watch + doctor)"

0 commit comments

Comments
 (0)