Simple script and config (type-safe) for building custom Linux kernels for Firecracker MicroVMs

Refactor build process to use TypeScript with Deno, replacing Bash script and updating CI workflow; enhance README with usage instructions and prerequisites

+294 -77
+4 -2
.github/workflows/ci.yml
··· 12 12 runs-on: ${{ matrix.os }} 13 13 steps: 14 14 - uses: actions/checkout@v4 15 - 15 + - uses: denoland/setup-deno@v2 16 + with: 17 + deno-version: v2.x 16 18 - name: Set TAG and VERSION 17 19 shell: bash 18 20 run: | ··· 32 34 33 35 - name: Build 34 36 run: | 35 - ./build.sh $TAG 37 + ./build.ts $TAG 36 38 sha256sum linux-stable/vmlinux-${{ env.VERSION }}.${{ env.CPU_ARCH }} > linux-stable/vmlinux-${{ env.VERSION }}.${{ env.CPU_ARCH }}.sha256 37 39 38 40 - name: Upload artifact
+2 -1
.gitignore
··· 1 - linux-stable 1 + linux-stable 2 + vmlinux-builder
+43 -9
README.md
··· 2 2 3 3 [![release](https://github.com/tsirysndr/vmlinux-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/tsirysndr/vmlinux-builder/actions/workflows/ci.yml) 4 4 5 - 📦 **vmlinux-builder** is a lightweight Bash-based tool to fetch, configure, and build the Linux kernel `vmlinux` image for a given version — ideal for use with Firecracker microVMs or other kernel-related debugging/testing tasks. 5 + 📦 **vmlinux-builder** is a lightweight TypeScript/Deno-based tool to fetch, configure, and build the Linux kernel `vmlinux` image for a given version — ideal for use with Firecracker microVMs or other kernel-related debugging/testing tasks. 6 6 7 7 ## ✨ Features 8 8 9 9 - Builds any stable Linux kernel version (e.g., `6.1`, `6.1.12`, `6.1.y`) 10 10 - Uses a custom `.config` for reproducible builds 11 11 - Outputs a ready-to-use `vmlinux-X.Y` file 12 + - Written in TypeScript with Deno for better type safety and cross-platform compatibility 13 + - Colored output using Chalk for improved readability 12 14 - Easily integrated into CI pipelines (e.g., GitHub Actions) 13 15 14 16 ## 🛠 Prerequisites 15 17 18 + ### System Dependencies 19 + 16 20 Ensure you have the following dependencies installed: 17 - 18 21 ```bash 19 22 sudo apt-get install -y git build-essential flex bison libncurses5-dev \ 20 23 libssl-dev gcc bc libelf-dev pahole 21 24 ``` 22 25 26 + ### Deno Runtime 27 + 28 + Install Deno if you haven't already: 29 + ```bash 30 + curl -fsSL https://deno.land/install.sh | sh 31 + ``` 32 + 33 + Or follow the [official Deno installation guide](https://deno.land/#installation). 34 + 23 35 ## 🚀 Usage 36 + 24 37 Clone the repo and provide a kernel version: 38 + ```bash 39 + # Make the script executable 40 + chmod +x build.ts 25 41 26 - ```bash 27 - ./build.sh 6.16.y 42 + # Run with a kernel version 43 + ./build.ts 6.16.y 44 + 45 + # Or run directly with Deno 46 + deno run --allow-run --allow-read --allow-write --allow-env --allow-net build.ts 6.16.y 28 47 ``` 29 48 30 49 **Note:** Ensure a valid `.config` file is present in the root directory before running. 31 50 51 + ### Supported Version Formats 52 + 53 + - `6.1` - Major.Minor version 54 + - `6.1.12` - Specific patch version 55 + - `6.1.y` - Latest from maintenance branch 56 + - `v6.1.12` - Version with 'v' prefix (automatically normalized) 32 57 33 58 ### Example output 34 - 35 59 ``` 60 + Building vmlinux for Linux kernel 6.16 36 61 vmlinux built successfully! 37 - You can find the vmlinux file in linux-stable/vmlinux-6.16 62 + You can find the vmlinux file in /path/to/linux-stable/vmlinux-6.16.x86_64 38 63 ``` 39 64 40 65 ## 📦 GitHub Actions 41 66 42 67 This repo includes a GitHub Actions workflow (`.github/workflows/ci.yml`) that: 43 68 44 - - Triggers on tag push (e.g. git tag 6.16.y && git push origin 6.16.y) 69 + - Triggers on tag push (e.g. `git tag 6.16.y && git push origin 6.16.y`) 45 70 - Builds the vmlinux for that version 46 - - Publishes the resulting vmlinux-X.Y as a GitHub Release asset 71 + - Publishes the resulting `vmlinux-X.Y` as a GitHub Release asset 72 + 73 + ## 🔧 Development 74 + 75 + The script is written in TypeScript and runs on Deno. Key features: 76 + 77 + - **Type-safe**: Full TypeScript support with type checking 78 + - **Cross-platform**: Works on Linux, macOS, and Windows (WSL) 79 + - **Modern**: Uses Deno's native APIs for file operations and process management 80 + - **Colored output**: Enhanced user experience with Chalk 47 81 48 82 ## 📄 License 49 83 50 - This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 84 + This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
-65
build.sh
··· 1 - #!/usr/bin/env bash 2 - set -e 3 - 4 - readonly MAGENTA="$(tput setaf 5 2>/dev/null || echo '')" 5 - readonly GREEN="$(tput setaf 2 2>/dev/null || echo '')" 6 - readonly CYAN="$(tput setaf 6 2>/dev/null || echo '')" 7 - readonly ORANGE="$(tput setaf 3 2>/dev/null || echo '')" 8 - readonly NO_COLOR="$(tput sgr0 2>/dev/null || echo '')" 9 - 10 - if [[ $# -lt 1 ]]; then 11 - echo "${ORANGE}Usage: $0 <kernel-version>{.y|.Z}${NO_COLOR}" 12 - echo "Example: ./build.sh 6.1 | 6.1.12 | 6.1.y | v6.1.12" 13 - exit 1 14 - fi 15 - 16 - INPUT="$1" 17 - NUM="${INPUT#v}" # normalize by stripping optional leading 'v' 18 - 19 - # Validate: X.Y, X.Y.Z, or X.Y.y 20 - if [[ ! "$NUM" =~ ^[0-9]+\.[0-9]+(\.(y|[0-9]+))?$ ]]; then 21 - echo "${ORANGE}Error: Invalid kernel version '${INPUT}'. Expected X.Y, X.Y.Z, or X.Y.y${NO_COLOR}" 22 - echo "Examples: 6.1 | 6.1.12 | 6.1.y | v6.1.12" 23 - exit 1 24 - fi 25 - 26 - echo "Building vmlinux for Linux kernel ${CYAN}${NUM}${NO_COLOR}" 27 - 28 - type apt-get >/dev/null 2>&1 && sudo apt-get install -y git build-essential flex bison libncurses5-dev libssl-dev gcc bc libelf-dev pahole || true 29 - 30 - REPO_URL="git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git" 31 - 32 - # Decide ref: maintenance branch vs tag 33 - if [[ "$NUM" == *".y" ]]; then 34 - REF="linux-${NUM}" # e.g. linux-6.16.y 35 - VERSION="${NUM%.y}" # e.g. 6.16 36 - else 37 - REF="v${NUM}" # e.g. v6.16.2 (ensure leading v) 38 - VERSION="${NUM}" # e.g. 6.16.2 (no leading v) 39 - fi 40 - 41 - if [[ ! -d linux-stable ]]; then 42 - # Clone directly at the desired ref (branch or tag) 43 - git clone --depth=1 --branch "$REF" "$REPO_URL" linux-stable 44 - else 45 - # Update existing checkout to the desired ref 46 - git -C linux-stable fetch --tags --force origin 47 - # Shallow-fetch the specific ref (works for both branches and tags) 48 - git -C linux-stable fetch --depth=1 origin "$REF":"$REF" || git -C linux-stable fetch origin "$REF":"$REF" 49 - git -C linux-stable checkout -f "$REF" 50 - fi 51 - 52 - cp .config linux-stable/.config 53 - 54 - cd linux-stable 55 - 56 - # Build 57 - yes '' | make vmlinux -j"$(nproc)" < /dev/null 58 - 59 - VMLINUX="vmlinux-${VERSION}" 60 - mv vmlinux "${VMLINUX}.$(uname -m)" 61 - 62 - echo "${GREEN}vmlinux built successfully!${NO_COLOR}" 63 - echo "You can find the vmlinux file in ${CYAN}$(pwd)/${VMLINUX}.$(uname -m)${NO_COLOR}" 64 - 65 - exit 0
+205
build.ts
··· 1 + #!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env --allow-net 2 + import chalk from "chalk"; 3 + 4 + async function run(cmd: string[]): Promise<void> { 5 + const process = new Deno.Command(cmd[0], { 6 + args: cmd.slice(1), 7 + stdout: "inherit", 8 + stderr: "inherit", 9 + }); 10 + const { code } = await process.output(); 11 + if (code !== 0) { 12 + Deno.exit(code); 13 + } 14 + } 15 + 16 + async function runQuiet(cmd: string[]): Promise<boolean> { 17 + const process = new Deno.Command(cmd[0], { 18 + args: cmd.slice(1), 19 + stdout: "null", 20 + stderr: "null", 21 + }); 22 + const { code } = await process.output(); 23 + return code === 0; 24 + } 25 + 26 + async function fileExists(path: string): Promise<boolean> { 27 + try { 28 + await Deno.stat(path); 29 + return true; 30 + } catch { 31 + return false; 32 + } 33 + } 34 + 35 + async function getMachineArch(): Promise<string> { 36 + const process = new Deno.Command("uname", { 37 + args: ["-m"], 38 + stdout: "piped", 39 + }); 40 + const { stdout } = await process.output(); 41 + return new TextDecoder().decode(stdout).trim(); 42 + } 43 + 44 + async function getNproc(): Promise<string> { 45 + const process = new Deno.Command("nproc", { 46 + stdout: "piped", 47 + }); 48 + const { stdout } = await process.output(); 49 + return new TextDecoder().decode(stdout).trim(); 50 + } 51 + 52 + const args = Deno.args; 53 + 54 + if (args.length < 1) { 55 + console.log(chalk.yellow(`Usage: $0 <kernel-version>{.y|.Z}`)); 56 + console.log("Example: ./build.sh 6.1 | 6.1.12 | 6.1.y | v6.1.12"); 57 + Deno.exit(1); 58 + } 59 + 60 + const INPUT = args[0]; 61 + const NUM = INPUT.startsWith("v") ? INPUT.slice(1) : INPUT; // normalize by stripping optional leading 'v' 62 + 63 + // Validate: X.Y, X.Y.Z, or X.Y.y 64 + const versionRegex = /^[0-9]+\.[0-9]+(\.(y|[0-9]+))?$/; 65 + if (!versionRegex.test(NUM)) { 66 + console.log( 67 + chalk.yellow( 68 + `Error: Invalid kernel version '${INPUT}'. Expected X.Y, X.Y.Z, or X.Y.y` 69 + ) 70 + ); 71 + console.log("Examples: 6.1 | 6.1.12 | 6.1.y | v6.1.12"); 72 + Deno.exit(1); 73 + } 74 + 75 + console.log(`Building vmlinux for Linux kernel ${chalk.cyan(NUM)}`); 76 + 77 + const hasAptGet = await runQuiet(["which", "apt-get"]); 78 + if (hasAptGet) { 79 + try { 80 + await run([ 81 + "sudo", 82 + "apt-get", 83 + "install", 84 + "-y", 85 + "git", 86 + "build-essential", 87 + "flex", 88 + "bison", 89 + "libncurses5-dev", 90 + "libssl-dev", 91 + "gcc", 92 + "bc", 93 + "libelf-dev", 94 + "pahole", 95 + ]); 96 + } catch { 97 + // Ignore errors 98 + } 99 + } 100 + 101 + const REPO_URL = 102 + "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git"; 103 + 104 + // Decide ref: maintenance branch vs tag 105 + let REF: string; 106 + let VERSION: string; 107 + 108 + if (NUM.endsWith(".y")) { 109 + REF = `linux-${NUM}`; // e.g. linux-6.16.y 110 + VERSION = NUM.slice(0, -2); // e.g. 6.16 111 + } else { 112 + REF = `v${NUM}`; // e.g. v6.16.2 (ensure leading v) 113 + VERSION = NUM; // e.g. 6.16.2 (no leading v) 114 + } 115 + 116 + if (!(await fileExists("linux-stable"))) { 117 + // Clone directly at the desired ref (branch or tag) 118 + await run([ 119 + "git", 120 + "clone", 121 + "--depth=1", 122 + "--branch", 123 + REF, 124 + REPO_URL, 125 + "linux-stable", 126 + ]); 127 + } else { 128 + // Update existing checkout to the desired ref 129 + await run([ 130 + "git", 131 + "-C", 132 + "linux-stable", 133 + "fetch", 134 + "--tags", 135 + "--force", 136 + "origin", 137 + ]); 138 + 139 + // Shallow-fetch the specific ref (works for both branches and tags) 140 + try { 141 + await run([ 142 + "git", 143 + "-C", 144 + "linux-stable", 145 + "fetch", 146 + "--depth=1", 147 + "origin", 148 + `${REF}:${REF}`, 149 + ]); 150 + } catch { 151 + await run([ 152 + "git", 153 + "-C", 154 + "linux-stable", 155 + "fetch", 156 + "origin", 157 + `${REF}:${REF}`, 158 + ]); 159 + } 160 + 161 + await run(["git", "-C", "linux-stable", "checkout", "-f", REF]); 162 + } 163 + 164 + await Deno.copyFile(".config", "linux-stable/.config"); 165 + 166 + Deno.chdir("linux-stable"); 167 + 168 + const nproc = await getNproc(); 169 + const makeProcess = new Deno.Command("make", { 170 + args: ["vmlinux", `-j${nproc}`], 171 + stdin: "null", 172 + stdout: "inherit", 173 + stderr: "inherit", 174 + }); 175 + 176 + // Pipe empty input (equivalent to yes '' | make ... < /dev/null) 177 + const yesProcess = new Deno.Command("yes", { 178 + args: [""], 179 + stdout: "piped", 180 + }); 181 + 182 + const yes = yesProcess.spawn(); 183 + const make = makeProcess.spawn(); 184 + 185 + yes.stdout.pipeTo(make.stdin); 186 + 187 + const { code: makeCode } = await make.status; 188 + yes.kill(); 189 + 190 + if (makeCode !== 0) { 191 + Deno.exit(makeCode); 192 + } 193 + 194 + // Rename vmlinux 195 + const arch = await getMachineArch(); 196 + const VMLINUX = `vmlinux-${VERSION}`; 197 + await Deno.rename("vmlinux", `${VMLINUX}.${arch}`); 198 + 199 + console.log(chalk.green("vmlinux built successfully!")); 200 + const cwd = Deno.cwd(); 201 + console.log( 202 + `You can find the vmlinux file in ${chalk.cyan(`${cwd}/${VMLINUX}.${arch}`)}` 203 + ); 204 + 205 + Deno.exit(0);
+9
deno.json
··· 1 + { 2 + "tasks": { 3 + "build": "deno compile --allow-net --allow-read --allow-write --allow-env -o vmlinux-builder ./build.ts" 4 + }, 5 + "imports": { 6 + "@std/assert": "jsr:@std/assert@1", 7 + "chalk": "npm:chalk@^5.6.2" 8 + } 9 + }
+30
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "jsr:@std/assert@1": "1.0.15", 5 + "jsr:@std/internal@^1.0.12": "1.0.12", 6 + "npm:chalk@^5.6.2": "5.6.2" 7 + }, 8 + "jsr": { 9 + "@std/assert@1.0.15": { 10 + "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 11 + "dependencies": [ 12 + "jsr:@std/internal" 13 + ] 14 + }, 15 + "@std/internal@1.0.12": { 16 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 17 + } 18 + }, 19 + "npm": { 20 + "chalk@5.6.2": { 21 + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 22 + } 23 + }, 24 + "workspace": { 25 + "dependencies": [ 26 + "jsr:@std/assert@1", 27 + "npm:chalk@^5.6.2" 28 + ] 29 + } 30 + }
+1
flake.nix
··· 15 15 in { 16 16 devShells.${system}.default = pkgs.mkShell { 17 17 buildInputs = with pkgs; [ 18 + deno 18 19 curl 19 20 git 20 21 gcc