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

feat: add CI workflow for testing and enhance kernel config handling

+63 -27
+20
.github/workflows/tests.yml
··· 1 + name: ci 2 + on: 3 + push: 4 + branches: 5 + - main 6 + pull_request: 7 + branches: 8 + - main 9 + 10 + 11 + jobs: 12 + publish: 13 + runs-on: ubuntu-latest 14 + steps: 15 + - uses: actions/checkout@v4 16 + - uses: denoland/setup-deno@v2 17 + with: 18 + deno-version: v2.x 19 + - name: Run tests 20 + run: deno test -A
+2 -1
.gitignore
··· 1 1 linux-stable 2 2 vmlinux-builder 3 - .config 3 + .config 4 + .config.toml
+22 -23
config.ts
··· 22 22 // Individual config entry 23 23 const ConfigEntrySchema = z.object({ 24 24 key: z.string(), 25 - value: z.union([ 26 - ConfigValueSchema, 27 - z.null(), // For commented out options (# CONFIG_* is not set) 28 - ]), 25 + value: ConfigValueSchema.optional(), 29 26 comment: z.string().optional(), 30 27 }); 31 28 ··· 55 52 }) 56 53 .optional(), 57 54 sections: z.array(ConfigSectionSchema), 58 - flatConfig: z.record(z.string(), ConfigValueSchema.or(z.null())), 55 + flatConfig: z.record(z.string(), ConfigValueSchema.optional()), 59 56 }); 60 57 61 58 // Specific schemas for common config categories ··· 114 111 */ 115 112 static parse(content: string): KernelConfig { 116 113 const lines = content.split("\n"); 117 - const flatConfig: Record<string, ConfigValue | null> = {}; 114 + const flatConfig: Record<string, ConfigValue | undefined> = {}; 118 115 const sections: ConfigSection[] = []; 119 - let currentSection: ConfigSection | null = null; 116 + let currentSection: ConfigSection | undefined = undefined; 120 117 const sectionStack: ConfigSection[] = []; 121 118 122 119 for (const line of lines) { ··· 132 129 // Check for section end 133 130 if (sectionName.startsWith("end of")) { 134 131 if (sectionStack.length > 0) { 135 - currentSection = sectionStack.pop() || null; 132 + currentSection = sectionStack.pop(); 136 133 } 137 134 continue; 138 135 } ··· 164 161 const disabledMatch = trimmed.match(/^#\s*(CONFIG_\w+)\s+is not set/); 165 162 if (disabledMatch) { 166 163 const key = disabledMatch[1]; 167 - flatConfig[key] = null; 164 + flatConfig[key] = undefined; 168 165 169 166 const entry: ConfigEntry = { 170 167 key, 171 - value: null, 168 + value: undefined, 172 169 comment: "is not set", 173 170 }; 174 171 ··· 286 283 /** 287 284 * Get a specific config value 288 285 */ 289 - static getValue(config: KernelConfig, key: string): ConfigValue | null { 290 - return config.flatConfig[key] ?? null; 286 + static getValue(config: KernelConfig, key: string): ConfigValue | undefined { 287 + return config.flatConfig[key]; 291 288 } 292 289 293 290 /** ··· 373 370 374 371 private static serializeFlatConfig( 375 372 lines: string[], 376 - flatConfig: Record<string, ConfigValue | null>, 373 + flatConfig: Record<string, ConfigValue | undefined>, 377 374 opts: Required<SerializeOptions> 378 375 ): void { 379 376 const keys = opts.sortKeys ··· 396 393 ): void { 397 394 const { key, value, comment } = entry; 398 395 399 - if (value === null) { 396 + if (!value) { 400 397 lines.push(`# ${key} is not set`); 401 398 } else if (value === "y" || value === "m" || value === "n") { 402 399 lines.push(`${key}=${value}`); ··· 522 519 try { 523 520 const data = toml.parse(tomlString); 524 521 return KernelConfigDeserializer.fromObject( 525 - data as Record<string, string | number | boolean | null> 522 + data as Record<string, string | number | boolean | undefined> 526 523 ); 527 524 } catch (error) { 528 525 throw new Error(`Failed to deserialize TOML: ${error}`); ··· 532 529 /** 533 530 * Deserialize from YAML-like format 534 531 */ 535 - static fromObject(obj: Record<string, ConfigValue | null>): KernelConfig { 536 - const flatConfig: Record<string, ConfigValue | null> = {}; 532 + static fromObject( 533 + obj: Record<string, ConfigValue | undefined> 534 + ): KernelConfig { 535 + const flatConfig: Record<string, ConfigValue | undefined> = {}; 537 536 538 537 for (const [key, value] of Object.entries(obj)) { 539 538 if (key.startsWith("CONFIG_")) { 540 539 if (value === true) { 541 540 flatConfig[key] = "y"; 542 - } else if (value === false || value === null) { 543 - flatConfig[key] = null; 541 + } else if (value === false || value === undefined) { 542 + flatConfig[key] = undefined; 544 543 } else if (value === "y" || value === "m" || value === "n") { 545 544 flatConfig[key] = value; 546 545 } else if (typeof value === "number" || typeof value === "string") { ··· 594 593 static toObject( 595 594 config: KernelConfig, 596 595 booleanStyle: boolean = false 597 - ): Record<string, ConfigValue | null> { 598 - const result: Record<string, ConfigValue | null> = {}; 596 + ): Record<string, ConfigValue | undefined> { 597 + const result: Record<string, ConfigValue | undefined> = {}; 599 598 600 599 for (const [key, value] of Object.entries(config.flatConfig)) { 601 600 if (booleanStyle) { 602 601 // Convert y/n to true/false 603 602 if (value === "y" || value === "m") { 604 603 result[key] = true; 605 - } else if (value === "n" || value === null) { 604 + } else if (value === "n" || value === undefined) { 606 605 result[key] = false; 607 606 } else { 608 607 result[key] = value; ··· 704 703 lines.push(""); 705 704 706 705 for (const [key, value] of Object.entries(config.flatConfig)) { 707 - if (value === null) { 706 + if (!value) { 708 707 lines.push(`# ${key} is not set`); 709 708 } else { 710 709 const shellValue =
+3 -3
config_test.ts
··· 76 76 77 77 Deno.test("parse disabled options", () => { 78 78 const config = KernelConfigDeserializer.deserialize(simpleConfig); 79 - assertEquals(config.flatConfig.CONFIG_DEBUG, null); 79 + assertEquals(config.flatConfig.CONFIG_DEBUG, undefined); 80 80 }); 81 81 82 82 Deno.test("parse hex values", () => { ··· 95 95 const obj = { CONFIG_SMP: true, CONFIG_DEBUG: false }; 96 96 const config = KernelConfigDeserializer.fromObject(obj); 97 97 assertEquals(config.flatConfig.CONFIG_SMP, "y"); 98 - assertEquals(config.flatConfig.CONFIG_DEBUG, null); 98 + assertEquals(config.flatConfig.CONFIG_DEBUG, undefined); 99 99 }); 100 100 101 101 Deno.test("handle empty config", () => { ··· 330 330 Deno.test("getValue returns null for non-existent key", () => { 331 331 const config = KernelConfigDeserializer.deserialize(simpleConfig); 332 332 const value = KernelConfigParser.getValue(config, "CONFIG_NONEXISTENT"); 333 - assertEquals(value, null); 333 + assertEquals(value, undefined); 334 334 }); 335 335 336 336 Deno.test("isEnabled detects enabled options", () => {
+8
example-toml.ts
··· 1 + 2 + import cfg from "./.default-config" with { type: "text" }; 3 + import { KernelConfigSerializer, parseKernelConfigFile } from './config.ts'; 4 + 5 + const config = parseKernelConfigFile(cfg); 6 + 7 + console.log("# Parsed Kernel Configuration:"); 8 + console.log(KernelConfigSerializer.toTOML(config));
+8
example.ts
··· 1 + 2 + import cfg from "./.default-config" with { type: "text" }; 3 + import { parseKernelConfigFile } from './config.ts'; 4 + 5 + const config = parseKernelConfigFile(cfg); 6 + 7 + console.log("Parsed Kernel Configuration:"); 8 + console.log(config);