mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-27 21:12:25 +03:00
Fixes: #335 Still a work-in-progress. --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Wanjohi <elviswanjohi47@gmail.com>
156 lines
7.3 KiB
Bash
156 lines
7.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Builds the guest kernel and installs it at "${KERNEL_OUTPUT}". Runs on the
|
|
# host, as yourself: nothing here needs root.
|
|
#
|
|
# The tree is CachyOS's fork, taken for its patches rather than its config.
|
|
# Theirs is a desktop distro config with thousands of modules; this guest has
|
|
# no /lib/modules at all. What must hold is kernel/nestri.fragment, merged onto
|
|
# whatever .config the tree has and then verified, so a version bump that
|
|
# quietly drops an option fails here instead of in a booted box.
|
|
set -euo pipefail
|
|
|
|
: "${KERNEL_GIT:?}"
|
|
: "${KERNEL_REF:?}"
|
|
: "${KERNEL_SRC:?}"
|
|
: "${KERNEL_OUTPUT:?}"
|
|
|
|
JOBS="${JOBS:-$(nproc)}"
|
|
KERNEL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../kernel" && pwd)"
|
|
FRAGMENT="${KERNEL_DIR}/nestri.fragment"
|
|
SEED="${KERNEL_DIR}/base.config"
|
|
|
|
# Resolved now, because everything below runs from inside the tree.
|
|
mkdir -p "$(dirname "${KERNEL_OUTPUT}")"
|
|
KERNEL_OUTPUT="$(cd "$(dirname "${KERNEL_OUTPUT}")" && pwd)/$(basename "${KERNEL_OUTPUT}")"
|
|
|
|
# ── Source ──────────────────────────────────────────────
|
|
if [[ ! -f "${KERNEL_SRC}/Makefile" ]]; then
|
|
echo "kernel: cloning ${KERNEL_REF} into ${KERNEL_SRC}"
|
|
mkdir -p "$(dirname "${KERNEL_SRC}")"
|
|
git clone --depth=1 --branch "${KERNEL_REF}" "${KERNEL_GIT}" "${KERNEL_SRC}"
|
|
else
|
|
have="$(git -C "${KERNEL_SRC}" describe --tags --exact-match 2>/dev/null || echo unknown)"
|
|
if [[ "${have}" != "${KERNEL_REF}" ]]; then
|
|
# Not fatal: a bisect or a local patch is a legitimate reason to be off
|
|
# the pinned ref, and silently checking it out would throw that away.
|
|
echo "kernel: tree is at '${have}', KERNEL_REF pins '${KERNEL_REF}'; building what is there" >&2
|
|
fi
|
|
fi
|
|
|
|
cd "${KERNEL_SRC}"
|
|
|
|
# ── Infinity scheduler (experimental) ───────────────────
|
|
# Applied once per tree, and recorded, because `patch -N` on an already patched
|
|
# tree does not skip cleanly: it rejects every hunk. The whole series goes in or
|
|
# none of it does -- upstream is explicit that a partial series misbehaves -- so
|
|
# every patch is dry-run against the stacked result before any is applied.
|
|
#
|
|
# In this guest only the fair and rt halves do anything. virtio-gpu does not
|
|
# use the DRM scheduler, so the gpu patch is compiled out with the rest of
|
|
# drivers/gpu/drm/scheduler; it is applied anyway to keep the series whole.
|
|
if [[ -n "${KERNEL_INFINITY:-}" ]]; then
|
|
: "${INFINITY_GIT:?}" "${INFINITY_REV:?}" "${INFINITY_SERIES:?}" "${INFINITY_WORK:?}"
|
|
stamp=".nestri-infinity-rev"
|
|
|
|
if [[ "$(git -C "${INFINITY_WORK}" rev-parse HEAD 2>/dev/null)" != "${INFINITY_REV}" ]]; then
|
|
echo "kernel: fetching infinity-sched ${INFINITY_REV}"
|
|
rm -rf "${INFINITY_WORK}"
|
|
git init -q "${INFINITY_WORK}"
|
|
git -C "${INFINITY_WORK}" fetch -q --depth=1 "${INFINITY_GIT}" "${INFINITY_REV}"
|
|
git -C "${INFINITY_WORK}" checkout -q FETCH_HEAD
|
|
fi
|
|
series_dir="${INFINITY_WORK}/${INFINITY_SERIES}"
|
|
[[ -f "${series_dir}/series" ]] || {
|
|
echo "kernel: infinity-sched has no series at ${INFINITY_SERIES} for ${KERNEL_REF}" >&2
|
|
exit 1
|
|
}
|
|
|
|
have="$(cat "${stamp}" 2>/dev/null || true)"
|
|
if [[ "${have}" == "${INFINITY_REV}" ]]; then
|
|
echo "kernel: infinity series already applied"
|
|
elif [[ -n "${have}" ]]; then
|
|
echo "kernel: tree carries infinity ${have}, INFINITY_REV pins ${INFINITY_REV}" >&2
|
|
echo "kernel: start the tree over with \`make kernel-clean\`" >&2
|
|
exit 1
|
|
else
|
|
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
|
|
echo "kernel: ${KERNEL_SRC} has local changes; not applying the series over them" >&2
|
|
exit 1
|
|
fi
|
|
mapfile -t patches < <(grep -v '^[[:space:]]*\(#\|$\)' "${series_dir}/series")
|
|
# git apply --check takes the whole list and checks each patch against
|
|
# the result of the ones before it, which a per-file `patch --dry-run`
|
|
# cannot do.
|
|
git apply --check "${patches[@]/#/${series_dir}/}"
|
|
for p in "${patches[@]}"; do
|
|
echo "kernel: applying ${p}"
|
|
# -F 0: zero fuzz. Offsets are fine; a hunk that only fits
|
|
# approximately is a scheduler change landing somewhere it was not
|
|
# written for.
|
|
patch -p1 -N -F 0 --quiet < "${series_dir}/${p}"
|
|
done
|
|
echo "${INFINITY_REV}" > "${stamp}"
|
|
fi
|
|
fi
|
|
|
|
# ── Config ──────────────────────────────────────────────
|
|
# A fresh tree has no .config. The seed is a known-good minimal config that
|
|
# olddefconfig migrates to whatever version the tree is at; it only saves a
|
|
# fresh tree from `make defconfig`, whose driver set is enormous next to what a
|
|
# microVM needs. An existing .config is always preferred.
|
|
if [[ ! -f .config ]]; then
|
|
echo "kernel: seeding .config from kernel/base.config"
|
|
cp "${SEED}" .config
|
|
fi
|
|
|
|
echo "kernel: merging kernel/nestri.fragment"
|
|
# -m merges without running a config target, so olddefconfig resolves
|
|
# dependencies once, in one place.
|
|
./scripts/kconfig/merge_config.sh -m .config "${FRAGMENT}" >/dev/null
|
|
make olddefconfig >/dev/null
|
|
|
|
# ── Verify the fragment actually took ───────────────────
|
|
# merge_config.sh warns about overridden symbols but exits 0, and olddefconfig
|
|
# will happily drop an option whose dependencies are unmet. Neither is loud
|
|
# enough for a setting whose failure mode is silent audio, so check the result
|
|
# rather than the intent. Both halves count: an option that must be on, and one
|
|
# that must be off.
|
|
missing=()
|
|
total=0
|
|
while read -r want; do
|
|
total=$((total + 1))
|
|
case "${want}" in
|
|
CONFIG_*) grep -qx "${want}" .config || missing+=("${want%%=*}") ;;
|
|
"# "*) grep -qx "${want}" .config || missing+=("${want:2} (must be off)") ;;
|
|
esac
|
|
done < <(grep -E '^(CONFIG_[A-Z0-9_]+=|# CONFIG_[A-Z0-9_]+ is not set)' "${FRAGMENT}" \
|
|
| sed -E 's/^(CONFIG_[A-Z0-9_]+=[^[:space:]#]+)[[:space:]]*#.*/\1/')
|
|
|
|
if (( ${#missing[@]} )); then
|
|
echo "kernel: these fragment entries did not survive olddefconfig:" >&2
|
|
printf '%s\n' "${missing[@]}" >&2
|
|
exit 1
|
|
fi
|
|
echo "kernel: all ${total} fragment entries hold"
|
|
|
|
# ── Build ───────────────────────────────────────────────
|
|
# -march goes in through KCFLAGS because mainline has no Kconfig for
|
|
# microarchitecture levels. It is safe for the kernel even at x86-64-v3:
|
|
# arch/x86/Makefile passes -mno-sse -mno-mmx -mno-sse2 -mno-avx and friends,
|
|
# and gcc applies those as a mask over -march regardless of flag order, so the
|
|
# kernel gets v3's integer ISA (BMI2, LZCNT, MOVBE) and its scheduling model
|
|
# and never touches a vector register.
|
|
make_args=()
|
|
if [[ -n "${KERNEL_MARCH:-}" ]]; then
|
|
make_args+=("KCFLAGS=-march=${KERNEL_MARCH}")
|
|
echo "kernel: building with -march=${KERNEL_MARCH}"
|
|
fi
|
|
|
|
# vmlinux, not bzImage: the guest is booted by an ELF loader with no
|
|
# bootloader in the path, and a bzImage is a self-decompressing image behind a
|
|
# real-mode setup header, not an ELF.
|
|
make -j"${JOBS}" "${make_args[@]}" vmlinux
|
|
|
|
cp vmlinux "${KERNEL_OUTPUT}"
|
|
echo "kernel: installed ${KERNEL_OUTPUT} ($(numfmt --to=iec "$(stat -c %s "${KERNEL_OUTPUT}")"))"
|