666 lines
25 KiB
Bash
Executable File
666 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build a distributable smolvm package
|
|
#
|
|
# Usage:
|
|
# ./scripts/build-dist.sh
|
|
# ./scripts/build-dist.sh --with-local-libkrun
|
|
#
|
|
# Output: dist/smolvm-<version>-<platform>.tar.gz
|
|
|
|
set -e
|
|
|
|
# Options
|
|
WITH_LOCAL_LIBKRUN=0
|
|
SKIP_AGENT_BUILD=0
|
|
LOCAL_LIBKRUN_DIR=""
|
|
LIBKRUN_MAKE_FLAGS="${LIBKRUN_MAKE_FLAGS:-BLK=1 NET=1 GPU=1}"
|
|
|
|
print_help() {
|
|
cat <<'EOF'
|
|
Build a distributable smolvm package.
|
|
|
|
Usage:
|
|
./scripts/build-dist.sh [options]
|
|
|
|
Options:
|
|
--with-local-libkrun Build libkrun from local checkout and refresh bundled lib/
|
|
--local-libkrun-dir PATH Local libkrun checkout (default: ../libkrun)
|
|
--skip-agent-build Skip agent cross-compilation (use pre-built binary)
|
|
-h, --help Show this help text
|
|
|
|
Environment:
|
|
LIBKRUN_MAKE_FLAGS make flags for local libkrun build (default: BLK=1 NET=1 GPU=1)
|
|
LIBCLANG_PATH path to libclang.dylib (auto-detected from brew llvm on macOS)
|
|
LIB_DIR Override bundled library directory used by smolvm build
|
|
CODESIGN_IDENTITY macOS code signing identity (default: - for ad-hoc)
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--with-local-libkrun)
|
|
WITH_LOCAL_LIBKRUN=1
|
|
shift
|
|
;;
|
|
--skip-agent-build)
|
|
SKIP_AGENT_BUILD=1
|
|
shift
|
|
;;
|
|
--local-libkrun-dir)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo "Error: --local-libkrun-dir requires a path"
|
|
exit 1
|
|
fi
|
|
LOCAL_LIBKRUN_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
print_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Error: unknown option: $1"
|
|
print_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Configuration
|
|
VERSION="${VERSION:-$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)}"
|
|
# Normalize architecture: aarch64 -> arm64 for consistent naming across platforms
|
|
_ARCH="$(uname -m)"
|
|
if [[ "$_ARCH" == "aarch64" ]]; then
|
|
_ARCH="arm64"
|
|
fi
|
|
PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')-${_ARCH}"
|
|
DIST_NAME="smolvm-${VERSION}-${PLATFORM}"
|
|
DIST_DIR="dist/${DIST_NAME}"
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
WORKSPACE_SRC_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
|
LOCAL_STAGE_DIR="$PROJECT_ROOT/target/local-lib-stage"
|
|
LOCAL_INIT_KRUN=""
|
|
|
|
if [[ -z "$LOCAL_LIBKRUN_DIR" ]]; then
|
|
LOCAL_LIBKRUN_DIR="$WORKSPACE_SRC_ROOT/libkrun"
|
|
fi
|
|
|
|
echo "Building smolvm distribution: ${DIST_NAME}"
|
|
|
|
# Check for git-lfs (required for library binaries)
|
|
if ! command -v git-lfs &> /dev/null && ! git lfs version &> /dev/null 2>&1; then
|
|
echo "Error: git-lfs is required to build smolvm distributions"
|
|
exit 1
|
|
fi
|
|
|
|
# Resolve bundled library directory
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
ARCH="$(uname -m)"
|
|
DEFAULT_LIB_DIR="./lib/linux-${ARCH}"
|
|
STAGED_LIB_DIR="$LOCAL_STAGE_DIR/usr/local/lib64"
|
|
else
|
|
DEFAULT_LIB_DIR="./lib"
|
|
STAGED_LIB_DIR="$LOCAL_STAGE_DIR/usr/local/lib"
|
|
fi
|
|
|
|
BASE_LIB_DIR="${LIB_DIR:-$DEFAULT_LIB_DIR}"
|
|
WORK_LIB_DIR="$BASE_LIB_DIR"
|
|
LOCAL_BUNDLE_DIR="$PROJECT_ROOT/target/local-lib-bundle"
|
|
|
|
run_make() {
|
|
local repo="$1"
|
|
local flags="$2"
|
|
shift 2
|
|
local -a args=()
|
|
if [[ -n "$flags" ]]; then
|
|
read -r -a args <<< "$flags"
|
|
fi
|
|
make -C "$repo" "${args[@]}" "$@"
|
|
}
|
|
|
|
copy_matching_libraries() {
|
|
local src_dir="$1"
|
|
local pattern="$2"
|
|
local dst_dir="$3"
|
|
|
|
if compgen -G "$src_dir/$pattern" > /dev/null; then
|
|
cp -a "$src_dir"/$pattern "$dst_dir"/
|
|
fi
|
|
}
|
|
|
|
setup_macos_libkrun_env() {
|
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
return
|
|
fi
|
|
if [[ ! -f "$LOCAL_LIBKRUN_DIR/Makefile" ]]; then
|
|
return
|
|
fi
|
|
|
|
# libkrun build scripts use bindgen and require libclang.dylib at runtime.
|
|
if [[ -z "${LIBCLANG_PATH:-}" ]] && command -v brew &> /dev/null; then
|
|
local llvm_prefix
|
|
llvm_prefix="$(brew --prefix llvm 2>/dev/null || true)"
|
|
if [[ -n "$llvm_prefix" ]] && [[ -f "$llvm_prefix/lib/libclang.dylib" ]]; then
|
|
export LIBCLANG_PATH="$llvm_prefix/lib"
|
|
echo "Using libclang from $LIBCLANG_PATH"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "${LIBCLANG_PATH:-}" ]]; then
|
|
export DYLD_FALLBACK_LIBRARY_PATH="$LIBCLANG_PATH:${DYLD_FALLBACK_LIBRARY_PATH:-}"
|
|
else
|
|
echo "Warning: LIBCLANG_PATH is not set."
|
|
echo " If libkrun build fails with 'Library not loaded: @rpath/libclang.dylib',"
|
|
echo " install llvm via brew and set LIBCLANG_PATH to its lib directory."
|
|
fi
|
|
|
|
if [[ "$LIBKRUN_MAKE_FLAGS" == *"BUILD_INIT=0"* ]] && [[ ! -f "$LOCAL_LIBKRUN_DIR/init/init" ]]; then
|
|
echo "Error: LIBKRUN_MAKE_FLAGS includes BUILD_INIT=0 but init binary is missing:"
|
|
echo " $LOCAL_LIBKRUN_DIR/init/init"
|
|
echo "Build init first (for example: make -C \"$LOCAL_LIBKRUN_DIR\" BLK=1),"
|
|
echo "or remove BUILD_INIT=0 from LIBKRUN_MAKE_FLAGS."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
refresh_bundled_libs_from_local() {
|
|
local repo="$1"
|
|
local flags="$2"
|
|
local prefix="$3"
|
|
|
|
if [[ ! -f "$repo/Makefile" ]]; then
|
|
echo "Error: local repo not found: $repo"
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$WORK_LIB_DIR"
|
|
rm -rf "$LOCAL_STAGE_DIR"
|
|
mkdir -p "$LOCAL_STAGE_DIR"
|
|
|
|
# On Linux, link with partial RELRO so libkrun's symbols bind lazily. Full
|
|
# RELRO forces BIND_NOW, which would defeat the lazy virglrenderer loading
|
|
# (RTLD_LAZY in src/agent/krun.rs + the patchelf --remove-needed below) that
|
|
# lets one GPU-enabled libkrun load on non-GPU hosts. Harmless for the
|
|
# install step and non-cargo builds; not applicable to macOS dylibs.
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }-C relro-level=partial" run_make "$repo" "$flags"
|
|
else
|
|
run_make "$repo" "$flags"
|
|
fi
|
|
run_make "$repo" "$flags" install "DESTDIR=$LOCAL_STAGE_DIR" "PREFIX=/usr/local"
|
|
|
|
if [[ ! -d "$STAGED_LIB_DIR" ]]; then
|
|
echo "Error: no staged libraries found in $STAGED_LIB_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
if ! compgen -G "$STAGED_LIB_DIR/${prefix}*" > /dev/null; then
|
|
echo "Error: no staged ${prefix} artifacts found in $STAGED_LIB_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
cp -a "$STAGED_LIB_DIR"/${prefix}* "$WORK_LIB_DIR"/
|
|
}
|
|
|
|
if [[ "$WITH_LOCAL_LIBKRUN" == "1" ]]; then
|
|
if [[ ! -d "$BASE_LIB_DIR" ]]; then
|
|
echo "Error: base library directory does not exist: $BASE_LIB_DIR"
|
|
echo "Set LIB_DIR to a directory containing libkrun/libkrunfw artifacts."
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf "$LOCAL_BUNDLE_DIR"
|
|
mkdir -p "$LOCAL_BUNDLE_DIR"
|
|
copy_matching_libraries "$BASE_LIB_DIR" "libkrun*" "$LOCAL_BUNDLE_DIR"
|
|
copy_matching_libraries "$BASE_LIB_DIR" "libkrunfw*" "$LOCAL_BUNDLE_DIR"
|
|
# GPU rendering libraries are not rebuilt by --with-local-libkrun, but must
|
|
# be carried over from the base lib dir so the dist stays GPU-capable.
|
|
copy_matching_libraries "$BASE_LIB_DIR" "libvirglrenderer*" "$LOCAL_BUNDLE_DIR"
|
|
copy_matching_libraries "$BASE_LIB_DIR" "libMoltenVK*" "$LOCAL_BUNDLE_DIR"
|
|
copy_matching_libraries "$BASE_LIB_DIR" "libepoxy*" "$LOCAL_BUNDLE_DIR"
|
|
# Linux render server binary (required for Venus Vulkan).
|
|
[[ -f "$BASE_LIB_DIR/virgl_render_server" ]] && cp "$BASE_LIB_DIR/virgl_render_server" "$LOCAL_BUNDLE_DIR/"
|
|
WORK_LIB_DIR="$LOCAL_BUNDLE_DIR"
|
|
echo "Staging local build bundle in $WORK_LIB_DIR"
|
|
fi
|
|
|
|
if [[ "$WITH_LOCAL_LIBKRUN" == "1" ]]; then
|
|
echo "Building local libkrun from $LOCAL_LIBKRUN_DIR..."
|
|
setup_macos_libkrun_env
|
|
refresh_bundled_libs_from_local "$LOCAL_LIBKRUN_DIR" "$LIBKRUN_MAKE_FLAGS" "libkrun"
|
|
if [[ -f "$LOCAL_LIBKRUN_DIR/init/init" ]]; then
|
|
LOCAL_INIT_KRUN="$LOCAL_LIBKRUN_DIR/init/init"
|
|
fi
|
|
fi
|
|
|
|
# Check for required libraries
|
|
if [[ ! -f "$WORK_LIB_DIR/libkrun.dylib" ]] && [[ ! -f "$WORK_LIB_DIR/libkrun.so" ]]; then
|
|
echo "Error: libkrun not found in $WORK_LIB_DIR"
|
|
echo "Set LIB_DIR to point to your libkrun library directory."
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$WORK_LIB_DIR/libkrunfw.5.dylib" ]] && [[ ! -f "$WORK_LIB_DIR/libkrunfw.so" ]]; then
|
|
echo "Error: libkrunfw not found in $WORK_LIB_DIR"
|
|
echo "Set LIB_DIR to point to your libkrunfw library directory."
|
|
exit 1
|
|
fi
|
|
|
|
# Build release binaries
|
|
echo "Building release binaries..."
|
|
LIBKRUN_BUNDLE="$WORK_LIB_DIR" cargo build --release --bin smolvm
|
|
|
|
# Build the unified `smol` CLI if its source is present (it lives in a sibling
|
|
# repo checked out at ./smol). It is a separate cargo workspace that depends on
|
|
# the engine by path, so build it from inside ./smol with an ABSOLUTE
|
|
# LIBKRUN_BUNDLE so its build.rs finds the same bundled libraries we ship.
|
|
BUILD_SMOL=0
|
|
if [[ -f "./smol/Cargo.toml" ]]; then
|
|
echo "Building unified smol CLI..."
|
|
_ABS_LIB_BUNDLE="$(cd "$WORK_LIB_DIR" && pwd)"
|
|
( cd ./smol && LIBKRUN_BUNDLE="$_ABS_LIB_BUNDLE" cargo build --release --bin smol )
|
|
BUILD_SMOL=1
|
|
else
|
|
echo "smol CLI source (./smol) not found — building engine-only distribution"
|
|
fi
|
|
|
|
# Build smolvm-agent for Linux (size-optimized)
|
|
if [[ "$SKIP_AGENT_BUILD" == "1" ]]; then
|
|
echo "Skipping agent build (--skip-agent-build)"
|
|
if [[ ! -f "./target/release-small/smolvm-agent" ]]; then
|
|
echo "Error: --skip-agent-build requires a pre-built agent at target/release-small/smolvm-agent"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Building smolvm-agent for Linux (optimized for size)..."
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
# On Linux, build natively with musl for static linking
|
|
MUSL_TARGET="$(uname -m)-unknown-linux-musl"
|
|
if command -v cargo &> /dev/null; then
|
|
if rustup target list --installed 2>/dev/null | grep -q "$MUSL_TARGET"; then
|
|
cargo build --profile release-small -p smolvm-agent --target "$MUSL_TARGET"
|
|
# Copy to the non-target-triple path that the rest of the script expects
|
|
mkdir -p ./target/release-small
|
|
cp "./target/${MUSL_TARGET}/release-small/smolvm-agent" \
|
|
"./target/release-small/smolvm-agent"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# If native build didn't produce the binary, use smolvm
|
|
if [[ ! -f "./target/release-small/smolvm-agent" ]]; then
|
|
if command -v smolvm &> /dev/null; then
|
|
echo "Building via smolvm (rust:alpine)..."
|
|
smolvm machine run --net --mem 2048 -v "$PROJECT_ROOT:/work" --image rust:alpine \
|
|
-- sh -c ". /usr/local/cargo/env && apk add musl-dev && cd /work && cargo build --profile release-small -p smolvm-agent"
|
|
else
|
|
echo "Error: Cannot build smolvm-agent."
|
|
echo " Install smolvm or the musl target (rustup target add x86_64-unknown-linux-musl)"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Sign binary (macOS only)
|
|
# Set CODESIGN_IDENTITY to a Developer ID for distribution signing.
|
|
# Defaults to ad-hoc signing (-) for local development.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
IDENTITY="${CODESIGN_IDENTITY:--}"
|
|
CODESIGN_ARGS=(--force --sign "$IDENTITY" --entitlements smolvm.entitlements)
|
|
if [[ "$IDENTITY" != "-" ]]; then
|
|
# Developer ID signing requires hardened runtime for notarization
|
|
CODESIGN_ARGS+=(--options runtime)
|
|
fi
|
|
echo "Signing binary (identity: $IDENTITY)..."
|
|
codesign "${CODESIGN_ARGS[@]}" ./target/release/smolvm
|
|
# The smol binary also calls the macOS hypervisor, so it needs the same
|
|
# entitlements + signature (otherwise it cannot start a VM).
|
|
if [[ "$BUILD_SMOL" == "1" ]]; then
|
|
codesign "${CODESIGN_ARGS[@]}" ./smol/target/release/smol
|
|
fi
|
|
fi
|
|
|
|
# Create distribution directory
|
|
echo "Creating distribution package..."
|
|
rm -rf "$DIST_DIR"
|
|
mkdir -p "$DIST_DIR/lib"
|
|
|
|
# Copy binary (renamed to smolvm-bin)
|
|
cp ./target/release/smolvm "$DIST_DIR/smolvm-bin"
|
|
|
|
# Copy wrapper script
|
|
cp ./scripts/smolvm-wrapper.sh "$DIST_DIR/smolvm"
|
|
chmod +x "$DIST_DIR/smolvm"
|
|
|
|
# Copy the unified smol CLI (binary renamed to smol-bin + its own wrapper).
|
|
# The wrapper points at the same lib/ and agent-rootfs, so one tarball serves
|
|
# both `smol` (the user-facing CLI) and `smolvm` (the lower-level engine).
|
|
if [[ "$BUILD_SMOL" == "1" ]]; then
|
|
cp ./smol/target/release/smol "$DIST_DIR/smol-bin"
|
|
cp ./scripts/smol-wrapper.sh "$DIST_DIR/smol"
|
|
chmod +x "$DIST_DIR/smol"
|
|
fi
|
|
|
|
# Copy libraries
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
cp "$WORK_LIB_DIR/libkrun.dylib" "$DIST_DIR/lib/"
|
|
cp "$WORK_LIB_DIR/libkrunfw.5.dylib" "$DIST_DIR/lib/"
|
|
# Create symlink for compatibility
|
|
ln -sf libkrunfw.5.dylib "$DIST_DIR/lib/libkrunfw.dylib"
|
|
# Bundle GPU rendering libraries if present (virglrenderer → MoltenVK + epoxy).
|
|
# All three dylibs use @loader_path refs so they resolve correctly regardless
|
|
# of where the lib/ directory is placed after installation.
|
|
for gpu_lib in libvirglrenderer.1.dylib libMoltenVK.dylib libepoxy.0.dylib; do
|
|
if [[ -f "$WORK_LIB_DIR/$gpu_lib" ]]; then
|
|
cp "$WORK_LIB_DIR/$gpu_lib" "$DIST_DIR/lib/"
|
|
echo "Bundled GPU library: $gpu_lib ($(du -h "$DIST_DIR/lib/$gpu_lib" | cut -f1))"
|
|
fi
|
|
done
|
|
else
|
|
copy_so_with_symlinks() {
|
|
local lib_prefix="$1"
|
|
local required="$2"
|
|
local local_so="$WORK_LIB_DIR/${lib_prefix}.so"
|
|
if [[ ! -e "$local_so" ]]; then
|
|
if [[ "$required" == "required" ]]; then
|
|
echo "Error: ${lib_prefix}.so not found in $WORK_LIB_DIR"
|
|
exit 1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# Copy each library's real file plus all symlinks that reference it.
|
|
# Only copies files in the active symlink chain — stale old versions
|
|
# (e.g. libkrunfw.so.5.2.0 when current is 5.3.0) are excluded.
|
|
real_file="$(readlink -f "$local_so")"
|
|
real_name="$(basename "$real_file")"
|
|
cp "$real_file" "$DIST_DIR/lib/$real_name"
|
|
|
|
# Copy every symlink in the directory that ultimately points to
|
|
# the same real file. This catches both directions:
|
|
# libkrun.so.1 → libkrun.so (SONAME → real)
|
|
# libkrunfw.so → libkrunfw.so.5 → libkrunfw.so.5.3.0
|
|
for candidate in "$WORK_LIB_DIR"/${lib_prefix}.so*; do
|
|
[[ -L "$candidate" ]] || continue
|
|
candidate_real="$(readlink -f "$candidate")"
|
|
if [[ "$candidate_real" == "$real_file" ]]; then
|
|
cp -a "$candidate" "$DIST_DIR/lib/"
|
|
fi
|
|
done
|
|
echo "Bundled library: ${lib_prefix} ($(du -h "$DIST_DIR/lib/$real_name" | cut -f1))"
|
|
}
|
|
|
|
copy_so_with_symlinks libkrun required
|
|
copy_so_with_symlinks libkrunfw required
|
|
|
|
# Strip the hard NEEDED on virglrenderer from the GPU-enabled libkrun so a
|
|
# host without it can still dlopen libkrun (paired with the RTLD_LAZY load in
|
|
# src/agent/krun.rs). virglrenderer is loaded by soname at runtime only when
|
|
# the GPU path actually runs — so one build serves both GPU and non-GPU hosts.
|
|
if command -v patchelf >/dev/null 2>&1; then
|
|
for lk in "$DIST_DIR"/lib/libkrun.so*; do
|
|
[[ -f "$lk" && ! -L "$lk" ]] || continue
|
|
if patchelf --print-needed "$lk" 2>/dev/null | grep -q libvirglrenderer; then
|
|
patchelf --remove-needed libvirglrenderer.so.1 "$lk"
|
|
echo "Stripped libvirglrenderer NEEDED from $(basename "$lk") — GPU stays optional at runtime"
|
|
fi
|
|
done
|
|
else
|
|
echo "Warning: patchelf not found — libkrun keeps its hard virglrenderer NEEDED;"
|
|
echo " non-GPU Linux hosts will fail to load it. Install patchelf in the build env."
|
|
fi
|
|
|
|
# Bundle GPU rendering libraries if present (virglrenderer chain for Venus/Vulkan).
|
|
# libMoltenVK is macOS-only — not included here.
|
|
for gpu_lib_prefix in libvirglrenderer libepoxy; do
|
|
copy_so_with_symlinks "$gpu_lib_prefix" optional
|
|
done
|
|
# Bundle render server binary (required for Venus Vulkan on Linux).
|
|
if [[ -f "$WORK_LIB_DIR/virgl_render_server" ]]; then
|
|
cp "$WORK_LIB_DIR/virgl_render_server" "$DIST_DIR/lib/"
|
|
chmod +x "$DIST_DIR/lib/virgl_render_server"
|
|
echo "Bundled: virgl_render_server ($(du -h "$DIST_DIR/lib/virgl_render_server" | cut -f1))"
|
|
fi
|
|
fi
|
|
|
|
# Copy init.krun for Linux only if a real (non-empty) init binary is present.
|
|
# libkrun 2.0.0 embeds the guest init (krun-init) in libkrun.so (INIT_BLOB on by
|
|
# default), so no external init.krun is needed — a VM boots from the embedded
|
|
# init (verified). The committed `libkrun/init/init` is now an empty placeholder,
|
|
# so the `-s` (non-empty) checks skip it; a real prebuilt init still gets bundled
|
|
# for older libkrun builds that expect an external init.krun.
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
# Look for a non-empty init.krun in the submodule or system locations.
|
|
INIT_KRUN=""
|
|
if [[ -n "$LOCAL_INIT_KRUN" ]] && [[ -s "$LOCAL_INIT_KRUN" ]]; then
|
|
INIT_KRUN="$LOCAL_INIT_KRUN"
|
|
elif [[ -s "$PROJECT_ROOT/libkrun/init/init" ]]; then
|
|
INIT_KRUN="$PROJECT_ROOT/libkrun/init/init"
|
|
elif [[ -s "/usr/local/share/smolvm/init.krun" ]]; then
|
|
INIT_KRUN="/usr/local/share/smolvm/init.krun"
|
|
fi
|
|
|
|
if [[ -n "$INIT_KRUN" ]]; then
|
|
echo "Copying init.krun from $INIT_KRUN..."
|
|
cp "$INIT_KRUN" "$DIST_DIR/init.krun"
|
|
chmod +x "$DIST_DIR/init.krun"
|
|
|
|
# init.krun runs as the guest PID 1, so it must match the target arch.
|
|
# libkrun/init/init is a committed binary that does NOT track the build
|
|
# host's arch, so a native dist build can silently ship a wrong-arch init
|
|
# (the x86_64 release once shipped the aarch64 init). Linux dist builds
|
|
# run natively, so the correct arch is `uname -m`; fail loudly otherwise.
|
|
host_arch="$(uname -m)"
|
|
case "$host_arch" in
|
|
x86_64) want="x86-64" ;;
|
|
aarch64|arm64) want="aarch64" ;;
|
|
*) want="" ;;
|
|
esac
|
|
init_desc="$(file -b "$DIST_DIR/init.krun")"
|
|
if [[ -n "$want" ]] && [[ "$init_desc" != *"$want"* ]]; then
|
|
echo "ERROR: init.krun is the wrong architecture for a $host_arch build." >&2
|
|
echo " expected '$want', got: $init_desc" >&2
|
|
echo " source: $INIT_KRUN — rebuild it for $host_arch (see scripts/build-libkrun-linux.sh)." >&2
|
|
exit 1
|
|
fi
|
|
echo "init.krun arch OK ($init_desc)"
|
|
else
|
|
echo "No external init.krun bundled (libkrun 2.0.0 embeds the guest init)."
|
|
fi
|
|
fi
|
|
|
|
# Build agent-rootfs
|
|
echo "Building agent-rootfs..."
|
|
ROOTFS_SRC="$PROJECT_ROOT/target/agent-rootfs"
|
|
if [[ ! -d "$ROOTFS_SRC" ]]; then
|
|
echo "Error: target/agent-rootfs not found"
|
|
echo "Run ./scripts/build-agent-rootfs.sh first to create the base rootfs."
|
|
exit 1
|
|
fi
|
|
|
|
# Copy rootfs and update agent binary
|
|
# Use cp -a to preserve symlinks (busybox creates many symlinks in /bin)
|
|
mkdir -p "$DIST_DIR/agent-rootfs"
|
|
cp -a "$ROOTFS_SRC"/* "$DIST_DIR/agent-rootfs/"
|
|
|
|
# Copy freshly built agent binary (from release-small profile)
|
|
# Remove existing symlinks first (busybox creates init as symlink)
|
|
rm -f "$DIST_DIR/agent-rootfs/usr/local/bin/smolvm-agent"
|
|
rm -f "$DIST_DIR/agent-rootfs/sbin/init"
|
|
cp ./target/release-small/smolvm-agent "$DIST_DIR/agent-rootfs/usr/local/bin/smolvm-agent"
|
|
chmod +x "$DIST_DIR/agent-rootfs/usr/local/bin/smolvm-agent"
|
|
# Symlink /sbin/init → agent (saves ~1.8MB in initramfs vs a copy).
|
|
# The agent handles overlayfs setup + pivot_root internally.
|
|
ln -sf /usr/local/bin/smolvm-agent "$DIST_DIR/agent-rootfs/sbin/init"
|
|
|
|
echo "Agent rootfs size: $(du -sh "$DIST_DIR/agent-rootfs" | cut -f1)"
|
|
|
|
# Create pre-formatted storage template
|
|
# This eliminates the e2fsprogs dependency for end users
|
|
echo "Creating storage template..."
|
|
TEMPLATE_SIZE=$((512 * 1024 * 1024)) # 512MB
|
|
TEMPLATE_PATH="$DIST_DIR/storage-template.ext4"
|
|
|
|
# Find mkfs.ext4
|
|
MKFS_PATHS=(
|
|
"/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4"
|
|
"/usr/local/opt/e2fsprogs/sbin/mkfs.ext4"
|
|
"/opt/homebrew/sbin/mkfs.ext4"
|
|
"/usr/local/sbin/mkfs.ext4"
|
|
"/sbin/mkfs.ext4"
|
|
"/usr/sbin/mkfs.ext4"
|
|
)
|
|
|
|
MKFS_BIN=""
|
|
for path in "${MKFS_PATHS[@]}"; do
|
|
if [[ -x "$path" ]]; then
|
|
MKFS_BIN="$path"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$MKFS_BIN" ]] && command -v mkfs.ext4 &> /dev/null; then
|
|
MKFS_BIN="mkfs.ext4"
|
|
fi
|
|
|
|
if [[ -z "$MKFS_BIN" ]]; then
|
|
echo "Warning: mkfs.ext4 not found, skipping storage template creation"
|
|
echo " Users will need e2fsprogs installed"
|
|
else
|
|
# Set a file's virtual size (sparse), portably (macOS lacks GNU truncate).
|
|
extend_sparse() { # $1=path $2=bytes
|
|
if command -v truncate >/dev/null 2>&1; then
|
|
truncate -s "$2" "$1"
|
|
else
|
|
perl -e 'truncate($ARGV[0], $ARGV[1]) or die "truncate: $!"' "$1" "$2"
|
|
fi
|
|
}
|
|
|
|
# Create sparse file
|
|
dd if=/dev/zero of="$TEMPLATE_PATH" bs=1 count=0 seek=$TEMPLATE_SIZE 2>/dev/null
|
|
|
|
# Format with ext4
|
|
"$MKFS_BIN" -F -q -m 0 -L smolvm "$TEMPLATE_PATH"
|
|
|
|
# Size to the default storage virtual size (DEFAULT_STORAGE_SIZE_GIB=20) so a
|
|
# fresh VM boots from an instant qcow2 overlay (the guest grows the 512 MiB
|
|
# ext4 with resize2fs); the runtime treats the template as immutable. Sparse.
|
|
extend_sparse "$TEMPLATE_PATH" $((20 * 1024 * 1024 * 1024))
|
|
echo "Storage template created: $(du -h "$TEMPLATE_PATH" | cut -f1) physical (20 GiB virtual)"
|
|
|
|
# Create overlay template (same format, different label)
|
|
OVERLAY_TEMPLATE_PATH="$DIST_DIR/overlay-template.ext4"
|
|
dd if=/dev/zero of="$OVERLAY_TEMPLATE_PATH" bs=1 count=0 seek=$TEMPLATE_SIZE 2>/dev/null
|
|
"$MKFS_BIN" -F -q -m 0 -L smolvm-overlay "$OVERLAY_TEMPLATE_PATH"
|
|
# Size to the default overlay virtual size (DEFAULT_OVERLAY_SIZE_GIB=10).
|
|
extend_sparse "$OVERLAY_TEMPLATE_PATH" $((10 * 1024 * 1024 * 1024))
|
|
echo "Overlay template created: $(du -h "$OVERLAY_TEMPLATE_PATH" | cut -f1) physical (10 GiB virtual)"
|
|
fi
|
|
|
|
# Copy README
|
|
cat > "$DIST_DIR/README.txt" << 'EOF'
|
|
smolvm - OCI-native microVM runtime
|
|
|
|
INSTALLATION
|
|
============
|
|
|
|
1. Extract this archive to a location of your choice:
|
|
tar -xzf smolvm-*.tar.gz
|
|
cd smolvm-*
|
|
|
|
2. Run the smolvm wrapper script. It automatically uses the bundled
|
|
agent-rootfs/ directory when present.
|
|
|
|
3. (Optional) Add to PATH:
|
|
# Add to ~/.bashrc or ~/.zshrc:
|
|
export PATH="/path/to/smolvm-directory:$PATH"
|
|
|
|
4. (Optional) Create a symlink:
|
|
sudo ln -s /path/to/smolvm-directory/smolvm /usr/local/bin/smolvm
|
|
|
|
PREREQUISITES
|
|
=============
|
|
|
|
macOS:
|
|
- macOS 11.0 (Big Sur) or later
|
|
- Apple Silicon or Intel Mac
|
|
|
|
Linux:
|
|
- KVM support (/dev/kvm must exist)
|
|
- User must have access to /dev/kvm (typically via 'kvm' group)
|
|
|
|
USAGE
|
|
=====
|
|
|
|
Run the 'smolvm' script (not smolvm-bin directly):
|
|
|
|
./smolvm machine run --net --image alpine -- echo "Hello World"
|
|
./smolvm machine create --net --name myvm
|
|
./smolvm machine start --name myvm
|
|
./smolvm machine exec --name myvm -- /bin/sh
|
|
./smolvm machine ls
|
|
./smolvm machine stop --name myvm
|
|
./smolvm machine delete --name myvm
|
|
|
|
TROUBLESHOOTING
|
|
===============
|
|
|
|
"library not found" errors:
|
|
Make sure you're running the 'smolvm' wrapper script, not 'smolvm-bin'
|
|
directly. The wrapper sets up the library path automatically.
|
|
|
|
"agent did not become ready within 30 seconds":
|
|
This usually means the storage disk couldn't be formatted.
|
|
Check that the storage-template.ext4 file exists in ~/.smolvm/
|
|
If not, you may need to reinstall smolvm or install e2fsprogs:
|
|
macOS: brew install e2fsprogs
|
|
Linux: apt install e2fsprogs
|
|
|
|
For more information: https://github.com/smolvm/smolvm
|
|
EOF
|
|
|
|
# Generate checksums. Use sha256sum where available (Linux/coreutils, incl.
|
|
# Arch) and fall back to `shasum -a 256` (the macOS default, which has no
|
|
# sha256sum). Both emit the same "<hash> <file>" format.
|
|
echo "Generating checksums..."
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
SHA256_CMD=(sha256sum)
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
SHA256_CMD=(shasum -a 256)
|
|
else
|
|
echo "Error: neither sha256sum nor shasum found; cannot generate checksums" >&2
|
|
exit 1
|
|
fi
|
|
(cd "$DIST_DIR" && "${SHA256_CMD[@]}" smolvm smolvm-bin lib/* > checksums.txt)
|
|
|
|
# Delete existing tarball. This is because when a new release is created, there could be
|
|
# tarball of the old release left in dist/, and ./install-local.sh may pick up the wrong tarball
|
|
echo "Cleaning up existing tarball..."
|
|
rm -f "smolvm-*.tar.gz"
|
|
|
|
# Create tarball. Preserve sparseness so the 20/10 GiB-virtual disk templates
|
|
# (a few hundred KiB physical) don't balloon to full size on extraction. GNU tar
|
|
# needs --sparse; bsdtar (macOS) detects holes automatically.
|
|
echo "Creating tarball..."
|
|
cd dist
|
|
if tar --version 2>/dev/null | grep -qi gnu; then
|
|
tar --sparse -czf "${DIST_NAME}.tar.gz" "${DIST_NAME}"
|
|
else
|
|
tar -czf "${DIST_NAME}.tar.gz" "${DIST_NAME}"
|
|
fi
|
|
cd ..
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "Distribution package created:"
|
|
echo " dist/${DIST_NAME}.tar.gz"
|
|
echo ""
|
|
echo "Contents:"
|
|
ls -la "$DIST_DIR"
|
|
echo ""
|
|
echo "To test locally:"
|
|
echo " cd $DIST_DIR && ./smolvm --help"
|
|
echo ""
|
|
echo "To install locally:"
|
|
echo " ./scripts/install-local.sh"
|