Simple script and config (type-safe) for building custom Linux kernels for Firecracker MicroVMs
at main 214 lines 5.0 kB view raw
1#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env --allow-net 2import _ from "@es-toolkit/es-toolkit/compat"; 3import chalk from "chalk"; 4import cfg from "./default-config.ts"; 5 6export * from "./config.ts"; 7 8async function run(cmd: string[]): Promise<void> { 9 console.log(`Running: ${chalk.green(cmd.join(" "))}`); 10 const process = new Deno.Command(cmd[0], { 11 args: cmd.slice(1), 12 stdout: "inherit", 13 stderr: "inherit", 14 }); 15 const { code } = await process.output(); 16 if (code !== 0) { 17 Deno.exit(code); 18 } 19} 20 21async function runQuiet(cmd: string[]): Promise<boolean> { 22 const process = new Deno.Command(cmd[0], { 23 args: cmd.slice(1), 24 stdout: "null", 25 stderr: "null", 26 }); 27 const { code } = await process.output(); 28 return code === 0; 29} 30 31async function fileExists(path: string): Promise<boolean> { 32 try { 33 await Deno.stat(path); 34 return true; 35 } catch { 36 return false; 37 } 38} 39 40async function getMachineArch(): Promise<string> { 41 const process = new Deno.Command("uname", { 42 args: ["-m"], 43 stdout: "piped", 44 }); 45 const { stdout } = await process.output(); 46 return new TextDecoder().decode(stdout).trim(); 47} 48 49async function getNproc(): Promise<string> { 50 const process = new Deno.Command("nproc", { 51 stdout: "piped", 52 }); 53 const { stdout } = await process.output(); 54 return new TextDecoder().decode(stdout).trim(); 55} 56 57const args = Deno.args; 58 59if (args.length < 1) { 60 console.log(chalk.yellow(`Usage: $0 <kernel-version>{.y|.Z}`)); 61 console.log("Example: ./build.sh 6.1 | 6.1.12 | 6.1.y | v6.1.12"); 62 Deno.exit(1); 63} 64 65const INPUT = args[0]; 66const NUM = INPUT.startsWith("v") ? INPUT.slice(1) : INPUT; // normalize by stripping optional leading 'v' 67 68// Validate: X.Y, X.Y.Z, or X.Y.y 69const versionRegex = /^[0-9]+\.[0-9]+(\.(y|[0-9]+))?$/; 70if (!versionRegex.test(NUM)) { 71 console.log( 72 chalk.yellow( 73 `Error: Invalid kernel version '${INPUT}'. Expected X.Y, X.Y.Z, or X.Y.y` 74 ) 75 ); 76 console.log("Examples: 6.1 | 6.1.12 | 6.1.y | v6.1.12"); 77 Deno.exit(1); 78} 79 80console.log(`Building vmlinux for Linux kernel ${chalk.cyan(NUM)}`); 81 82const hasAptGet = await runQuiet(["which", "apt-get"]); 83const hasSudo = await runQuiet(["which", "sudo"]); 84if (hasAptGet) { 85 try { 86 await run([ 87 ..._.compact([hasSudo ? "sudo" : null]), 88 "apt-get", 89 "install", 90 "-y", 91 "git", 92 "build-essential", 93 "flex", 94 "bison", 95 "libncurses5-dev", 96 "libssl-dev", 97 "gcc", 98 "bc", 99 "libelf-dev", 100 "pahole", 101 ]); 102 } catch { 103 // Ignore errors 104 } 105} 106 107const REPO_URL = 108 "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git"; 109 110// Decide ref: maintenance branch vs tag 111let REF: string; 112let VERSION: string; 113 114if (NUM.endsWith(".y")) { 115 REF = `linux-${NUM}`; // e.g. linux-6.16.y 116 VERSION = NUM.slice(0, -2); // e.g. 6.16 117} else { 118 REF = `v${NUM}`; // e.g. v6.16.2 (ensure leading v) 119 VERSION = NUM; // e.g. 6.16.2 (no leading v) 120} 121 122if (!(await fileExists("linux-stable"))) { 123 // Clone directly at the desired ref (branch or tag) 124 await run([ 125 "git", 126 "clone", 127 "--depth=1", 128 "--branch", 129 REF, 130 REPO_URL, 131 "linux-stable", 132 ]); 133} else { 134 // Shallow-fetch the specific ref (works for both branches and tags) 135 try { 136 await run([ 137 "git", 138 "-C", 139 "linux-stable", 140 "fetch", 141 "--depth=1", 142 "origin", 143 REF, 144 ]); 145 } catch { 146 await run(["git", "-C", "linux-stable", "fetch", "origin", REF]); 147 } 148 149 Deno.chdir("linux-stable"); 150 151 await run(["rm", "-rf", "Documentation/Kbuild"]); 152 await run(["make", "mrproper"]); 153 154 await run(["git", "checkout", "-f", REF]); 155 156 Deno.chdir(".."); 157} 158 159if (!(await Deno.stat(".config").catch(() => false))) { 160 console.log( 161 chalk.yellow( 162 "No .config file found in the current directory. Using default configuration." 163 ) 164 ); 165 await Deno.writeTextFile(".config", cfg); 166} 167 168Deno.chdir("linux-stable"); 169 170await Deno.copyFile("../.config", ".config"); 171 172await run(["make", "prepare"]); 173 174const nproc = await getNproc(); 175const makeProcess = new Deno.Command("make", { 176 args: ["vmlinux", `-j${nproc}`], 177 stdin: "piped", 178 stdout: "inherit", 179 stderr: "inherit", 180}); 181 182// Pipe empty input (equivalent to yes '' | make ... < /dev/null) 183const yesProcess = new Deno.Command("yes", { 184 args: [""], 185 stdout: "piped", 186}); 187 188const yes = yesProcess.spawn(); 189const make = makeProcess.spawn(); 190 191yes.stdout.pipeTo(make.stdin).catch((err) => { 192 if (!err.message?.includes("Broken pipe")) { 193 throw err; 194 } 195}); 196 197const { code: makeCode } = await make.status; 198 199if (makeCode !== 0) { 200 Deno.exit(makeCode); 201} 202 203// Rename vmlinux 204const arch = await getMachineArch(); 205const VMLINUX = `vmlinux-${VERSION}`; 206await Deno.rename("vmlinux", `${VMLINUX}.${arch}`); 207 208console.log(chalk.green("vmlinux built successfully!")); 209const cwd = Deno.cwd(); 210console.log( 211 `You can find the vmlinux file in ${chalk.cyan(`${cwd}/${VMLINUX}.${arch}`)}` 212); 213 214Deno.exit(0);