1#[cfg(feature = "alloc")]
8extern crate alloc;
9
10#[cfg(feature = "alloc")]
11use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
12
13#[cfg(feature = "alloc")]
14use crate::error::KernelError;
15
16#[cfg(feature = "alloc")]
22#[derive(Debug, Clone)]
23pub struct BuildSnapshot {
24 pub toolchain_version: String,
26 pub env_vars: BTreeMap<String, String>,
28 pub timestamp_override: Option<u64>,
30 pub source_hashes: Vec<(String, [u8; 32])>,
32 pub target_triple: String,
34}
35
36#[cfg(feature = "alloc")]
37impl BuildSnapshot {
38 pub fn new() -> Self {
40 Self {
41 toolchain_version: String::new(),
42 env_vars: BTreeMap::new(),
43 timestamp_override: None,
44 source_hashes: Vec::new(),
45 target_triple: String::new(),
46 }
47 }
48}
49
50#[cfg(feature = "alloc")]
51impl Default for BuildSnapshot {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57#[cfg(feature = "alloc")]
59#[derive(Debug, Clone)]
60pub struct BuildManifest {
61 pub port_name: String,
63 pub port_version: String,
65 pub inputs: BuildInputs,
67 pub outputs: BuildOutputs,
69 pub build_duration_ms: u64,
71}
72
73#[cfg(feature = "alloc")]
75#[derive(Debug, Clone)]
76pub struct BuildInputs {
77 pub source_hashes: Vec<(String, [u8; 32])>,
79 pub env_snapshot: BuildSnapshot,
81 pub dependency_versions: BTreeMap<String, String>,
83}
84
85#[cfg(feature = "alloc")]
86impl BuildInputs {
87 pub fn new() -> Self {
89 Self {
90 source_hashes: Vec::new(),
91 env_snapshot: BuildSnapshot::new(),
92 dependency_versions: BTreeMap::new(),
93 }
94 }
95}
96
97#[cfg(feature = "alloc")]
98impl Default for BuildInputs {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104#[cfg(feature = "alloc")]
106#[derive(Debug, Clone)]
107pub struct BuildOutputs {
108 pub file_hashes: Vec<(String, [u8; 32])>,
110 pub total_size: u64,
112 pub file_count: usize,
114}
115
116#[cfg(feature = "alloc")]
117impl BuildOutputs {
118 pub fn new() -> Self {
120 Self {
121 file_hashes: Vec::new(),
122 total_size: 0,
123 file_count: 0,
124 }
125 }
126}
127
128#[cfg(feature = "alloc")]
129impl Default for BuildOutputs {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[cfg(feature = "alloc")]
137#[derive(Debug, Clone)]
138pub struct ReproducibilityResult {
139 pub matching_files: Vec<String>,
141 pub differing_files: Vec<(String, [u8; 32], [u8; 32])>,
144 pub missing_in_a: Vec<String>,
146 pub missing_in_b: Vec<String>,
148}
149
150#[cfg(feature = "alloc")]
151impl ReproducibilityResult {
152 pub fn is_reproducible(&self) -> bool {
154 self.differing_files.is_empty()
155 && self.missing_in_a.is_empty()
156 && self.missing_in_b.is_empty()
157 }
158}
159
160#[cfg(feature = "alloc")]
173pub fn normalize_environment(env: &mut super::ports::BuildEnvironment) {
174 env.env_vars
176 .insert(String::from("SOURCE_DATE_EPOCH"), String::from("0"));
177
178 env.env_vars
180 .insert(String::from("LC_ALL"), String::from("C"));
181 env.env_vars.insert(String::from("LANG"), String::from("C"));
182 env.env_vars
183 .insert(String::from("LANGUAGE"), String::from("C"));
184
185 env.env_vars.insert(String::from("TZ"), String::from("UTC"));
187
188 let keys: Vec<String> = env.env_vars.keys().cloned().collect();
190 for key in keys {
191 if let Some(val) = env.env_vars.get_mut(&key) {
192 *val = canonicalize_path_value(val);
193 }
194 }
195}
196
197#[cfg(feature = "alloc")]
200fn canonicalize_path_value(path: &str) -> String {
201 let mut result = String::with_capacity(path.len());
202 let mut prev_slash = false;
203
204 for ch in path.chars() {
205 if ch == '/' {
206 if !prev_slash {
207 result.push('/');
208 }
209 prev_slash = true;
210 } else {
211 prev_slash = false;
212 result.push(ch);
213 }
214 }
215
216 if result.len() > 1 && result.ends_with('/') {
218 result.pop();
219 }
220
221 result
222}
223
224#[cfg(feature = "alloc")]
230pub fn create_build_manifest(
231 port: &super::ports::Port,
232 env: &super::ports::BuildEnvironment,
233 output_dir: &str,
234) -> Result<BuildManifest, KernelError> {
235 let mut source_hashes = Vec::new();
237 for (i, checksum) in port.checksums.iter().enumerate() {
238 let name = if i < port.sources.len() {
239 port.sources[i].clone()
240 } else {
241 alloc::format!("source-{}", i)
242 };
243 source_hashes.push((name, *checksum));
244 }
245
246 let env_snapshot = BuildSnapshot {
248 toolchain_version: env
249 .get_env("RUSTC_VERSION")
250 .map(String::from)
251 .unwrap_or_default(),
252 env_vars: env.env_vars.clone(),
253 timestamp_override: Some(0), source_hashes: source_hashes.clone(),
255 target_triple: env.get_env("TARGET").map(String::from).unwrap_or_default(),
256 };
257
258 let mut dependency_versions = BTreeMap::new();
260 for dep in &port.dependencies {
261 dependency_versions.insert(dep.clone(), String::from("*"));
262 }
263
264 let inputs = BuildInputs {
265 source_hashes,
266 env_snapshot,
267 dependency_versions,
268 };
269
270 let outputs = compute_outputs(output_dir);
272
273 Ok(BuildManifest {
274 port_name: port.name.clone(),
275 port_version: port.version.clone(),
276 inputs,
277 outputs,
278 build_duration_ms: 0, })
280}
281
282#[cfg(feature = "alloc")]
287fn compute_outputs(output_dir: &str) -> BuildOutputs {
288 let mut outputs = BuildOutputs::new();
289
290 let vfs_lock = match crate::fs::try_get_vfs() {
291 Some(lock) => lock,
292 None => {
293 crate::println!(
294 "[REPRO] VFS unavailable, recording empty outputs for {}",
295 output_dir
296 );
297 return outputs;
298 }
299 };
300
301 let vfs = vfs_lock.read();
302
303 let dir_node = match vfs.resolve_path(output_dir) {
305 Ok(node) => node,
306 Err(_) => {
307 crate::println!(
308 "[REPRO] Output directory not found: {}, recording empty outputs",
309 output_dir
310 );
311 return outputs;
312 }
313 };
314
315 let entries = match dir_node.readdir() {
317 Ok(entries) => entries,
318 Err(_) => {
319 return outputs;
320 }
321 };
322
323 for entry in &entries {
324 if entry.name == "." || entry.name == ".." {
326 continue;
327 }
328
329 if entry.node_type == crate::fs::NodeType::File {
330 let file_path = alloc::format!("{}/{}", output_dir, entry.name);
331 if let Ok(node) = vfs.resolve_path(&file_path) {
332 if let Ok(metadata) = node.metadata() {
333 let size = metadata.size;
334 let mut buf = vec![0u8; size];
336 if let Ok(bytes_read) = node.read(0, &mut buf) {
337 buf.truncate(bytes_read);
338 let hash = crate::crypto::hash::sha256(&buf);
339 outputs
340 .file_hashes
341 .push((entry.name.clone(), *hash.as_bytes()));
342 outputs.total_size += size as u64;
343 outputs.file_count += 1;
344 }
345 }
346 }
347 }
348 }
349
350 outputs
351}
352
353#[cfg(feature = "alloc")]
358pub fn verify_reproducible(a: &BuildManifest, b: &BuildManifest) -> ReproducibilityResult {
359 let map_a: BTreeMap<&str, &[u8; 32]> = a
360 .outputs
361 .file_hashes
362 .iter()
363 .map(|(name, hash)| (name.as_str(), hash))
364 .collect();
365
366 let map_b: BTreeMap<&str, &[u8; 32]> = b
367 .outputs
368 .file_hashes
369 .iter()
370 .map(|(name, hash)| (name.as_str(), hash))
371 .collect();
372
373 let mut matching_files = Vec::new();
374 let mut differing_files = Vec::new();
375 let mut missing_in_a = Vec::new();
376 let mut missing_in_b = Vec::new();
377
378 for (name, hash_a) in &map_a {
380 match map_b.get(name) {
381 Some(hash_b) => {
382 if hash_a == hash_b {
383 matching_files.push(String::from(*name));
384 } else {
385 differing_files.push((String::from(*name), **hash_a, **hash_b));
386 }
387 }
388 None => {
389 missing_in_b.push(String::from(*name));
390 }
391 }
392 }
393
394 for name in map_b.keys() {
396 if !map_a.contains_key(name) {
397 missing_in_a.push(String::from(*name));
398 }
399 }
400
401 ReproducibilityResult {
402 matching_files,
403 differing_files,
404 missing_in_a,
405 missing_in_b,
406 }
407}
408
409#[cfg(feature = "alloc")]
427pub fn serialize_manifest(manifest: &BuildManifest) -> Vec<u8> {
428 let mut out = String::new();
429
430 out.push_str("PORT=");
431 out.push_str(&manifest.port_name);
432 out.push('\n');
433
434 out.push_str("VERSION=");
435 out.push_str(&manifest.port_version);
436 out.push('\n');
437
438 out.push_str("TOOLCHAIN=");
439 out.push_str(&manifest.inputs.env_snapshot.toolchain_version);
440 out.push('\n');
441
442 out.push_str("TARGET=");
443 out.push_str(&manifest.inputs.env_snapshot.target_triple);
444 out.push('\n');
445
446 out.push_str("DURATION_MS=");
447 push_u64(&mut out, manifest.build_duration_ms);
448 out.push('\n');
449
450 out.push_str("INPUT_COUNT=");
452 push_usize(&mut out, manifest.inputs.source_hashes.len());
453 out.push('\n');
454
455 for (name, hash) in &manifest.inputs.source_hashes {
456 out.push_str("INPUT:");
457 out.push_str(name);
458 out.push('=');
459 out.push_str(&bytes_to_hex(hash));
460 out.push('\n');
461 }
462
463 out.push_str("OUTPUT_COUNT=");
465 push_usize(&mut out, manifest.outputs.file_hashes.len());
466 out.push('\n');
467
468 for (name, hash) in &manifest.outputs.file_hashes {
469 out.push_str("OUTPUT:");
470 out.push_str(name);
471 out.push('=');
472 out.push_str(&bytes_to_hex(hash));
473 out.push('\n');
474 }
475
476 out.push_str("TOTAL_SIZE=");
477 push_u64(&mut out, manifest.outputs.total_size);
478 out.push('\n');
479
480 out.into_bytes()
481}
482
483#[cfg(feature = "alloc")]
485fn bytes_to_hex(bytes: &[u8]) -> String {
486 let mut hex = String::with_capacity(bytes.len() * 2);
487 for &b in bytes {
488 let high = HEX_CHARS[(b >> 4) as usize];
489 let low = HEX_CHARS[(b & 0x0f) as usize];
490 hex.push(high as char);
491 hex.push(low as char);
492 }
493 hex
494}
495
496#[cfg(feature = "alloc")]
498const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
499
500#[cfg(feature = "alloc")]
502fn push_u64(s: &mut String, value: u64) {
503 use core::fmt::Write;
504 let _ = write!(s, "{}", value);
505}
506
507#[cfg(feature = "alloc")]
509fn push_usize(s: &mut String, value: usize) {
510 use core::fmt::Write;
511 let _ = write!(s, "{}", value);
512}
513
514#[cfg(test)]
515mod tests {
516 #[allow(unused_imports)]
517 use alloc::vec;
518
519 use super::*;
520
521 #[test]
524 fn test_build_snapshot_new() {
525 let snap = BuildSnapshot::new();
526 assert!(snap.toolchain_version.is_empty());
527 assert!(snap.env_vars.is_empty());
528 assert!(snap.timestamp_override.is_none());
529 assert!(snap.source_hashes.is_empty());
530 assert!(snap.target_triple.is_empty());
531 }
532
533 #[test]
536 fn test_build_inputs_new() {
537 let inp = BuildInputs::new();
538 assert!(inp.source_hashes.is_empty());
539 assert!(inp.dependency_versions.is_empty());
540 }
541
542 #[test]
543 fn test_build_outputs_new() {
544 let out = BuildOutputs::new();
545 assert!(out.file_hashes.is_empty());
546 assert_eq!(out.total_size, 0);
547 assert_eq!(out.file_count, 0);
548 }
549
550 #[test]
553 fn test_reproducibility_result_is_reproducible() {
554 let result = ReproducibilityResult {
555 matching_files: vec![String::from("a.o")],
556 differing_files: vec![],
557 missing_in_a: vec![],
558 missing_in_b: vec![],
559 };
560 assert!(result.is_reproducible());
561 }
562
563 #[test]
564 fn test_reproducibility_result_not_reproducible_differing() {
565 let result = ReproducibilityResult {
566 matching_files: vec![],
567 differing_files: vec![(String::from("a.o"), [1u8; 32], [2u8; 32])],
568 missing_in_a: vec![],
569 missing_in_b: vec![],
570 };
571 assert!(!result.is_reproducible());
572 }
573
574 #[test]
575 fn test_reproducibility_result_not_reproducible_missing() {
576 let result = ReproducibilityResult {
577 matching_files: vec![],
578 differing_files: vec![],
579 missing_in_a: vec![String::from("b.o")],
580 missing_in_b: vec![],
581 };
582 assert!(!result.is_reproducible());
583 }
584
585 #[test]
588 fn test_canonicalize_path_no_change() {
589 assert_eq!(canonicalize_path_value("/usr/bin"), "/usr/bin");
590 }
591
592 #[test]
593 fn test_canonicalize_path_trailing_slash() {
594 assert_eq!(canonicalize_path_value("/usr/bin/"), "/usr/bin");
595 }
596
597 #[test]
598 fn test_canonicalize_path_double_slash() {
599 assert_eq!(canonicalize_path_value("/usr//bin"), "/usr/bin");
600 }
601
602 #[test]
603 fn test_canonicalize_path_root_only() {
604 assert_eq!(canonicalize_path_value("/"), "/");
605 }
606
607 #[test]
608 fn test_canonicalize_path_non_path() {
609 assert_eq!(canonicalize_path_value("hello"), "hello");
610 }
611
612 #[test]
615 fn test_normalize_environment() {
616 use super::super::ports::{BuildEnvironment, Port};
617 let port = Port::new(String::from("test"), String::from("1.0.0"));
618 let mut env = BuildEnvironment::new(&port);
619 normalize_environment(&mut env);
620 assert_eq!(env.get_env("SOURCE_DATE_EPOCH"), Some("0"));
621 assert_eq!(env.get_env("LC_ALL"), Some("C"));
622 assert_eq!(env.get_env("LANG"), Some("C"));
623 assert_eq!(env.get_env("LANGUAGE"), Some("C"));
624 assert_eq!(env.get_env("TZ"), Some("UTC"));
625 }
626
627 #[test]
630 fn test_bytes_to_hex() {
631 assert_eq!(bytes_to_hex(&[0x00, 0xff, 0xab]), "00ffab");
632 assert_eq!(bytes_to_hex(&[]), "");
633 assert_eq!(bytes_to_hex(&[0x01, 0x23, 0x45, 0x67]), "01234567");
634 }
635
636 #[test]
639 fn test_verify_reproducible_identical() {
640 let a = BuildManifest {
641 port_name: String::from("test"),
642 port_version: String::from("1.0.0"),
643 inputs: BuildInputs::new(),
644 outputs: BuildOutputs {
645 file_hashes: vec![(String::from("a.o"), [1u8; 32])],
646 total_size: 100,
647 file_count: 1,
648 },
649 build_duration_ms: 0,
650 };
651 let b = a.clone();
652 let result = verify_reproducible(&a, &b);
653 assert!(result.is_reproducible());
654 assert_eq!(result.matching_files.len(), 1);
655 }
656
657 #[test]
658 fn test_verify_reproducible_differing() {
659 let a = BuildManifest {
660 port_name: String::from("test"),
661 port_version: String::from("1.0.0"),
662 inputs: BuildInputs::new(),
663 outputs: BuildOutputs {
664 file_hashes: vec![(String::from("a.o"), [1u8; 32])],
665 total_size: 100,
666 file_count: 1,
667 },
668 build_duration_ms: 0,
669 };
670 let b = BuildManifest {
671 port_name: String::from("test"),
672 port_version: String::from("1.0.0"),
673 inputs: BuildInputs::new(),
674 outputs: BuildOutputs {
675 file_hashes: vec![(String::from("a.o"), [2u8; 32])],
676 total_size: 100,
677 file_count: 1,
678 },
679 build_duration_ms: 0,
680 };
681 let result = verify_reproducible(&a, &b);
682 assert!(!result.is_reproducible());
683 assert_eq!(result.differing_files.len(), 1);
684 }
685
686 #[test]
687 fn test_verify_reproducible_missing_files() {
688 let a = BuildManifest {
689 port_name: String::from("test"),
690 port_version: String::from("1.0.0"),
691 inputs: BuildInputs::new(),
692 outputs: BuildOutputs {
693 file_hashes: vec![(String::from("a.o"), [1u8; 32])],
694 total_size: 100,
695 file_count: 1,
696 },
697 build_duration_ms: 0,
698 };
699 let b = BuildManifest {
700 port_name: String::from("test"),
701 port_version: String::from("1.0.0"),
702 inputs: BuildInputs::new(),
703 outputs: BuildOutputs {
704 file_hashes: vec![(String::from("b.o"), [1u8; 32])],
705 total_size: 100,
706 file_count: 1,
707 },
708 build_duration_ms: 0,
709 };
710 let result = verify_reproducible(&a, &b);
711 assert!(!result.is_reproducible());
712 assert_eq!(result.missing_in_b.len(), 1); assert_eq!(result.missing_in_a.len(), 1); }
715
716 #[test]
719 fn test_serialize_manifest() {
720 let manifest = BuildManifest {
721 port_name: String::from("curl"),
722 port_version: String::from("8.5.0"),
723 inputs: BuildInputs {
724 source_hashes: vec![(String::from("main.c"), [0xABu8; 32])],
725 env_snapshot: BuildSnapshot {
726 toolchain_version: String::from("rustc 1.93"),
727 env_vars: BTreeMap::new(),
728 timestamp_override: Some(0),
729 source_hashes: vec![],
730 target_triple: String::from("x86_64-veridian"),
731 },
732 dependency_versions: BTreeMap::new(),
733 },
734 outputs: BuildOutputs {
735 file_hashes: vec![(String::from("curl"), [0xCDu8; 32])],
736 total_size: 12345,
737 file_count: 1,
738 },
739 build_duration_ms: 5000,
740 };
741 let bytes = serialize_manifest(&manifest);
742 let text = core::str::from_utf8(&bytes).unwrap();
743 assert!(text.contains("PORT=curl"));
744 assert!(text.contains("VERSION=8.5.0"));
745 assert!(text.contains("TOOLCHAIN=rustc 1.93"));
746 assert!(text.contains("TARGET=x86_64-veridian"));
747 assert!(text.contains("DURATION_MS=5000"));
748 assert!(text.contains("INPUT_COUNT=1"));
749 assert!(text.contains("OUTPUT_COUNT=1"));
750 assert!(text.contains("TOTAL_SIZE=12345"));
751 assert!(text.contains("INPUT:main.c="));
752 assert!(text.contains("OUTPUT:curl="));
753 }
754
755 #[test]
758 fn test_push_u64() {
759 let mut s = String::new();
760 push_u64(&mut s, 42);
761 assert_eq!(s, "42");
762 }
763
764 #[test]
765 fn test_push_usize() {
766 let mut s = String::new();
767 push_usize(&mut s, 0);
768 assert_eq!(s, "0");
769 }
770}