Simple script and config (type-safe) for building custom Linux kernels for Firecracker MicroVMs
at main 812 lines 22 kB view raw
1import * as toml from "@std/toml"; 2import z from "@zod/zod"; 3 4/** 5 * Zod schema for Linux Kernel Configuration (.config) files 6 * Supports parsing the standard kernel config format with: 7 * - CONFIG_* options set to y, m, n, or numeric/string values 8 * - Comments (# lines) 9 * - Section headers 10 */ 11 12// Base config value types 13const ConfigValueSchema: z.ZodType< 14 "y" | "m" | "n" | number | string | boolean 15> = z.union([ 16 z.literal("y"), // Built-in 17 z.literal("m"), // Module 18 z.literal("n"), // Not set (explicit) 19 z.number(), // Numeric value 20 z.string(), // String value 21 z.boolean(), // Derived from 'y'/'n' or presence 22]); 23 24// Individual config entry 25const ConfigEntrySchema: z.ZodType<{ 26 key: string; 27 value?: z.infer<typeof ConfigValueSchema>; 28 comment?: string; 29}> = z.object({ 30 key: z.string(), 31 value: ConfigValueSchema.optional(), 32 comment: z.string().optional(), 33}); 34 35// Section in the config file 36interface ConfigSection { 37 name: string; 38 entries: ConfigEntry[]; 39 subsections?: ConfigSection[]; 40} 41 42const ConfigSectionSchema: z.ZodType<ConfigSection> = z.lazy(() => 43 z.object({ 44 name: z.string(), 45 entries: z.array(ConfigEntrySchema), 46 subsections: z.array(z.lazy(() => ConfigSectionSchema)).optional(), 47 }) 48); 49 50// Main kernel config schema 51export const KernelConfigSchema: z.ZodType<{ 52 version?: string | undefined; 53 buildInfo?: 54 | { 55 compiler?: string | undefined; 56 gccVersion?: string | undefined; 57 buildSalt?: string | undefined; 58 } 59 | undefined; 60 sections: ConfigSection[]; 61 flatConfig: Record<string, ConfigValue | undefined>; 62}> = z.object({ 63 version: z.string().optional(), 64 buildInfo: z 65 .object({ 66 compiler: z.string().optional(), 67 gccVersion: z.string().optional(), 68 buildSalt: z.string().optional(), 69 }) 70 .optional(), 71 sections: z.array(ConfigSectionSchema), 72 flatConfig: z.record(z.string(), ConfigValueSchema.optional()), 73}); 74 75// Specific schemas for common config categories 76export const ProcessorConfigSchema: z.ZodType<{ 77 SMP?: boolean | undefined; 78 NR_CPUS?: number | undefined; 79 X86_64?: boolean | undefined; 80 NUMA?: boolean | undefined; 81 PREEMPT?: boolean | undefined; 82 PREEMPT_VOLUNTARY?: boolean | undefined; 83 PREEMPT_NONE?: boolean | undefined; 84}> = z.object({ 85 SMP: z.boolean().optional(), 86 NR_CPUS: z.number().optional(), 87 X86_64: z.boolean().optional(), 88 NUMA: z.boolean().optional(), 89 PREEMPT: z.boolean().optional(), 90 PREEMPT_VOLUNTARY: z.boolean().optional(), 91 PREEMPT_NONE: z.boolean().optional(), 92}); 93 94export const SecurityConfigSchema: z.ZodType<{ 95 SECURITY?: boolean | undefined; 96 SECURITY_SELINUX?: boolean | undefined; 97 SECURITY_APPARMOR?: boolean | undefined; 98 SECURITY_SMACK?: boolean | undefined; 99 SECCOMP?: boolean | undefined; 100 STACKPROTECTOR?: boolean | undefined; 101 FORTIFY_SOURCE?: boolean | undefined; 102}> = z.object({ 103 SECURITY: z.boolean().optional(), 104 SECURITY_SELINUX: z.boolean().optional(), 105 SECURITY_APPARMOR: z.boolean().optional(), 106 SECURITY_SMACK: z.boolean().optional(), 107 SECCOMP: z.boolean().optional(), 108 STACKPROTECTOR: z.boolean().optional(), 109 FORTIFY_SOURCE: z.boolean().optional(), 110}); 111 112export const NetworkingConfigSchema: z.ZodType<{ 113 NET?: boolean | undefined; 114 INET?: boolean | undefined; 115 IPV6?: boolean | undefined; 116 NETFILTER?: boolean | undefined; 117 PACKET?: boolean | undefined; 118 UNIX?: boolean | undefined; 119}> = z.object({ 120 NET: z.boolean().optional(), 121 INET: z.boolean().optional(), 122 IPV6: z.boolean().optional(), 123 NETFILTER: z.boolean().optional(), 124 PACKET: z.boolean().optional(), 125 UNIX: z.boolean().optional(), 126}); 127 128export const FilesystemConfigSchema: z.ZodType<{ 129 EXT4_FS?: boolean | undefined; 130 XFS_FS?: boolean | undefined; 131 BTRFS_FS?: boolean | undefined; 132 NFS_FS?: boolean | undefined; 133 TMPFS?: boolean | undefined; 134}> = z.object({ 135 EXT4_FS: z.boolean().optional(), 136 XFS_FS: z.boolean().optional(), 137 BTRFS_FS: z.boolean().optional(), 138 NFS_FS: z.boolean().optional(), 139 TMPFS: z.boolean().optional(), 140}); 141 142// TypeScript types derived from schemas 143export type ConfigValue = z.infer<typeof ConfigValueSchema>; 144export type ConfigEntry = z.infer<typeof ConfigEntrySchema>; 145export type KernelConfig = z.infer<typeof KernelConfigSchema>; 146export type ProcessorConfig = z.infer<typeof ProcessorConfigSchema>; 147export type SecurityConfig = z.infer<typeof SecurityConfigSchema>; 148export type NetworkingConfig = z.infer<typeof NetworkingConfigSchema>; 149export type FilesystemConfig = z.infer<typeof FilesystemConfigSchema>; 150 151/** 152 * Parser for Linux kernel .config files 153 */ 154export class KernelConfigParser { 155 /** 156 * Parse a kernel config file content 157 */ 158 static parse(content: string): KernelConfig { 159 const lines = content.split("\n"); 160 const flatConfig: Record<string, ConfigValue | undefined> = {}; 161 const sections: ConfigSection[] = []; 162 let currentSection: ConfigSection | undefined = undefined; 163 const sectionStack: ConfigSection[] = []; 164 165 for (const line of lines) { 166 const trimmed = line.trim(); 167 168 if (!trimmed) continue; 169 170 if (trimmed.startsWith("#") && !trimmed.includes("CONFIG_")) { 171 const sectionMatch = trimmed.match(/^#\s*(.+)$/); 172 if (sectionMatch) { 173 const sectionName = sectionMatch[1]; 174 175 // Check for section end 176 if (sectionName.startsWith("end of")) { 177 if (sectionStack.length > 0) { 178 currentSection = sectionStack.pop(); 179 } 180 continue; 181 } 182 183 // Create new section 184 const newSection: ConfigSection = { 185 name: sectionName, 186 entries: [], 187 subsections: [], 188 }; 189 190 if (currentSection) { 191 // Add as subsection 192 if (!currentSection.subsections) { 193 currentSection.subsections = []; 194 } 195 currentSection.subsections.push(newSection); 196 sectionStack.push(currentSection); 197 } else { 198 // Add as top-level section 199 sections.push(newSection); 200 } 201 currentSection = newSection; 202 } 203 continue; 204 } 205 206 // Disabled option: # CONFIG_* is not set 207 const disabledMatch = trimmed.match(/^#\s*(CONFIG_\w+)\s+is not set/); 208 if (disabledMatch) { 209 const key = disabledMatch[1]; 210 flatConfig[key] = undefined; 211 212 const entry: ConfigEntry = { 213 key, 214 value: undefined, 215 comment: "is not set", 216 }; 217 218 if (currentSection) { 219 currentSection.entries.push(entry); 220 } 221 continue; 222 } 223 224 // Enabled option: CONFIG_*=value 225 const enabledMatch = trimmed.match(/^(CONFIG_\w+)=(.+)$/); 226 if (enabledMatch) { 227 const key = enabledMatch[1]; 228 let value: ConfigValue; 229 const rawValue = enabledMatch[2]; 230 231 // Parse value type 232 if (rawValue === "y") { 233 value = "y"; 234 } else if (rawValue === "m") { 235 value = "m"; 236 } else if (rawValue === "n") { 237 value = "n"; 238 } else if (rawValue.match(/^-?\d+$/)) { 239 value = parseInt(rawValue, 10); 240 } else if (rawValue.match(/^0x[0-9a-fA-F]+$/)) { 241 value = parseInt(rawValue, 16); 242 } else { 243 // String value (remove quotes if present) 244 value = rawValue.replace(/^"(.*)"$/, "$1"); 245 } 246 247 flatConfig[key] = value; 248 249 const entry: ConfigEntry = { 250 key, 251 value, 252 }; 253 254 if (currentSection) { 255 currentSection.entries.push(entry); 256 } 257 continue; 258 } 259 } 260 261 // Extract build info 262 const buildInfo = { 263 compiler: flatConfig.CONFIG_CC_VERSION_TEXT as string | undefined, 264 gccVersion: flatConfig.CONFIG_GCC_VERSION 265 ? String(flatConfig.CONFIG_GCC_VERSION) 266 : undefined, 267 buildSalt: flatConfig.CONFIG_BUILD_SALT as string | undefined, 268 }; 269 270 return { 271 buildInfo, 272 sections, 273 flatConfig, 274 }; 275 } 276 277 /** 278 * Extract specific config category 279 */ 280 static extractProcessorConfig(config: KernelConfig): ProcessorConfig { 281 const flat = config.flatConfig; 282 return { 283 SMP: flat.CONFIG_SMP === "y", 284 NR_CPUS: flat.CONFIG_NR_CPUS as number | undefined, 285 X86_64: flat.CONFIG_X86_64 === "y", 286 NUMA: flat.CONFIG_NUMA === "y", 287 PREEMPT: flat.CONFIG_PREEMPT === "y", 288 PREEMPT_VOLUNTARY: flat.CONFIG_PREEMPT_VOLUNTARY === "y", 289 PREEMPT_NONE: flat.CONFIG_PREEMPT_NONE === "y", 290 }; 291 } 292 293 static extractSecurityConfig(config: KernelConfig): SecurityConfig { 294 const flat = config.flatConfig; 295 return { 296 SECURITY: flat.CONFIG_SECURITY === "y", 297 SECURITY_SELINUX: flat.CONFIG_SECURITY_SELINUX === "y", 298 SECURITY_APPARMOR: flat.CONFIG_SECURITY_APPARMOR === "y", 299 SECURITY_SMACK: flat.CONFIG_SECURITY_SMACK === "y", 300 SECCOMP: flat.CONFIG_SECCOMP === "y", 301 STACKPROTECTOR: flat.CONFIG_STACKPROTECTOR === "y", 302 FORTIFY_SOURCE: flat.CONFIG_FORTIFY_SOURCE === "y", 303 }; 304 } 305 306 static extractNetworkingConfig(config: KernelConfig): NetworkingConfig { 307 const flat = config.flatConfig; 308 return { 309 NET: flat.CONFIG_NET === "y", 310 INET: flat.CONFIG_INET === "y", 311 IPV6: flat.CONFIG_IPV6 === "y", 312 NETFILTER: flat.CONFIG_NETFILTER === "y", 313 PACKET: flat.CONFIG_PACKET === "y", 314 UNIX: flat.CONFIG_UNIX === "y", 315 }; 316 } 317 318 static extractFilesystemConfig(config: KernelConfig): FilesystemConfig { 319 const flat = config.flatConfig; 320 return { 321 EXT4_FS: flat.CONFIG_EXT4_FS === "y", 322 XFS_FS: flat.CONFIG_XFS_FS === "y", 323 BTRFS_FS: flat.CONFIG_BTRFS_FS === "y", 324 NFS_FS: flat.CONFIG_NFS_FS === "y", 325 TMPFS: flat.CONFIG_TMPFS === "y", 326 }; 327 } 328 329 /** 330 * Get a specific config value 331 */ 332 static getValue(config: KernelConfig, key: string): ConfigValue | undefined { 333 return config.flatConfig[key]; 334 } 335 336 /** 337 * Check if a config option is enabled (set to 'y' or 'm') 338 */ 339 static isEnabled(config: KernelConfig, key: string): boolean { 340 const value = config.flatConfig[key]; 341 return value === "y" || value === "m"; 342 } 343 344 /** 345 * Serialize config back to .config format 346 */ 347 static serialize(config: KernelConfig, options?: SerializeOptions): string { 348 const opts: Required<SerializeOptions> = { 349 preserveSections: true, 350 includeComments: true, 351 sortKeys: false, 352 addHeader: true, 353 formatStyle: "kernel", 354 ...options, 355 }; 356 357 const lines: string[] = []; 358 359 // Add header 360 if (opts.addHeader) { 361 lines.push("#"); 362 lines.push("# Automatically generated file; DO NOT EDIT."); 363 364 if (config.buildInfo?.compiler) { 365 lines.push(`# ${config.buildInfo.compiler}`); 366 } 367 if (config.buildInfo?.buildSalt) { 368 lines.push(`# Linux Kernel Configuration`); 369 } 370 lines.push("#"); 371 lines.push(""); 372 } 373 374 if (opts.preserveSections && config.sections.length > 0) { 375 // Serialize with section structure 376 this.serializeSections(lines, config.sections, 0, opts); 377 } else { 378 // Serialize flat config 379 this.serializeFlatConfig(lines, config.flatConfig, opts); 380 } 381 382 return lines.join("\n"); 383 } 384 385 private static serializeSections( 386 lines: string[], 387 sections: ConfigSection[], 388 depth: number, 389 opts: Required<SerializeOptions> 390 ): void { 391 for (const section of sections) { 392 // Add section header 393 if (opts.includeComments) { 394 lines.push(""); 395 lines.push("#"); 396 lines.push(`# ${section.name}`); 397 lines.push("#"); 398 } 399 400 // Serialize entries 401 for (const entry of section.entries) { 402 this.serializeEntry(lines, entry, opts); 403 } 404 405 // Serialize subsections recursively 406 if (section.subsections && section.subsections.length > 0) { 407 this.serializeSections(lines, section.subsections, depth + 1, opts); 408 } 409 410 // Add section footer 411 if (opts.includeComments) { 412 lines.push(`# end of ${section.name}`); 413 } 414 } 415 } 416 417 private static serializeFlatConfig( 418 lines: string[], 419 flatConfig: Record<string, ConfigValue | undefined>, 420 opts: Required<SerializeOptions> 421 ): void { 422 const keys = opts.sortKeys 423 ? Object.keys(flatConfig).sort() 424 : Object.keys(flatConfig); 425 426 for (const key of keys) { 427 const entry: ConfigEntry = { 428 key, 429 value: flatConfig[key], 430 }; 431 this.serializeEntry(lines, entry, opts); 432 } 433 } 434 435 private static serializeEntry( 436 lines: string[], 437 entry: ConfigEntry, 438 opts: Required<SerializeOptions> 439 ): void { 440 const { key, value, comment } = entry; 441 442 if (!value) { 443 lines.push(`# ${key} is not set`); 444 } else if (value === "y" || value === "m" || value === "n") { 445 lines.push(`${key}=${value}`); 446 } else if (typeof value === "number") { 447 lines.push(`${key}=${value}`); 448 } else if (typeof value === "string") { 449 const needsQuotes = 450 value.includes(" ") || 451 value.includes("#") || 452 value.includes("=") || 453 opts.formatStyle === "quoted"; 454 const formatted = needsQuotes ? `"${value}"` : value; 455 lines.push(`${key}=${formatted}`); 456 } 457 458 // Add inline comment if present 459 if (comment && opts.includeComments) { 460 const lastLine = lines[lines.length - 1]; 461 lines[lines.length - 1] = `${lastLine} # ${comment}`; 462 } 463 } 464} 465 466/** 467 * Serialization options 468 */ 469export interface SerializeOptions { 470 /** Preserve section structure (default: true) */ 471 preserveSections?: boolean; 472 /** Include comments (default: true) */ 473 includeComments?: boolean; 474 /** Sort keys alphabetically (default: false) */ 475 sortKeys?: boolean; 476 /** Add header with build info (default: true) */ 477 addHeader?: boolean; 478 /** Format style: 'kernel' or 'quoted' (default: 'kernel') */ 479 formatStyle?: "kernel" | "quoted"; 480} 481 482/** 483 * Deserialization options 484 */ 485export interface DeserializeOptions { 486 /** Strict mode: fail on parse errors (default: false) */ 487 strict?: boolean; 488 /** Preserve comments as metadata (default: true) */ 489 preserveComments?: boolean; 490 /** Parse section hierarchy (default: true) */ 491 parseSections?: boolean; 492 /** Validate with Zod schema (default: false) */ 493 validate?: boolean; 494} 495 496/** 497 * Enhanced deserializer with options 498 */ 499export class KernelConfigDeserializer { 500 /** 501 * Deserialize kernel config with options 502 */ 503 static deserialize( 504 content: string, 505 options?: DeserializeOptions 506 ): KernelConfig { 507 const opts: Required<DeserializeOptions> = { 508 strict: false, 509 preserveComments: true, 510 parseSections: true, 511 validate: false, 512 ...options, 513 }; 514 515 try { 516 const config = KernelConfigParser.parse(content); 517 518 if (opts.validate) { 519 const validated = KernelConfigSchema.parse(config); 520 return validated; 521 } 522 523 return config; 524 } catch (error) { 525 if (opts.strict) { 526 throw new Error(`Failed to deserialize kernel config: ${error}`); 527 } 528 529 // Return minimal valid config on error 530 return { 531 sections: [], 532 flatConfig: {}, 533 }; 534 } 535 } 536 537 /** 538 * Deserialize from JSON format 539 */ 540 static fromJSON(json: string, options?: DeserializeOptions): KernelConfig { 541 try { 542 const data = JSON.parse(json); 543 544 if (options?.validate) { 545 return KernelConfigSchema.parse(data); 546 } 547 548 return data as KernelConfig; 549 } catch (error) { 550 if (options?.strict) { 551 throw new Error(`Failed to deserialize JSON: ${error}`); 552 } 553 554 return { 555 sections: [], 556 flatConfig: {}, 557 }; 558 } 559 } 560 561 /** 562 * Deserialize from TOML format 563 */ 564 static fromTOML(tomlString: string): KernelConfig { 565 try { 566 const data = toml.parse(tomlString); 567 return KernelConfigDeserializer.fromObject( 568 data as Record<string, string | number | boolean | undefined> 569 ); 570 } catch (error) { 571 throw new Error(`Failed to deserialize TOML: ${error}`); 572 } 573 } 574 575 /** 576 * Deserialize from YAML-like format 577 */ 578 static fromObject( 579 obj: Record<string, ConfigValue | undefined> 580 ): KernelConfig { 581 const flatConfig: Record<string, ConfigValue | undefined> = {}; 582 583 for (const [key, value] of Object.entries(obj)) { 584 if (key.startsWith("CONFIG_")) { 585 if (value === true) { 586 flatConfig[key] = "y"; 587 } else if (value === false || value === undefined) { 588 flatConfig[key] = undefined; 589 } else if (value === "y" || value === "m" || value === "n") { 590 flatConfig[key] = value; 591 } else if (typeof value === "number" || typeof value === "string") { 592 flatConfig[key] = value; 593 } 594 } 595 } 596 597 return { 598 sections: [], 599 flatConfig, 600 }; 601 } 602} 603 604/** 605 * Enhanced serializer with multiple output formats 606 */ 607export class KernelConfigSerializer { 608 /** 609 * Serialize to kernel .config format 610 */ 611 static toConfig(config: KernelConfig, options?: SerializeOptions): string { 612 return KernelConfigParser.serialize(config, options); 613 } 614 615 /** 616 * Serialize to JSON format 617 */ 618 static toJSON(config: KernelConfig, pretty: boolean = true): string { 619 if (pretty) { 620 return JSON.stringify(config, null, 2); 621 } 622 return JSON.stringify(config); 623 } 624 625 /** 626 * Serialize to TOML format 627 */ 628 static toTOML(config: KernelConfig): string { 629 const tomlObj: Record<string, unknown> = { 630 buildInfo: config.buildInfo || {}, 631 config: config.flatConfig, 632 }; 633 return toml.stringify(tomlObj); 634 } 635 636 /** 637 * Serialize to simple key-value object 638 */ 639 static toObject( 640 config: KernelConfig, 641 booleanStyle: boolean = false 642 ): Record<string, ConfigValue | undefined> { 643 const result: Record<string, ConfigValue | undefined> = {}; 644 645 for (const [key, value] of Object.entries(config.flatConfig)) { 646 if (booleanStyle) { 647 // Convert y/n to true/false 648 if (value === "y" || value === "m") { 649 result[key] = true; 650 } else if (value === "n" || value === undefined) { 651 result[key] = false; 652 } else { 653 result[key] = value; 654 } 655 } else { 656 result[key] = value; 657 } 658 } 659 660 return result; 661 } 662 663 /** 664 * Serialize to YAML-like format (as string) 665 */ 666 static toYAML(config: KernelConfig): string { 667 const lines: string[] = []; 668 lines.push("---"); 669 670 if (config.buildInfo) { 671 lines.push("buildInfo:"); 672 if (config.buildInfo.compiler) { 673 lines.push(` compiler: "${config.buildInfo.compiler}"`); 674 } 675 if (config.buildInfo.gccVersion) { 676 lines.push(` gccVersion: "${config.buildInfo.gccVersion}"`); 677 } 678 if (config.buildInfo.buildSalt) { 679 lines.push(` buildSalt: "${config.buildInfo.buildSalt}"`); 680 } 681 } 682 683 lines.push(""); 684 lines.push("config:"); 685 686 for (const [key, value] of Object.entries(config.flatConfig)) { 687 const yamlValue = 688 value === null 689 ? "null" 690 : typeof value === "string" 691 ? `"${value}"` 692 : value; 693 lines.push(` ${key}: ${yamlValue}`); 694 } 695 696 return lines.join("\n"); 697 } 698 699 /** 700 * Serialize to Makefile-compatible format 701 */ 702 static toMakefile(config: KernelConfig): string { 703 const lines: string[] = []; 704 lines.push("# Kernel configuration as Makefile variables"); 705 lines.push(""); 706 707 for (const [key, value] of Object.entries(config.flatConfig)) { 708 if (value === null) { 709 lines.push(`# ${key} is not set`); 710 } else { 711 const makeKey = key.replace("CONFIG_", ""); 712 if (value === "y") { 713 lines.push(`${makeKey} := y`); 714 } else if (value === "m") { 715 lines.push(`${makeKey} := m`); 716 } else if (typeof value === "number") { 717 lines.push(`${makeKey} := ${value}`); 718 } else { 719 lines.push(`${makeKey} := ${value}`); 720 } 721 } 722 } 723 724 return lines.join("\n"); 725 } 726 727 /** 728 * Serialize only enabled options 729 */ 730 static toEnabledOnly(config: KernelConfig): string { 731 const lines: string[] = []; 732 733 for (const [key, value] of Object.entries(config.flatConfig)) { 734 if (value === "y" || value === "m") { 735 lines.push(`${key}=${value}`); 736 } 737 } 738 739 return lines.join("\n"); 740 } 741 742 /** 743 * Serialize to shell script format 744 */ 745 static toShellScript(config: KernelConfig): string { 746 const lines: string[] = []; 747 lines.push("#!/bin/bash"); 748 lines.push("# Kernel configuration as shell variables"); 749 lines.push(""); 750 751 for (const [key, value] of Object.entries(config.flatConfig)) { 752 if (!value) { 753 lines.push(`# ${key} is not set`); 754 } else { 755 const shellValue = 756 typeof value === "string" && 757 value !== "y" && 758 value !== "m" && 759 value !== "n" 760 ? `"${value}"` 761 : value; 762 lines.push(`export ${key}=${shellValue}`); 763 } 764 } 765 766 return lines.join("\n"); 767 } 768 769 /** 770 * Serialize differences between two configs 771 */ 772 static toDiff(oldConfig: KernelConfig, newConfig: KernelConfig): string { 773 const lines: string[] = []; 774 lines.push("# Configuration differences"); 775 lines.push(""); 776 777 const allKeys = new Set([ 778 ...Object.keys(oldConfig.flatConfig), 779 ...Object.keys(newConfig.flatConfig), 780 ]); 781 782 for (const key of allKeys) { 783 const oldValue = oldConfig.flatConfig[key]; 784 const newValue = newConfig.flatConfig[key]; 785 786 if (oldValue !== newValue) { 787 if (oldValue === undefined) { 788 lines.push(`+ ${key}=${newValue}`); 789 } else if (newValue === undefined) { 790 lines.push(`- ${key}=${oldValue}`); 791 } else { 792 lines.push(`- ${key}=${oldValue}`); 793 lines.push(`+ ${key}=${newValue}`); 794 } 795 } 796 } 797 798 return lines.join("\n"); 799 } 800} 801 802export const validateKernelConfig = ( 803 data: unknown 804): ReturnType<typeof KernelConfigSchema.safeParse> => { 805 return KernelConfigSchema.safeParse(data); 806}; 807 808export const parseKernelConfigFile = (content: string): KernelConfig => { 809 const parsed = KernelConfigParser.parse(content); 810 const validated = KernelConfigSchema.parse(parsed); 811 return validated; 812};