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