Files

291 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Build libkrun and libkrunfw from source on Linux.
#
# This script builds libkrun (VMM) and libkrunfw (kernel firmware) from source
# with OpenShell's custom kernel configuration for bridge/netfilter support.
#
# In addition to the platform's native .so artifacts, this script exports
# kernel.c and ABI_VERSION metadata so that other platforms (e.g. macOS) can
# compile their own libkrunfw wrapper without rebuilding the kernel.
#
# Prerequisites:
# - Linux (aarch64 or x86_64)
# - Build tools: make, git, gcc, flex, bison, bc
# - Python 3 with pyelftools
# - Rust toolchain
#
# Usage:
# ./build-libkrun.sh
#
# The script will install missing dependencies on Debian/Ubuntu and Fedora.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/_lib.sh"
ROOT="$(vm_lib_root)"
# Source pinned dependency versions
source "${ROOT}/crates/openshell-vm/pins.env" 2>/dev/null || true
BUILD_DIR="${ROOT}/target/libkrun-build"
OUTPUT_DIR="${BUILD_DIR}"
KERNEL_CONFIG="${ROOT}/crates/openshell-vm/runtime/kernel/openshell.kconfig"
if [ "$(uname -s)" != "Linux" ]; then
echo "Error: This script only runs on Linux" >&2
exit 1
fi
HOST_ARCH="$(uname -m)"
echo "==> Building libkrun for Linux ${HOST_ARCH}"
echo " Build directory: ${BUILD_DIR}"
echo " Kernel config: ${KERNEL_CONFIG}"
echo ""
# Map host arch to kernel ARCH value
case "$HOST_ARCH" in
aarch64) KARCH="arm64"; KERNEL_IMAGE_PATH="arch/arm64/boot/Image" ;;
x86_64) KARCH="x86"; KERNEL_IMAGE_PATH="arch/x86/boot/bzImage" ;;
*) echo "Error: Unsupported architecture: ${HOST_ARCH}" >&2; exit 1 ;;
esac
# ── Install dependencies ────────────────────────────────────────────────
# Use sudo only when not already running as root (e.g. inside CI containers).
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
install_deps() {
echo "==> Checking/installing build dependencies..."
if command -v apt-get &>/dev/null; then
# Debian/Ubuntu
DEPS="build-essential git python3 python3-pip python3-pyelftools flex bison libelf-dev libssl-dev bc curl libclang-dev cpio zstd jq"
MISSING=""
for dep in $DEPS; do
if ! dpkg -s "$dep" &>/dev/null; then
MISSING="$MISSING $dep"
fi
done
if [ -n "$MISSING" ]; then
echo " Installing:$MISSING"
$SUDO apt-get update
$SUDO apt-get install -y $MISSING
else
echo " All dependencies installed"
fi
elif command -v dnf &>/dev/null; then
# Fedora/RHEL
DEPS="make git python3 python3-pyelftools gcc flex bison elfutils-libelf-devel openssl-devel bc glibc-static curl clang-devel cpio zstd jq"
echo " Installing dependencies via dnf..."
$SUDO dnf install -y $DEPS
else
echo "Warning: Unknown package manager. Please install manually:" >&2
echo " build-essential git python3 python3-pyelftools flex bison" >&2
echo " libelf-dev libssl-dev bc curl cpio" >&2
fi
# Ensure pyelftools is importable by the Python that will run bin2cbundle.py.
# The apt package may install to a different Python than the default python3.
if ! python3 -c "import elftools" &>/dev/null; then
echo " pyelftools not importable, installing via pip..."
python3 -m pip install --break-system-packages pyelftools 2>/dev/null || \
python3 -m pip install pyelftools || true
fi
}
install_deps
# ── Setup build directory ───────────────────────────────────────────────
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
# ── Build libkrunfw (kernel firmware) ───────────────────────────────────
echo ""
echo "==> Building libkrunfw with custom kernel config..."
if [ ! -d libkrunfw ]; then
echo " Cloning libkrunfw (pinned: ${LIBKRUNFW_REF:-HEAD})..."
git clone https://github.com/containers/libkrunfw.git
fi
cd libkrunfw
# Ensure we're on the pinned commit for reproducible builds
if [ -n "${LIBKRUNFW_REF:-}" ]; then
echo " Checking out pinned ref: ${LIBKRUNFW_REF}"
git fetch origin
git checkout "${LIBKRUNFW_REF}"
fi
# Copy custom kernel config fragment
if [ -f "$KERNEL_CONFIG" ]; then
cp "$KERNEL_CONFIG" openshell.kconfig
echo " Applied custom kernel config fragment: openshell.kconfig"
else
echo "Warning: Custom kernel config not found at ${KERNEL_CONFIG}" >&2
echo " Building with default config (k3s networking may not work)" >&2
fi
echo " Building kernel and libkrunfw (this may take 15-20 minutes)..."
# The libkrunfw Makefile does not support a config fragment — it copies the
# base config and runs olddefconfig, then builds the kernel image in one
# make invocation. We cannot inject the fragment mid-build via make flags.
#
# Instead we drive the build in two phases:
#
# Phase 1: Run the Makefile's $(KERNEL_SOURCES) target, which:
# - downloads and extracts the kernel tarball (if needed)
# - applies patches
# - copies config-libkrunfw_{arch} to $(KERNEL_SOURCES)/.config
# - runs olddefconfig
#
# Phase 2: Merge our fragment on top of the .config produced by Phase 1
# using the kernel's own merge_config.sh, then re-run olddefconfig
# to resolve new dependency chains (e.g. CONFIG_BRIDGE pulls in
# CONFIG_BRIDGE_NETFILTER which needs CONFIG_NETFILTER etc).
#
# Phase 3: Let the Makefile build everything (kernel + kernel.c + .so),
# skipping the $(KERNEL_SOURCES) target since it already exists.
KERNEL_VERSION="$(grep '^KERNEL_VERSION' Makefile | head -1 | awk '{print $3}')"
KERNEL_SOURCES="${KERNEL_VERSION}"
# Phase 1: prepare kernel source tree + base .config.
# Run the Makefile's $(KERNEL_SOURCES) target whenever the .config is absent
# (either because the tree was never extracted, or because it was cleaned).
# The target is idempotent: if the directory already exists make skips the
# tarball extraction but still copies the base config and runs olddefconfig.
if [ ! -f "${KERNEL_SOURCES}/.config" ]; then
echo " Phase 1: preparing kernel source tree and base .config..."
# Remove the directory so make re-runs the full $(KERNEL_SOURCES) recipe
# (extract + patch + config copy + olddefconfig).
rm -rf "${KERNEL_SOURCES}"
make "${KERNEL_SOURCES}"
else
echo " Phase 1: kernel source tree and .config already present, skipping"
fi
# Phase 2: merge the openshell fragment on top
if [ -f openshell.kconfig ]; then
echo " Phase 2: merging openshell.kconfig fragment..."
# merge_config.sh must be called with ARCH set so it finds the right Kconfig
# entry points. -m means "merge into existing .config" (vs starting fresh).
ARCH="${KARCH}" KCONFIG_CONFIG="${KERNEL_SOURCES}/.config" \
"${KERNEL_SOURCES}/scripts/kconfig/merge_config.sh" \
-m -O "${KERNEL_SOURCES}" \
"${KERNEL_SOURCES}/.config" \
openshell.kconfig
# Re-run olddefconfig to fill in any new symbols introduced by the fragment.
make -C "${KERNEL_SOURCES}" ARCH="${KARCH}" olddefconfig
# Verify that the key options were actually applied.
all_ok=true
for opt in CONFIG_BRIDGE CONFIG_NETFILTER CONFIG_NF_NAT; do
val="$(grep "^${opt}=" "${KERNEL_SOURCES}/.config" 2>/dev/null || true)"
if [ -n "$val" ]; then
echo " ${opt}: ${val#*=}"
else
echo " WARNING: ${opt} not set after merge!" >&2
all_ok=false
fi
done
if [ "$all_ok" = false ]; then
echo "ERROR: kernel config fragment merge failed — required options missing" >&2
exit 1
fi
# The kernel binary and kernel.c from the previous (bad) build must be
# removed so make rebuilds them with the updated .config.
rm -f kernel.c "${KERNEL_SOURCES}/${KERNEL_IMAGE_PATH}" \
"${KERNEL_SOURCES}/vmlinux" libkrunfw.so*
fi
# Phase 3: build kernel image, kernel.c bundle, and the shared library
make -j"$(nproc)"
# Copy output
cp libkrunfw.so* "$OUTPUT_DIR/"
echo " Built: $(ls "$OUTPUT_DIR"/libkrunfw.so* | xargs -n1 basename | tr '\n' ' ')"
# ── Export kernel.c for cross-platform builds ───────────────────────────
# kernel.c is a C source file containing the compiled Linux kernel as a byte
# array. It is architecture-specific (aarch64 vs x86_64) but OS-agnostic —
# any C compiler can turn it into a .so or .dylib. We export it so the macOS
# job can produce libkrunfw.dylib without rebuilding the kernel.
ABI_VERSION="$(grep -oE 'ABI_VERSION\s*=\s*[0-9]+' Makefile | head -1 | sed 's/[^0-9]//g')"
if [ -f kernel.c ]; then
cp kernel.c "$OUTPUT_DIR/kernel.c"
echo "${ABI_VERSION}" > "$OUTPUT_DIR/ABI_VERSION"
echo " Exported kernel.c ($(du -sh kernel.c | cut -f1)) and ABI_VERSION=${ABI_VERSION}"
else
echo "Warning: kernel.c not found — cross-platform builds will not work" >&2
fi
cd "$BUILD_DIR"
# ── Build libkrun (VMM) ─────────────────────────────────────────────────
echo ""
echo "==> Building libkrun..."
if [ ! -d libkrun ]; then
echo " Cloning libkrun..."
git clone --depth 1 https://github.com/containers/libkrun.git
fi
cd libkrun
# Build with NET support for gvproxy networking and BLK support for the
# host-backed state disk.
echo " Building libkrun with NET=1 BLK=1..."
# Locate libclang for clang-sys if LIBCLANG_PATH isn't already set.
# clang-sys looks for libclang.so or libclang-*.so; on Debian/Ubuntu the
# versioned file (e.g. libclang-18.so.18) lives under the LLVM lib dir.
if [ -z "${LIBCLANG_PATH:-}" ]; then
for llvm_lib in /usr/lib/llvm-*/lib; do
if ls "$llvm_lib"/libclang*.so* &>/dev/null; then
export LIBCLANG_PATH="$llvm_lib"
echo " LIBCLANG_PATH=$LIBCLANG_PATH"
break
fi
done
fi
make NET=1 BLK=1 -j"$(nproc)"
# Copy output
cp target/release/libkrun.so "$OUTPUT_DIR/"
echo " Built: libkrun.so"
cd "$BUILD_DIR"
# ── Summary ─────────────────────────────────────────────────────────────
echo ""
echo "==> Build complete!"
echo " Output directory: ${OUTPUT_DIR}"
echo ""
echo " Artifacts:"
ls -lah "$OUTPUT_DIR"/*.so* "$OUTPUT_DIR"/kernel.c "$OUTPUT_DIR"/ABI_VERSION 2>/dev/null || \
ls -lah "$OUTPUT_DIR"/*.so*
echo ""
echo "Next step: mise run vm:build"