Simple script and config (type-safe) for building custom Linux kernels for Firecracker MicroVMs
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);