diff --git a/.github/workflows/fc-kernels.yml b/.github/workflows/fc-kernels.yml deleted file mode 100644 index e8e4013..0000000 --- a/.github/workflows/fc-kernels.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: FC Kernels - -on: - push: - -permissions: - id-token: write - contents: write - -jobs: - build: - name: Build kernels (${{ matrix.arch }}) - runs-on: ubuntu-22.04 - strategy: - matrix: - arch: [x86_64, arm64] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build kernels - run: sudo TARGET_ARCH=${{ matrix.arch }} make build - - - name: Upload kernels as artifact - uses: actions/upload-artifact@v4 - with: - name: kernels-${{ matrix.arch }}-${{ github.run_id }} - path: ./builds - retention-days: 7 - - publish: - name: Publish kernels - needs: build - if: github.ref_name == 'main' - runs-on: ubuntu-22.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.VERSION_BUMPER_APPID }} - private-key: ${{ secrets.VERSION_BUMPER_SECRET }} - - - name: Get the last release - id: last_release - uses: cardinalby/git-get-release-action@v1 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - latest: true - prerelease: false - draft: false - - - name: Get next version - id: get-version - run: | - version=${{ steps.last_release.outputs.tag_name }} - result=$(echo ${version} | awk -F. -v OFS=. '{$NF += 1 ; print}') - echo "version=$result" >> $GITHUB_OUTPUT - - - name: Download all build artifacts - uses: actions/download-artifact@v4 - with: - path: ./builds - merge-multiple: true - - - name: Setup Service Account - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} - - - name: Upload kernels to GCS - uses: "google-github-actions/upload-cloud-storage@v1" - with: - path: "./builds" - destination: ${{ vars.GCP_BUCKET_NAME }}/kernels - gzip: false - parent: false - - - name: Create Git tag - run: | - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git tag ${{ steps.get-version.outputs.version }} - git push origin ${{ steps.get-version.outputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Prepare release assets - run: | - mkdir -p release-assets - for dir in ./builds/*/; do - name=$(basename "$dir") - # Legacy x86_64 (no arch subdir) - if [ -f "$dir/vmlinux.bin" ]; then - cp "$dir/vmlinux.bin" "release-assets/${name}.bin" - fi - # Per-arch binaries - for archdir in "$dir"/*/; do - [ -d "$archdir" ] || continue - arch=$(basename "$archdir") - if [ -f "$archdir/vmlinux.bin" ]; then - cp "$archdir/vmlinux.bin" "release-assets/${name}-${arch}.bin" - fi - done - done - - - name: Upload Release Asset - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: Kernels ${{ steps.get-version.outputs.version }} - tag_name: ${{ steps.get-version.outputs.version }} - files: "./release-assets/*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a14f80a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,156 @@ +name: Build & Release + +on: + workflow_dispatch: + pull_request: + +permissions: + contents: read + +jobs: + matrix: + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + build_matrix: ${{ steps.gen.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - id: gen + run: | + python3 - >> "$GITHUB_OUTPUT" <<'PY' + import json + import re + import sys + VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)+$") + versions = [] + for line in open("kernel_versions.txt").read().splitlines(): + v = line.split("#", 1)[0].strip() + if not v: + continue + if not VERSION_RE.match(v): + sys.exit(f"::error::invalid kernel version in kernel_versions.txt: {v!r}") + versions.append(v) + include = [] + for v in versions: + include.append({"version": v, "arch": "amd64", "target_arch": "x86_64", "runner": "ubuntu-24.04"}) + include.append({"version": v, "arch": "arm64", "target_arch": "arm64", "runner": "ubuntu-24.04-arm"}) + print(f"matrix={json.dumps({'include': include})}") + PY + + build: + needs: matrix + name: Build ${{ matrix.version }} (${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.build_matrix) }} + runs-on: ${{ matrix.runner }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Build kernel + env: + KERNEL_VERSION: ${{ matrix.version }} + KERNEL_TARGET_ARCH: ${{ matrix.target_arch }} + run: sudo ./build.sh "$KERNEL_VERSION" "$KERNEL_TARGET_ARCH" + + - uses: actions/upload-artifact@v4 + with: + name: kernel-${{ matrix.version }}-${{ matrix.arch }} + path: ./builds + retention-days: 7 + + # Aggregator with the exact name required by the `Main` ruleset on this + # repo. Per-(version, arch) jobs above run in parallel; this single check + # gates the whole arch on their combined success. + build-status: + needs: build + if: always() + name: Build kernels (${{ matrix.target_arch }}) + strategy: + matrix: + target_arch: [x86_64, arm64] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - run: | + if [[ "${{ needs.build.result }}" != "success" ]]; then + echo "build matrix did not succeed (result=${{ needs.build.result }})" + exit 1 + fi + + publish: + needs: build + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v4 + with: + path: ./builds + merge-multiple: true + + - name: Prepare release assets + run: | + set -euo pipefail + mkdir -p release-assets + for dir in ./builds/vmlinux-*/; do + name=$(basename "$dir") + [ -f "$dir/amd64/vmlinux.bin" ] && cp "$dir/amd64/vmlinux.bin" "release-assets/${name}-amd64.bin" + [ -f "$dir/arm64/vmlinux.bin" ] && cp "$dir/arm64/vmlinux.bin" "release-assets/${name}-arm64.bin" + # Legacy non-arch-suffixed asset (= amd64) for backwards compat. + [ -f "$dir/vmlinux.bin" ] && cp "$dir/vmlinux.bin" "release-assets/${name}.bin" + done + ls -la release-assets/ + + - name: Pick calver tag + id: tag + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + base="$(date -u +%Y.%m.%d)" + tag="$base" + n=0 + while gh release view "$tag" >/dev/null 2>&1 \ + || git rev-parse "refs/tags/$tag" >/dev/null 2>&1 \ + || git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; do + n=$((n + 1)) + tag="${base}.${n}" + done + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.tag.outputs.tag }}" + git push origin "${{ steps.tag.outputs.tag }}" + gh release create "${{ steps.tag.outputs.tag }}" \ + --title "Kernels ${{ steps.tag.outputs.tag }}" \ + --notes "Built from commit ${{ github.sha }} on ${{ github.ref_name }}" \ + ./release-assets/* + + - uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - uses: google-github-actions/upload-cloud-storage@v2 + with: + path: ./builds + destination: ${{ vars.GCP_BUCKET_NAME }}/kernels + gzip: false + parent: false diff --git a/README.md b/README.md index b1e1579..829be4d 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,57 @@ ## Overview -This project automates the building of custom Linux kernels for Firecracker microVMs, using the same kernel sources as official Firecracker repo and custom configuration files. It supports building specific kernel versions and uploading the resulting binaries to a Google Cloud Storage (GCS) bucket. +This project builds custom Linux kernels for Firecracker microVMs from the same kernel sources as the official Firecracker repo, using the configuration files (and optional patches) that live in this repo. ## Prerequisites - Linux environment (for building kernels) -## Building Kernels +## Building locally 1. **Configure kernel versions:** - - Edit `kernel_versions.txt` to specify which kernel versions to build (one per line, e.g., `6.1.102`). - - Place the corresponding config file in `configs/` (e.g., `configs/6.1.102.config`). + - Edit `kernel_versions.txt` to specify which kernel versions to build (one per line, e.g. `6.1.158`). + - Place the corresponding config(s) in `configs/x86_64/.config` and `configs/arm64/.config`. + - (Optional) Drop `*.patch` files into `patches//` to apply on top of the upstream tree before build. 2. **Build:** ```sh - make build - # or directly - ./build.sh + make build # builds all versions in kernel_versions.txt for x86_64 + make build-arm64 # same, for arm64 + ./build.sh 6.1.158 # build a single version (x86_64) + ./build.sh 6.1.158 arm64 ``` - The built kernels will be placed in `builds/vmlinux-//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 backward compatibility, a legacy copy is also placed at `builds/vmlinux-/vmlinux.bin`. -## Development Workflow - - On every push, GitHub Actions will automatically build the kernels and save it as an artifact. + Output: `builds/vmlinux-//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 a legacy copy is also placed at `builds/vmlinux-/vmlinux.bin`. + +## CI / Releasing + +The **Build & Release** workflow runs in two modes: + +- **On every pull request**: builds every (kernel version × arch) combination from `kernel_versions.txt` in parallel (one runner per pair) and uploads the binaries as workflow artifacts (downloadable from the PR's checks tab) so reviewers can inspect them. No release or GCS upload happens. +- **Manually (workflow_dispatch)**: pick the branch in the GitHub UI and run. It does the same build as a PR and additionally creates a GitHub release tagged `YYYY.MM.DD` (with a `.N` suffix for additional runs the same day) containing every binary, and uploads them to GCS. + +Release asset naming for that commit: + + ``` + vmlinux--amd64.bin + vmlinux--arm64.bin + vmlinux-.bin # legacy (= amd64) for backwards compat + ``` + +4. The same binaries are uploaded to GCS at `gs://$GCP_BUCKET_NAME/kernels/vmlinux-//vmlinux.bin`. + +## New kernel in E2B's infra +_Note: these steps should give you a new kernel on your self-hosted E2B using https://github.com/e2b-dev/infra_ + +- Run the release workflow on the branch with the new config/patch. +- Update `DefaultKernelVersion` in [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) if you changed the kernel version. +- Build and deploy `api`. ## Architecture naming Output directories use Go's `runtime.GOARCH` convention (`amd64`, `arm64`) so they match the infra orchestrator's `TargetArch()` path resolution. The build-time variable `TARGET_ARCH` (`x86_64`, `arm64`) is only used internally for config paths and cross-compilation flags. -## New Kernel in E2B's infra -_Note: these steps should give you new kernel on your self-hosted E2B using https://github.com/e2b-dev/infra_ - - - Copy the kernel build in your project's object storage under `e2b-*-fc-kernels` - - In [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) update `DefaultKernelVersion` - - Build and deploy `api` - ## License -This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. +This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. diff --git a/build.sh b/build.sh index 209ee22..d0ed279 100755 --- a/build.sh +++ b/build.sh @@ -3,13 +3,17 @@ set -euo pipefail -# TARGET_ARCH: x86_64 (default) or arm64 -TARGET_ARCH="${TARGET_ARCH:-x86_64}" +# Usage: +# ./build.sh # build all versions in kernel_versions.txt for $TARGET_ARCH +# ./build.sh [arch] # build a single version +# +# arch is one of: x86_64 (default), arm64 (kernel-style names). +# Output: builds/vmlinux-//vmlinux.bin where +# is the Go/OCI name (amd64/arm64) used by the orchestrator. + HOST_ARCH="$(uname -m)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Go/OCI-normalized arch name for output directory structure. -# The infra orchestrator uses Go's runtime.GOARCH convention (amd64/arm64) -# for path resolution, so output directories must match. normalize_arch() { case "$1" in x86_64) echo "amd64" ;; @@ -17,43 +21,60 @@ normalize_arch() { *) echo "$1" ;; esac } -OUTPUT_ARCH="$(normalize_arch "$TARGET_ARCH")" -function install_dependencies { - local packages=( - bc bison busybox-static cpio curl flex gcc libelf-dev libssl-dev make patch squashfs-tools tree - ) +install_dependencies() { + local target_arch="$1" + local packages=( + bc bison busybox-static cpio curl flex gcc libelf-dev libssl-dev make patch squashfs-tools tree + ) - [[ "${TARGET_ARCH}" == "arm64" && "${HOST_ARCH}" != "aarch64" ]] && packages+=( gcc-aarch64-linux-gnu ) + [[ "$target_arch" == "arm64" && "$HOST_ARCH" != "aarch64" ]] && packages+=( gcc-aarch64-linux-gnu ) - apt update - apt install -y "${packages[@]}" + apt update + apt install -y "${packages[@]}" } -# prints the git tag corresponding to the newest and best matching the provided kernel version $1 -function get_tag { - local KERNEL_VERSION="${1}" +# Newest-tag matching the requested kernel version. +get_tag() { + local kernel_version="$1" + { + git --no-pager tag -l --sort=-creatordate | grep "microvm-kernel-${kernel_version}-.*\.amzn2" \ + || git --no-pager tag -l --sort=-creatordate | grep "kernel-${kernel_version}-.*\.amzn2" + } | head -n1 +} - # list all tags from newest to oldest - { - git --no-pager tag -l --sort=-creatordate | grep "microvm-kernel-${KERNEL_VERSION}-.*\.amzn2" \ - || git --no-pager tag -l --sort=-creatordate | grep "kernel-${KERNEL_VERSION}-.*\.amzn2" - } | head -n1 +apply_patches() { + local version="$1" + local patches_dir="$SCRIPT_DIR/patches/$version" + [ -d "$patches_dir" ] || return 0 + shopt -s nullglob + local patches=("$patches_dir"/*.patch) + shopt -u nullglob + [ "${#patches[@]}" -gt 0 ] || return 0 + echo "Applying ${#patches[@]} patch(es) for $version" + for p in "${patches[@]}"; do + git apply --check "$p" + git apply "$p" + done } -function build_version { - local version=$1 - echo "Starting build for kernel version: $version (${TARGET_ARCH})" +build_version() { + local version="$1" + local target_arch="$2" + local output_arch + output_arch="$(normalize_arch "$target_arch")" + + echo "Starting build for kernel version: $version (${target_arch})" - # Configs live in configs/{arch}/ - cp ../configs/"${TARGET_ARCH}/${version}.config" .config + cp "$SCRIPT_DIR/configs/${target_arch}/${version}.config" .config echo "Checking out repo for kernel at version: $version" - git checkout "$(get_tag "$version")" + git checkout -f "$(get_tag "$version")" + + apply_patches "$version" - # Set up cross-compilation if building arm64 on x86_64 local make_opts="" - if [[ "$TARGET_ARCH" == "arm64" ]]; then + if [[ "$target_arch" == "arm64" ]]; then if [[ "$HOST_ARCH" != "aarch64" ]]; then make_opts="ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-" else @@ -63,39 +84,54 @@ function build_version { echo "Building kernel version: $version" make $make_opts olddefconfig - - if [[ "$TARGET_ARCH" == "arm64" ]]; then + + if [[ "$target_arch" == "arm64" ]]; then make $make_opts Image -j "$(nproc)" else make $make_opts vmlinux -j "$(nproc)" fi echo "Copying finished build to builds directory" - # Output to normalized arch dir (amd64/arm64) matching Go's runtime.GOARCH - mkdir -p "../builds/vmlinux-${version}/${OUTPUT_ARCH}" - if [[ "$TARGET_ARCH" == "arm64" ]]; then - cp arch/arm64/boot/Image "../builds/vmlinux-${version}/${OUTPUT_ARCH}/vmlinux.bin" + local out_dir="$SCRIPT_DIR/builds/vmlinux-${version}/${output_arch}" + mkdir -p "$out_dir" + if [[ "$target_arch" == "arm64" ]]; then + cp arch/arm64/boot/Image "$out_dir/vmlinux.bin" else - cp vmlinux "../builds/vmlinux-${version}/${OUTPUT_ARCH}/vmlinux.bin" + cp vmlinux "$out_dir/vmlinux.bin" fi - # x86_64: also copy to legacy path (no arch subdir) for backwards compat - if [[ "$TARGET_ARCH" == "x86_64" ]]; then - cp vmlinux "../builds/vmlinux-${version}/vmlinux.bin" + # x86_64: also copy to legacy path (no arch subdir) for backwards compat. + if [[ "$target_arch" == "x86_64" ]]; then + cp vmlinux "$SCRIPT_DIR/builds/vmlinux-${version}/vmlinux.bin" fi } -echo "Building kernels for ${TARGET_ARCH}" +ensure_linux_repo() { + cd "$SCRIPT_DIR" + [ -d linux ] || git clone --no-checkout --filter=tree:0 https://github.com/amazonlinux/linux + cd linux + make distclean || true +} -install_dependencies +main() { + local single_version="${1:-}" + local target_arch="${2:-${TARGET_ARCH:-x86_64}}" -[ -d linux ] || git clone --no-checkout --filter=tree:0 https://github.com/amazonlinux/linux -pushd linux + install_dependencies "$target_arch" -make distclean || true + ensure_linux_repo -grep -v '^ *#' <../kernel_versions.txt | while IFS= read -r version; do - build_version "$version" -done + if [[ -n "$single_version" ]]; then + build_version "$single_version" "$target_arch" + else + while IFS= read -r raw; do + local version="${raw%%#*}" + version="${version#"${version%%[![:space:]]*}"}" + version="${version%"${version##*[![:space:]]}"}" + [ -z "$version" ] && continue + build_version "$version" "$target_arch" + done <"$SCRIPT_DIR/kernel_versions.txt" + fi +} -popd +main "$@"