1use std::path::Path;
25
26use anyhow::{Context, Result, bail};
27
28use super::super::kernel_cmd::EMBEDDED_KCONFIG;
29use super::make::{MAKE_POLL_INTERVAL, MAKE_TIMEOUT, run_make_captured};
30
31pub(super) fn all_fragment_lines_present(fragment: &str, config: &str) -> bool {
59 let existing: std::collections::HashSet<&str> = config.lines().map(str::trim).collect();
60 fragment
61 .lines()
62 .map(str::trim)
63 .filter(|t| is_kconfig_semantic_line(t))
64 .all(|t| existing.contains(t))
65}
66
67pub(super) fn is_kconfig_semantic_line(trimmed: &str) -> bool {
79 if trimmed.is_empty() {
80 return false;
81 }
82 if let Some(rest) = trimmed.strip_prefix('#') {
83 let rest = rest.trim_start();
87 return rest.starts_with("CONFIG_") && rest.ends_with(" is not set");
88 }
89 true
93}
94
95pub fn read_extra_kconfig(path: &Path, cli_label: &str) -> std::result::Result<String, String> {
119 let display = path.display();
120 let bytes = match std::fs::read(path) {
121 Ok(b) => b,
122 Err(e) => {
123 let msg = match e.kind() {
124 std::io::ErrorKind::NotFound => {
125 format!(
126 "--extra-kconfig {display}: file not found; check the \
127 path spelling and that the file exists"
128 )
129 }
130 std::io::ErrorKind::IsADirectory => {
131 format!(
132 "--extra-kconfig {display}: is a directory; pass a \
133 regular file containing kconfig fragment lines"
134 )
135 }
136 std::io::ErrorKind::PermissionDenied => {
137 format!(
138 "--extra-kconfig {display}: permission denied; check \
139 file ownership and mode (the kconfig fragment must \
140 be readable by the current user)"
141 )
142 }
143 _ => format!("--extra-kconfig {display}: {e}"),
144 };
145 return Err(msg);
146 }
147 };
148
149 if bytes.is_empty() {
154 let path_str = display.to_string();
155 tracing::warn!(
156 cli_label = cli_label,
157 path = %path_str,
158 "--extra-kconfig file is empty; the build will land at a \
159 distinct cache slot but no user symbols will merge into the \
160 configuration. Did you mean to populate {path_str} with \
161 CONFIG_X=... lines?",
162 );
163 }
164
165 String::from_utf8(bytes).map_err(|_| {
166 format!(
167 "--extra-kconfig {display}: file is not valid UTF-8; kconfig \
168 fragments must be ASCII text"
169 )
170 })
171}
172
173pub fn append_extra_kconfig_suffix(cache_key: &mut String, extra: Option<&str>) {
186 if let Some(content) = extra {
187 cache_key.push_str("-xkc");
188 cache_key.push_str(&crate::extra_kconfig_hash(content));
189 }
190}
191
192pub(super) fn warn_extra_kconfig_overrides_baked_in(extra: &str, cli_label: &str) {
210 let mut baked: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
214 for raw in EMBEDDED_KCONFIG.lines() {
215 let line = raw.trim();
216 if let Some(sym) = parse_kconfig_symbol(line) {
217 baked.insert(sym, line);
218 }
219 }
220
221 for raw in extra.lines() {
222 let line = raw.trim();
223 let Some(user_sym) = parse_kconfig_symbol(line) else {
224 continue;
225 };
226 let Some(baked_line) = baked.get(user_sym) else {
227 continue;
228 };
229 if *baked_line == line {
230 continue;
231 }
232 tracing::warn!(
233 cli_label = cli_label,
234 symbol = user_sym,
235 was = *baked_line,
236 now = line,
237 "--extra-kconfig overrides baked-in {user_sym} (was {}, now {})",
238 render_kconfig_value(baked_line, user_sym),
239 render_kconfig_value(line, user_sym),
240 );
241 }
242}
243
244pub(super) fn parse_kconfig_symbol(line: &str) -> Option<&str> {
250 if let Some(rest) = line.strip_prefix("# ")
251 && let Some(sym) = rest.strip_suffix(" is not set")
252 && sym.starts_with("CONFIG_")
253 {
254 return Some(sym);
255 }
256 if line.starts_with("CONFIG_")
257 && let Some((sym, _)) = line.split_once('=')
258 {
259 return Some(sym);
260 }
261 None
262}
263
264pub(super) fn render_kconfig_value<'a>(line: &'a str, sym: &str) -> &'a str {
271 if let Some(value) = line.strip_prefix(sym)
272 && value.starts_with('=')
273 {
274 return value;
275 }
276 if line == format!("# {sym} is not set") {
277 return "is not set";
278 }
279 line
280}
281
282pub(super) fn warn_dropped_extra_kconfig_lines(kernel_dir: &Path, extra: &str, cli_label: &str) {
303 let config_path = kernel_dir.join(".config");
304 let Ok(final_config) = std::fs::read_to_string(&config_path) else {
305 return;
306 };
307 let final_lines: std::collections::HashSet<&str> =
308 final_config.lines().map(str::trim).collect();
309
310 for raw_line in extra.lines() {
311 let line = raw_line.trim();
312 if line.is_empty() {
313 continue;
314 }
315 let is_disable_directive = line.starts_with("# CONFIG_") && line.ends_with(" is not set");
319 let is_assignment = line.starts_with("CONFIG_") && line.contains('=');
320 if !is_disable_directive && !is_assignment {
321 continue;
322 }
323 if final_lines.contains(line) {
324 continue;
325 }
326 let sym_name = if is_assignment {
332 line.split('=').next().unwrap_or(line)
333 } else {
334 line.trim_start_matches("# ")
335 .trim_end_matches(" is not set")
336 };
337 let final_state = final_config
338 .lines()
339 .find(|l| {
340 let t = l.trim();
341 t.starts_with(&format!("{sym_name}=")) || t == format!("# {sym_name} is not set")
342 })
343 .map(str::trim)
344 .unwrap_or(
345 "(absent — symbol not present in .config; likely \
346 disabled or unrecognized by kconfig)",
347 );
348 tracing::warn!(
349 cli_label = cli_label,
350 requested = line,
351 final_state = final_state,
352 "--extra-kconfig line did not survive `make olddefconfig` (likely an \
353 unmet dependency or unrecognized symbol)"
354 );
355 }
356}
357
358pub(crate) fn configure_kernel(
375 kernel_dir: &Path,
376 fragment: &str,
377 progress: Option<&crate::cli::FetchProgress>,
378) -> Result<()> {
379 let config_path = kernel_dir.join(".config");
380 if !config_path.exists() {
381 run_make_captured(
382 kernel_dir,
383 &["defconfig"],
384 progress,
385 MAKE_TIMEOUT,
386 MAKE_POLL_INTERVAL,
387 )?;
388 }
389
390 let config_content = std::fs::read_to_string(&config_path)?;
391 if all_fragment_lines_present(fragment, &config_content) {
392 return Ok(());
393 }
394
395 let mut config = std::fs::OpenOptions::new()
396 .append(true)
397 .open(&config_path)?;
398 std::io::Write::write_all(&mut config, fragment.as_bytes())?;
399
400 run_make_captured(
401 kernel_dir,
402 &["olddefconfig"],
403 progress,
404 MAKE_TIMEOUT,
405 MAKE_POLL_INTERVAL,
406 )?;
407
408 Ok(())
409}
410
411const VALIDATE_CONFIG_CRITICAL: &[(&str, &str)] = &[
425 (
426 "CONFIG_SCHED_CLASS_EXT",
427 "depends on CONFIG_DEBUG_INFO_BTF — ensure pahole >= 1.16 is installed (dwarves package)",
428 ),
429 (
430 "CONFIG_DEBUG_INFO_BTF",
431 "requires pahole >= 1.16 (dwarves package)",
432 ),
433 ("CONFIG_BPF_SYSCALL", "required for BPF program loading"),
434 (
435 "CONFIG_FTRACE",
436 "gate for all tracing infrastructure — arm64 defconfig disables it, \
437 silently dropping KPROBE_EVENTS and BPF_EVENTS",
438 ),
439 (
440 "CONFIG_KPROBE_EVENTS",
441 "required for ktstr probe pipeline (depends on FTRACE + KPROBES)",
442 ),
443 (
444 "CONFIG_BPF_EVENTS",
445 "required for BPF kprobe/tracepoint attachment (depends on KPROBE_EVENTS + PERF_EVENTS)",
446 ),
447 (
448 "CONFIG_VIRTIO_BLK",
449 "required for ktstr DiskConfig — backs /dev/vd* in the guest. Depends on VIRTIO + BLOCK; \
450 a user --extra-kconfig that strips BLOCK would silently disable this and disk-IO WorkTypes \
451 would fail with a confusing 'no /dev/vda' inside the guest instead of a clear build error",
452 ),
453 (
454 "CONFIG_ACPI",
455 "the linchpin of the x86 PCI transport — the guest parses the MCFG (ECAM base), the \
456 _SB.PCI0 DSDT _PRT (PCI INTx -> GSI routing), and the FADT PM register blocks only when \
457 ACPI is enabled. x86_64 defconfig sets it, but an --extra-kconfig that strips it would \
458 drop PCI_MMCONFIG too (default-y only on PCI + ACPI) and PCI INTx would fall back to \
459 legacy MP-table routing with no NIC entry — the exact silent failure the FADT PM blocks \
460 + _PRT exist to fix. Validated here (not only via PCI_MMCONFIG) because it is the most \
461 catastrophic to lose",
462 ),
463 (
464 "CONFIG_PCI",
465 "required for the virtio-PCI transport — the host bridge at 00:00.0 and any PCI device \
466 (NICs) enumerate only with PCI compiled in. x86_64 defconfig sets it, but an \
467 --extra-kconfig that strips it would silently leave the guest with no /sys/bus/pci and \
468 every PCI device invisible instead of a clear build error",
469 ),
470 (
471 "CONFIG_VIRTIO_PCI",
472 "the guest virtio-PCI DRIVER that binds the virtio-net NIC (a PCI function). Without it \
473 the guest enumerates the PCI device but no driver attaches, so eth* never appears and \
474 every NIC WorkType/e2e fails with a confusing 'no eth0' inside the guest instead of a \
475 clear build error. Distinct from CONFIG_PCI (the bus) and CONFIG_VIRTIO_NET (the \
476 transport-agnostic net driver)",
477 ),
478];
479
480#[cfg(target_arch = "x86_64")]
488const VALIDATE_CONFIG_CRITICAL_X86: &[(&str, &str)] = &[(
489 "CONFIG_PCI_MMCONFIG",
490 "backs ECAM (extended config space, reg >= 256) which the MCFG table and _SB.PCI0 DSDT \
491 window describe and the PCI enumeration e2e exercises (a host-bridge-class device is \
492 sized to 4096-byte config and read at reg 0x100 via ECAM). default-y on x86_64 when \
493 PCI + ACPI are set, but an --extra-kconfig that strips it would silently drop ECAM — \
494 base config still works over CAM, so the loss is invisible until an extended-config \
495 access (the e2e, or a future MSI-X device) fails",
496)];
497
498fn critical_config_options() -> Vec<(&'static str, &'static str)> {
503 #[cfg_attr(not(target_arch = "x86_64"), allow(unused_mut))]
504 let mut opts = VALIDATE_CONFIG_CRITICAL.to_vec();
505 #[cfg(target_arch = "x86_64")]
506 opts.extend_from_slice(VALIDATE_CONFIG_CRITICAL_X86);
507 opts
508}
509
510pub fn validate_kernel_config(kernel_dir: &std::path::Path) -> Result<()> {
517 let config_path = kernel_dir.join(".config");
518 let config = std::fs::read_to_string(&config_path)
519 .with_context(|| format!("read {}", config_path.display()))?;
520
521 let existing: std::collections::HashSet<&str> = config.lines().map(str::trim).collect();
535
536 let mut missing = Vec::new();
537 let critical = critical_config_options();
538 for &(option, hint) in &critical {
539 let enabled = format!("{option}=y");
540 if !existing.contains(enabled.as_str()) {
541 missing.push((option, hint));
542 }
543 }
544
545 if !missing.is_empty() {
546 let mut msg =
547 String::from("kernel build completed but critical config options are missing:\n");
548 for (option, hint) in &missing {
549 msg.push_str(&format!(" {option} not set — {hint}\n"));
550 }
551 msg.push_str(
552 "\nThe kernel build system silently disables options whose dependencies \
553 are not met. Install missing tools and rebuild with --force.",
554 );
555 bail!("{msg}");
556 }
557 Ok(())
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
573 fn critical_options_are_in_embedded_kconfig() {
574 let fragment = crate::EMBEDDED_KCONFIG;
575 for &(option, _) in &critical_config_options() {
576 let enabled = format!("{option}=y");
577 assert!(
578 fragment.lines().any(|l| l.trim() == enabled),
579 "VALIDATE_CONFIG_CRITICAL lists {option:?} but ktstr.kconfig does not \
580 enable it; either add `{option}=y` to the fragment or drop the entry \
581 from VALIDATE_CONFIG_CRITICAL",
582 );
583 }
584 }
585
586 #[test]
587 fn validate_kernel_config_all_present() {
588 let dir = tempfile::TempDir::new().unwrap();
589 let content: String = critical_config_options()
593 .iter()
594 .map(|(option, _)| format!("{option}=y\n"))
595 .collect();
596 std::fs::write(dir.path().join(".config"), content).unwrap();
597 assert!(validate_kernel_config(dir.path()).is_ok());
598 }
599
600 #[test]
601 fn validate_kernel_config_missing_btf() {
602 let dir = tempfile::TempDir::new().unwrap();
603 std::fs::write(
604 dir.path().join(".config"),
605 "CONFIG_SCHED_CLASS_EXT=y\n\
606 CONFIG_BPF_SYSCALL=y\n\
607 CONFIG_FTRACE=y\n\
608 CONFIG_KPROBE_EVENTS=y\n\
609 CONFIG_BPF_EVENTS=y\n",
610 )
611 .unwrap();
612 let err = validate_kernel_config(dir.path()).unwrap_err();
613 let msg = format!("{err}");
614 assert!(msg.contains("CONFIG_DEBUG_INFO_BTF"), "got: {msg}");
615 }
616
617 #[test]
618 fn validate_kernel_config_missing_multiple() {
619 let dir = tempfile::TempDir::new().unwrap();
620 std::fs::write(dir.path().join(".config"), "CONFIG_BPF_SYSCALL=y\n").unwrap();
621 let err = validate_kernel_config(dir.path()).unwrap_err();
622 let msg = format!("{err}");
623 assert!(msg.contains("CONFIG_SCHED_CLASS_EXT"), "got: {msg}");
624 assert!(msg.contains("CONFIG_DEBUG_INFO_BTF"), "got: {msg}");
625 }
626
627 #[test]
628 fn validate_kernel_config_no_config_file() {
629 let dir = tempfile::TempDir::new().unwrap();
630 assert!(validate_kernel_config(dir.path()).is_err());
631 }
632
633 #[test]
640 fn validate_kernel_config_trim_handles_crlf_and_trailing_whitespace() {
641 let dir = tempfile::TempDir::new().unwrap();
642 let mut content = String::new();
648 let critical = critical_config_options();
649 for (i, (option, _)) in critical.iter().enumerate() {
650 if i % 2 == 0 {
651 content.push_str(&format!("{option}=y\r\n")); } else {
653 content.push_str(&format!("{option}=y \n")); }
655 }
656 std::fs::write(dir.path().join(".config"), content).unwrap();
657 let result = validate_kernel_config(dir.path());
658 assert!(
659 result.is_ok(),
660 "validate_kernel_config must trim per-line whitespace \
661 before the HashSet probe; got: {result:?}",
662 );
663 }
664
665 #[test]
668 fn parse_kconfig_symbol_assignment() {
669 assert_eq!(parse_kconfig_symbol("CONFIG_FOO=y"), Some("CONFIG_FOO"));
670 assert_eq!(parse_kconfig_symbol("CONFIG_FOO=m"), Some("CONFIG_FOO"));
671 assert_eq!(parse_kconfig_symbol("CONFIG_FOO=n"), Some("CONFIG_FOO"));
672 assert_eq!(
673 parse_kconfig_symbol("CONFIG_BAR=\"value\""),
674 Some("CONFIG_BAR")
675 );
676 }
677
678 #[test]
679 fn parse_kconfig_symbol_disable_directive() {
680 assert_eq!(
681 parse_kconfig_symbol("# CONFIG_FOO is not set"),
682 Some("CONFIG_FOO")
683 );
684 }
685
686 #[test]
687 fn parse_kconfig_symbol_rejects_free_text_comment() {
688 assert!(parse_kconfig_symbol("# user note about foo").is_none());
689 assert!(parse_kconfig_symbol("#").is_none());
690 assert!(parse_kconfig_symbol("# this is a doc line").is_none());
691 }
692
693 #[test]
694 fn parse_kconfig_symbol_rejects_blank_and_non_config() {
695 assert!(parse_kconfig_symbol("").is_none());
696 assert!(parse_kconfig_symbol("not a kconfig line").is_none());
697 assert!(parse_kconfig_symbol("FOO=y").is_none());
698 }
699
700 #[test]
701 fn render_kconfig_value_assignment_returns_value_with_equals() {
702 assert_eq!(render_kconfig_value("CONFIG_FOO=y", "CONFIG_FOO"), "=y");
703 assert_eq!(render_kconfig_value("CONFIG_FOO=n", "CONFIG_FOO"), "=n");
704 assert_eq!(
705 render_kconfig_value("CONFIG_BAR=\"value\"", "CONFIG_BAR"),
706 "=\"value\""
707 );
708 }
709
710 #[test]
711 fn render_kconfig_value_disable_returns_is_not_set() {
712 assert_eq!(
713 render_kconfig_value("# CONFIG_FOO is not set", "CONFIG_FOO"),
714 "is not set"
715 );
716 }
717
718 #[test]
719 fn render_kconfig_value_falls_back_to_full_line_on_unknown_shape() {
720 let s = "CONFIG_FOO without equals";
721 assert_eq!(render_kconfig_value(s, "CONFIG_FOO"), s);
722 }
723
724 #[test]
739 #[tracing_test::traced_test]
740 fn warn_extra_kconfig_overrides_emits_on_actual_override() {
741 assert!(
742 EMBEDDED_KCONFIG.lines().any(|l| l.trim() == "CONFIG_BPF=y"),
743 "fixture invariant: CONFIG_BPF must be baked in as =y",
744 );
745 let user = "# CONFIG_BPF is not set\n";
746 warn_extra_kconfig_overrides_baked_in(user, "test");
747 assert!(
748 logs_contain("overrides baked-in CONFIG_BPF"),
749 "override of a baked-in symbol must emit the WARN naming it",
750 );
751 assert!(
753 logs_contain("(was =y, now is not set)"),
754 "WARN must render the baked before-value and the user after-value",
755 );
756 for field in [
758 "symbol=\"CONFIG_BPF\"",
759 "was=\"CONFIG_BPF=y\"",
760 "now=\"# CONFIG_BPF is not set\"",
761 "cli_label=\"test\"",
762 ] {
763 assert!(
764 logs_contain(field),
765 "WARN must include structured field `{field}`",
766 );
767 }
768 }
769
770 #[test]
771 #[tracing_test::traced_test]
772 fn warn_extra_kconfig_overrides_silent_on_empty_fragment() {
773 warn_extra_kconfig_overrides_baked_in("", "test");
774 assert!(
775 !logs_contain("overrides baked-in"),
776 "an empty fragment overrides nothing — no WARN",
777 );
778 }
779
780 #[test]
781 #[tracing_test::traced_test]
782 fn warn_extra_kconfig_overrides_silent_on_novel_symbol() {
783 let novel = "CONFIG_KTSTR_TEST_NOVEL_SYMBOL_OVERRIDE_TEST=y\n";
784 assert!(
785 !EMBEDDED_KCONFIG.contains("CONFIG_KTSTR_TEST_NOVEL_SYMBOL_OVERRIDE_TEST"),
786 "test fixture must use a symbol absent from EMBEDDED_KCONFIG"
787 );
788 warn_extra_kconfig_overrides_baked_in(novel, "test");
789 assert!(
790 !logs_contain("overrides baked-in"),
791 "a symbol not present in EMBEDDED_KCONFIG overrides nothing — no WARN",
792 );
793 }
794
795 #[test]
799 #[tracing_test::traced_test]
800 fn warn_extra_kconfig_overrides_silent_on_matching_assignment() {
801 assert!(
802 EMBEDDED_KCONFIG.lines().any(|l| l.trim() == "CONFIG_BPF=y"),
803 "fixture invariant: CONFIG_BPF must be baked in as =y",
804 );
805 let user = "CONFIG_BPF=y\n";
806 warn_extra_kconfig_overrides_baked_in(user, "test");
807 assert!(
808 !logs_contain("overrides baked-in"),
809 "a user line identical to the baked-in line is not an override — no WARN",
810 );
811 }
812
813 #[test]
814 #[tracing_test::traced_test]
815 fn warn_extra_kconfig_overrides_silent_on_free_text_comments() {
816 let user = "# this is a comment about something\n# another comment\n";
817 warn_extra_kconfig_overrides_baked_in(user, "test");
818 assert!(
819 !logs_contain("overrides baked-in"),
820 "free-text `#` comments are not kconfig assignments — no WARN",
821 );
822 }
823
824 #[test]
827 fn read_extra_kconfig_returns_content_for_valid_file() {
828 let dir = tempfile::TempDir::new().unwrap();
829 let path = dir.path().join("frag.kconfig");
830 let content = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
831 std::fs::write(&path, content).unwrap();
832 let got = read_extra_kconfig(&path, "test").unwrap();
833 assert_eq!(got, content, "content must round-trip byte-for-byte");
834 }
835
836 #[test]
837 fn read_extra_kconfig_not_found_arm_names_path_and_intent() {
838 let dir = tempfile::TempDir::new().unwrap();
839 let missing = dir.path().join("does-not-exist.kconfig");
840 let display = missing.display().to_string();
841 let err = read_extra_kconfig(&missing, "cargo ktstr").expect_err("missing file must Err");
842 assert!(
843 err.contains(&display),
844 "ENOENT message must name the literal path: {err}"
845 );
846 assert!(
847 err.contains("--extra-kconfig"),
848 "ENOENT message must name the flag: {err}"
849 );
850 assert!(
851 err.contains("file not found"),
852 "ENOENT arm must surface a `file not found` token: {err}"
853 );
854 }
855
856 #[test]
857 fn read_extra_kconfig_directory_arm_distinguishes_from_not_found() {
858 let dir = tempfile::TempDir::new().unwrap();
859 let display = dir.path().display().to_string();
860 let err =
861 read_extra_kconfig(dir.path(), "cargo ktstr").expect_err("directory path must Err");
862 assert!(
863 err.contains(&display),
864 "EISDIR message must name the path: {err}"
865 );
866 assert!(
867 err.contains("is a directory"),
868 "EISDIR arm must surface its specific token: {err}"
869 );
870 }
871
872 #[test]
873 fn read_extra_kconfig_invalid_utf8_arm_names_constraint() {
874 let dir = tempfile::TempDir::new().unwrap();
875 let path = dir.path().join("binary.bin");
876 std::fs::write(&path, [0xFFu8, 0xFE, 0x00, 0x42]).unwrap();
877 let err = read_extra_kconfig(&path, "cargo ktstr").expect_err("non-UTF-8 file must Err");
878 assert!(
879 err.contains("not valid UTF-8"),
880 "UTF-8 arm must surface the constraint name: {err}"
881 );
882 assert!(
883 err.contains("ASCII text"),
884 "UTF-8 arm must mention the kconfig content constraint: {err}"
885 );
886 }
887
888 #[test]
889 fn read_extra_kconfig_empty_file_returns_empty_string() {
890 let dir = tempfile::TempDir::new().unwrap();
891 let path = dir.path().join("empty.kconfig");
892 std::fs::write(&path, "").unwrap();
893 let got = read_extra_kconfig(&path, "cargo ktstr").unwrap();
894 assert_eq!(
895 got, "",
896 "empty file must round-trip as empty String, not Err"
897 );
898 }
899
900 #[test]
901 #[cfg(unix)]
902 fn read_extra_kconfig_follows_symlink_chain() {
903 use std::os::unix::fs::symlink;
904 let dir = tempfile::TempDir::new().unwrap();
905 let target = dir.path().join("real.kconfig");
906 let link = dir.path().join("link.kconfig");
907 std::fs::write(&target, "CONFIG_BPF=y\n").unwrap();
908 symlink(&target, &link).unwrap();
909 let got = read_extra_kconfig(&link, "test").unwrap();
910 assert_eq!(
911 got, "CONFIG_BPF=y\n",
912 "symlink must resolve to target content"
913 );
914 }
915
916 #[test]
930 #[tracing_test::traced_test]
931 fn warn_dropped_extra_kconfig_lines_silent_when_config_missing() {
932 let dir = tempfile::TempDir::new().unwrap();
933 let extra = "CONFIG_FOO=y\n";
934 warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
935 assert!(
936 !logs_contain("did not survive"),
937 "a missing .config collapses to a silent return — no WARN",
938 );
939 }
940
941 #[test]
942 #[tracing_test::traced_test]
943 fn warn_dropped_extra_kconfig_lines_silent_when_all_present() {
944 let dir = tempfile::TempDir::new().unwrap();
945 std::fs::write(dir.path().join(".config"), "CONFIG_FOO=y\nCONFIG_BAR=m\n").unwrap();
946 let extra = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
947 warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
948 assert!(
949 !logs_contain("did not survive"),
950 "every requested line present verbatim in .config — no WARN",
951 );
952 }
953
954 #[test]
959 #[tracing_test::traced_test]
960 fn warn_dropped_extra_kconfig_lines_emits_on_dropped_line() {
961 let dir = tempfile::TempDir::new().unwrap();
962 std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
963 let extra = "CONFIG_KTSTR_DROPPED_TEST_NOVEL=y\n";
964 warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
965 assert!(
966 logs_contain("did not survive"),
967 "a dropped requested line must emit the WARN",
968 );
969 assert!(
970 logs_contain("requested=\"CONFIG_KTSTR_DROPPED_TEST_NOVEL=y\""),
971 "WARN must carry the requested line",
972 );
973 assert!(
975 logs_contain("symbol not present in .config"),
976 "WARN's final_state must surface the absent-symbol sentinel",
977 );
978 }
979
980 #[test]
985 #[tracing_test::traced_test]
986 fn warn_dropped_extra_kconfig_lines_emits_on_rewritten_line() {
987 let dir = tempfile::TempDir::new().unwrap();
988 std::fs::write(
989 dir.path().join(".config"),
990 "# CONFIG_KTSTR_REWRITE_TEST is not set\n",
991 )
992 .unwrap();
993 let extra = "CONFIG_KTSTR_REWRITE_TEST=y\n";
994 warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
995 assert!(
996 logs_contain("did not survive"),
997 "a rewritten requested line must emit the WARN",
998 );
999 assert!(
1000 logs_contain("requested=\"CONFIG_KTSTR_REWRITE_TEST=y\""),
1001 "WARN must carry the requested =y line",
1002 );
1003 assert!(
1005 logs_contain("final_state=\"# CONFIG_KTSTR_REWRITE_TEST is not set\""),
1006 "WARN must enrich final_state with the rewritten .config value",
1007 );
1008 }
1009
1010 #[test]
1014 #[tracing_test::traced_test]
1015 fn warn_dropped_extra_kconfig_lines_silent_on_free_text_comments() {
1016 let dir = tempfile::TempDir::new().unwrap();
1017 std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
1018 let extra = "# decorative header\nCONFIG_BPF=y\n";
1019 warn_dropped_extra_kconfig_lines(dir.path(), extra, "test");
1020 assert!(
1021 !logs_contain("did not survive"),
1022 "free-text comments are skipped and the assignment is present — no WARN",
1023 );
1024 }
1025
1026 #[test]
1029 fn configure_kernel_appends_missing() {
1030 let dir = tempfile::TempDir::new().unwrap();
1031 std::fs::write(dir.path().join(".config"), "CONFIG_BPF=y\n").unwrap();
1032 std::fs::write(dir.path().join("Makefile"), "olddefconfig:\n\t@true\n").unwrap();
1033 let fragment = "CONFIG_EXTRA=y\n";
1034 configure_kernel(dir.path(), fragment, None).unwrap();
1035 let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
1036 assert!(config.contains("CONFIG_EXTRA=y"));
1037 assert!(config.contains("CONFIG_BPF=y"));
1038 }
1039
1040 #[test]
1041 fn configure_kernel_skips_when_present() {
1042 let dir = tempfile::TempDir::new().unwrap();
1043 let initial = "CONFIG_BPF=y\nCONFIG_EXTRA=y\n";
1044 std::fs::write(dir.path().join(".config"), initial).unwrap();
1045 let fragment = "CONFIG_EXTRA=y\n";
1046 configure_kernel(dir.path(), fragment, None).unwrap();
1047 let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
1048 assert_eq!(config, initial);
1049 }
1050
1051 #[test]
1057 fn configure_kernel_rejects_numeric_prefix_false_match() {
1058 let dir = tempfile::TempDir::new().unwrap();
1059 let initial = "CONFIG_NR_CPUS=128\n";
1060 std::fs::write(dir.path().join(".config"), initial).unwrap();
1061 std::fs::write(dir.path().join("Makefile"), "olddefconfig:\n\t@true\n").unwrap();
1062 let fragment = "CONFIG_NR_CPUS=1\n";
1063 configure_kernel(dir.path(), fragment, None).unwrap();
1064 let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
1065 assert!(
1066 config.lines().any(|l| l.trim() == "CONFIG_NR_CPUS=1"),
1067 "CONFIG_NR_CPUS=1 must be appended as its own line: {config:?}"
1068 );
1069 assert!(
1070 config.lines().any(|l| l.trim() == "CONFIG_NR_CPUS=128"),
1071 "original CONFIG_NR_CPUS=128 must be preserved: {config:?}"
1072 );
1073 }
1074
1075 #[test]
1083 fn configure_kernel_runs_defconfig_when_config_absent() {
1084 let dir = tempfile::TempDir::new().unwrap();
1085 std::fs::write(
1086 dir.path().join("Makefile"),
1087 "defconfig:\n\t@printf 'CONFIG_BASE=y\\n' > .config\nolddefconfig:\n\t@true\n",
1088 )
1089 .unwrap();
1090 let fragment = "CONFIG_EXTRA=y\n";
1091 configure_kernel(dir.path(), fragment, None).unwrap();
1092 let config = std::fs::read_to_string(dir.path().join(".config")).unwrap();
1093 assert!(
1094 config.contains("CONFIG_BASE=y"),
1095 "defconfig target must have created the base .config: {config:?}"
1096 );
1097 assert!(
1098 config.contains("CONFIG_EXTRA=y"),
1099 "fragment must be appended after defconfig: {config:?}"
1100 );
1101 }
1102
1103 #[test]
1106 fn all_fragment_lines_present_exact_match() {
1107 let config = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
1108 assert!(all_fragment_lines_present("CONFIG_FOO=y\n", config));
1109 assert!(all_fragment_lines_present("CONFIG_BAR=m\n", config));
1110 assert!(all_fragment_lines_present(
1111 "CONFIG_FOO=y\nCONFIG_BAR=m\n",
1112 config
1113 ));
1114 }
1115
1116 #[test]
1117 fn all_fragment_lines_present_numeric_prefix_not_present() {
1118 let config = "CONFIG_NR_CPUS=128\n";
1119 assert!(!all_fragment_lines_present("CONFIG_NR_CPUS=1\n", config));
1120 assert!(!all_fragment_lines_present("CONFIG_NR_CPUS=12\n", config));
1121 }
1122
1123 #[test]
1124 fn all_fragment_lines_present_disable_directive_participates() {
1125 let config = "CONFIG_BPF=y\n";
1126 assert!(!all_fragment_lines_present(
1127 "# CONFIG_BPF is not set\n",
1128 config
1129 ));
1130 }
1131
1132 #[test]
1133 fn all_fragment_lines_present_empty_lines_skipped() {
1134 let config = "CONFIG_FOO=y\n";
1135 assert!(all_fragment_lines_present("\n\nCONFIG_FOO=y\n\n", config));
1136 }
1137
1138 #[test]
1139 fn all_fragment_lines_present_free_text_comment_stripped() {
1140 let config = "CONFIG_FOO=y\n";
1141 let fragment = "# Build for testing scx schedulers\nCONFIG_FOO=y\n";
1142 assert!(
1143 all_fragment_lines_present(fragment, config),
1144 "free-text comment must not block the present-in-config check"
1145 );
1146 }
1147
1148 #[test]
1149 fn all_fragment_lines_present_disable_directive_still_participates() {
1150 let config = "CONFIG_FOO=y\n# CONFIG_BAR is not set\n";
1151 let fragment_present = "# CONFIG_BAR is not set\n";
1152 assert!(
1153 all_fragment_lines_present(fragment_present, config),
1154 "disable directive present in config must satisfy probe"
1155 );
1156 let config_missing = "CONFIG_FOO=y\n";
1157 assert!(
1158 !all_fragment_lines_present(fragment_present, config_missing),
1159 "disable directive missing from config must fail probe"
1160 );
1161 }
1162
1163 #[test]
1164 fn all_fragment_lines_present_section_header_comment_stripped() {
1165 let config = "CONFIG_FOO=y\nCONFIG_BAR=m\n";
1166 let fragment = "\
1167# == BPF support ==\n\
1168CONFIG_FOO=y\n\
1169# == Tracing ==\n\
1170CONFIG_BAR=m\n";
1171 assert!(all_fragment_lines_present(fragment, config));
1172 }
1173
1174 #[test]
1175 fn all_fragment_lines_present_changed_baked_in_with_sched_ext_present() {
1176 let config = "CONFIG_SCHED_CLASS_EXT=y\nCONFIG_NR_CPUS=64\n";
1184 let fragment = "CONFIG_SCHED_CLASS_EXT=y\nCONFIG_NR_CPUS=512\n";
1185 assert!(
1186 !all_fragment_lines_present(fragment, config),
1187 "changed CONFIG_NR_CPUS (64->512) must read as not-present even with sched_ext=y"
1188 );
1189 }
1190
1191 #[test]
1194 fn is_kconfig_semantic_line_classifies_assignment_disable_and_comment() {
1195 assert!(is_kconfig_semantic_line("CONFIG_FOO=y"));
1196 assert!(is_kconfig_semantic_line("CONFIG_NR_CPUS=128"));
1197 assert!(is_kconfig_semantic_line("# CONFIG_BPF is not set"));
1198 assert!(is_kconfig_semantic_line("# CONFIG_BPF is not set"));
1199 assert!(!is_kconfig_semantic_line("# Build for testing"));
1200 assert!(!is_kconfig_semantic_line("# == Section header =="));
1201 assert!(!is_kconfig_semantic_line(""));
1202 assert!(!is_kconfig_semantic_line("# CONFIG_FOO is enabled"));
1203 assert!(!is_kconfig_semantic_line("# CONFIG_FOO"));
1204 }
1205}