⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/ports/
mod.rs

1//! Ports System Core
2//!
3//! Source-based package building framework for VeridianOS. Provides the
4//! `Port` definition, `BuildType` enumeration, `BuildEnvironment` setup,
5//! and the orchestration logic for building software from source via
6//! Portfile.toml definitions.
7//!
8//! Ports live under `/usr/ports/<category>/<port>/Portfile.toml` and are
9//! parsed with the minimal TOML parser in [`super::toml_parser`].
10
11pub mod collection;
12#[cfg(feature = "alloc")]
13pub mod llvm;
14#[cfg(feature = "alloc")]
15pub mod rustc_bootstrap;
16#[cfg(feature = "alloc")]
17pub mod rustdoc;
18
19#[cfg(feature = "alloc")]
20use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
21
22#[cfg(feature = "alloc")]
23use super::toml_parser;
24#[cfg(feature = "alloc")]
25use crate::error::KernelError;
26
27// ---------------------------------------------------------------------------
28// Port definition
29// ---------------------------------------------------------------------------
30
31/// Supported build system types.
32#[cfg(feature = "alloc")]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BuildType {
35    /// GNU Autotools (./configure && make)
36    Autotools,
37    /// CMake
38    CMake,
39    /// Meson + Ninja
40    Meson,
41    /// Rust / Cargo
42    Cargo,
43    /// Plain Makefile
44    Make,
45    /// Custom build steps only
46    Custom,
47}
48
49#[cfg(feature = "alloc")]
50impl BuildType {
51    /// Parse a build type from a string (case-insensitive match).
52    pub fn parse(s: &str) -> Option<Self> {
53        match s {
54            "autotools" | "Autotools" => Some(Self::Autotools),
55            "cmake" | "CMake" | "CMAKE" => Some(Self::CMake),
56            "meson" | "Meson" => Some(Self::Meson),
57            "cargo" | "Cargo" => Some(Self::Cargo),
58            "make" | "Make" => Some(Self::Make),
59            "custom" | "Custom" => Some(Self::Custom),
60            _ => None,
61        }
62    }
63
64    /// Return the conventional configure command for this build type.
65    pub fn configure_command(&self) -> &'static str {
66        match self {
67            Self::Autotools => "./configure --prefix=/usr",
68            Self::CMake => "cmake -B build -DCMAKE_INSTALL_PREFIX=/usr",
69            Self::Meson => "meson setup build --prefix=/usr",
70            Self::Cargo => "cargo build --release",
71            Self::Make => "",
72            Self::Custom => "",
73        }
74    }
75
76    /// Return the conventional build command for this build type.
77    pub fn build_command(&self) -> &'static str {
78        match self {
79            Self::Autotools | Self::Make => "make -j$(nproc)",
80            Self::CMake => "cmake --build build",
81            Self::Meson => "ninja -C build",
82            Self::Cargo => "", // cargo build already done in configure
83            Self::Custom => "",
84        }
85    }
86
87    /// Return the conventional install command for this build type.
88    pub fn install_command(&self) -> &'static str {
89        match self {
90            Self::Autotools | Self::Make => "make install DESTDIR=$PKG_DIR",
91            Self::CMake => "cmake --install build --prefix $PKG_DIR/usr",
92            Self::Meson => "DESTDIR=$PKG_DIR ninja -C build install",
93            Self::Cargo => "cargo install --root $PKG_DIR/usr --path .",
94            Self::Custom => "",
95        }
96    }
97}
98
99/// A single port definition loaded from a `Portfile.toml`.
100#[cfg(feature = "alloc")]
101#[derive(Debug, Clone)]
102pub struct Port {
103    /// Port name (e.g., "curl")
104    pub name: String,
105    /// Port version string (e.g., "8.5.0")
106    pub version: String,
107    /// Human-readable description
108    pub description: String,
109    /// Project homepage URL
110    pub homepage: String,
111    /// Source archive URLs
112    pub sources: Vec<String>,
113    /// SHA-256 checksums for each source (32 bytes each)
114    pub checksums: Vec<[u8; 32]>,
115    /// Build system type
116    pub build_type: BuildType,
117    /// Custom build steps (executed in order)
118    pub build_steps: Vec<String>,
119    /// Runtime / build dependency port names
120    pub dependencies: Vec<String>,
121    /// Category this port belongs to
122    pub category: String,
123    /// License identifier (e.g., "MIT", "GPL-3.0")
124    pub license: String,
125}
126
127#[cfg(feature = "alloc")]
128impl Port {
129    /// Create a minimal port with required fields only.
130    pub fn new(name: String, version: String) -> Self {
131        Self {
132            name,
133            version,
134            description: String::new(),
135            homepage: String::new(),
136            sources: Vec::new(),
137            checksums: Vec::new(),
138            build_type: BuildType::Make,
139            build_steps: Vec::new(),
140            dependencies: Vec::new(),
141            category: String::from("misc"),
142            license: String::new(),
143        }
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Build environment
149// ---------------------------------------------------------------------------
150
151/// Isolated build environment for compiling a port.
152#[cfg(feature = "alloc")]
153#[derive(Debug, Clone)]
154pub struct BuildEnvironment {
155    /// Path to the isolated build root (e.g., `/tmp/ports-build/<name>`)
156    pub build_root: String,
157    /// Source directory within the build root
158    pub source_dir: String,
159    /// Build output directory
160    pub build_dir: String,
161    /// Packaging / staging directory
162    pub pkg_dir: String,
163    /// Environment variables for the build
164    pub env_vars: BTreeMap<String, String>,
165    /// Build timeout in milliseconds (default: 300_000 = 5 minutes)
166    pub build_timeout_ms: u64,
167}
168
169#[cfg(feature = "alloc")]
170impl BuildEnvironment {
171    /// Create a new build environment for the given port.
172    pub fn new(port: &Port) -> Self {
173        let build_root = alloc::format!("/tmp/ports-build/{}-{}", port.name, port.version);
174        let source_dir = alloc::format!("{}/src", build_root);
175        let build_dir = alloc::format!("{}/build", build_root);
176        let pkg_dir = alloc::format!("{}/pkg", build_root);
177
178        let mut env_vars = BTreeMap::new();
179        env_vars.insert(String::from("PKG_DIR"), pkg_dir.clone());
180        env_vars.insert(String::from("SRC_DIR"), source_dir.clone());
181        env_vars.insert(String::from("BUILD_DIR"), build_dir.clone());
182        env_vars.insert(String::from("PORT_NAME"), port.name.clone());
183        env_vars.insert(String::from("PORT_VERSION"), port.version.clone());
184
185        Self {
186            build_root,
187            source_dir,
188            build_dir,
189            pkg_dir,
190            env_vars,
191            build_timeout_ms: 300_000, // 5 minutes default
192        }
193    }
194
195    /// Set up directories for the build. In a running system this would
196    /// create the directory tree via the VFS; here we validate the paths
197    /// and record readiness.
198    pub fn setup(&self) -> Result<(), KernelError> {
199        // Validate that the build root path is sane
200        if self.build_root.is_empty() {
201            return Err(KernelError::InvalidArgument {
202                name: "build_root",
203                value: "empty_path",
204            });
205        }
206
207        // In a full implementation, this would use the VFS to create:
208        //   build_root/
209        //     src/
210        //     build/
211        //     pkg/
212        // For now we log the intent. Actual directory creation happens in
213        // user-space when the port build is executed.
214        crate::println!("[PORTS] Build environment ready: {}", self.build_root);
215
216        Ok(())
217    }
218
219    /// Look up an environment variable by key.
220    pub fn get_env(&self, key: &str) -> Option<&str> {
221        self.env_vars.get(key).map(|v| v.as_str())
222    }
223
224    /// Set an environment variable for the build.
225    pub fn set_env(&mut self, key: String, value: String) {
226        self.env_vars.insert(key, value);
227    }
228}
229
230// ---------------------------------------------------------------------------
231// Port manager
232// ---------------------------------------------------------------------------
233
234/// Manages loaded ports and provides lookup / search capabilities.
235#[cfg(feature = "alloc")]
236pub struct PortManager {
237    /// All loaded ports, keyed by name.
238    ports: BTreeMap<String, Port>,
239}
240
241#[cfg(feature = "alloc")]
242impl PortManager {
243    /// Create an empty port manager.
244    pub fn new() -> Self {
245        Self {
246            ports: BTreeMap::new(),
247        }
248    }
249
250    /// Load a port from a Portfile.toml string (the file contents).
251    ///
252    /// `path` is informational and included in error messages.
253    #[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
254    pub fn load_port(&mut self, path: &str, content: &str) -> Result<(), KernelError> {
255        let port = parse_portfile(content).inspect_err(|e| {
256            crate::println!("[PORTS] Failed to parse {}: {:?}", path, e);
257        })?;
258
259        crate::println!(
260            "[PORTS] Loaded port {} {} from {}",
261            port.name,
262            port.version,
263            path
264        );
265        self.ports.insert(port.name.clone(), port);
266        Ok(())
267    }
268
269    /// Register an already-constructed `Port`.
270    pub fn register_port(&mut self, port: Port) {
271        self.ports.insert(port.name.clone(), port);
272    }
273
274    /// Look up a port by exact name.
275    pub fn get_port(&self, name: &str) -> Option<&Port> {
276        self.ports.get(name)
277    }
278
279    /// List all loaded ports.
280    pub fn list_ports(&self) -> Vec<&Port> {
281        self.ports.values().collect()
282    }
283
284    /// Search ports whose name or description contains `query`
285    /// (case-insensitive substring match).
286    pub fn search(&self, query: &str) -> Vec<&Port> {
287        let query_lower = query.to_lowercase();
288        self.ports
289            .values()
290            .filter(|p| {
291                p.name.to_lowercase().contains(&query_lower)
292                    || p.description.to_lowercase().contains(&query_lower)
293            })
294            .collect()
295    }
296
297    /// Resolve the transitive build-dependency list for `port` in
298    /// topological order (dependencies before dependents).
299    ///
300    /// Returns an error if a dependency is not loaded.
301    pub fn resolve_build_deps(&self, port: &Port) -> Result<Vec<String>, KernelError> {
302        let mut resolved: Vec<String> = Vec::new();
303        let mut visited = BTreeMap::<String, bool>::new();
304        self.resolve_deps_inner(&port.name, &mut resolved, &mut visited)?;
305        // The port itself will be the last entry; remove it so the caller
306        // only gets the dependencies.
307        if let Some(pos) = resolved.iter().position(|n| n == &port.name) {
308            resolved.remove(pos);
309        }
310        Ok(resolved)
311    }
312
313    /// Recursive depth-first dependency resolution with cycle detection.
314    fn resolve_deps_inner(
315        &self,
316        name: &str,
317        resolved: &mut Vec<String>,
318        visited: &mut BTreeMap<String, bool>,
319    ) -> Result<(), KernelError> {
320        if let Some(&in_progress) = visited.get(name) {
321            if in_progress {
322                // Cycle detected
323                return Err(KernelError::InvalidState {
324                    expected: "acyclic dependency graph",
325                    actual: "dependency cycle detected",
326                });
327            }
328            // Already fully resolved
329            return Ok(());
330        }
331
332        // Mark as in-progress
333        visited.insert(String::from(name), true);
334
335        let port = self.ports.get(name).ok_or(KernelError::NotFound {
336            resource: "port",
337            id: 0,
338        })?;
339
340        for dep_name in &port.dependencies {
341            self.resolve_deps_inner(dep_name, resolved, visited)?;
342        }
343
344        // Mark as resolved
345        visited.insert(String::from(name), false);
346        resolved.push(String::from(name));
347        Ok(())
348    }
349
350    /// Return the number of loaded ports.
351    pub fn port_count(&self) -> usize {
352        self.ports.len()
353    }
354}
355
356#[cfg(feature = "alloc")]
357impl Default for PortManager {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363// ---------------------------------------------------------------------------
364// Portfile.toml parsing
365// ---------------------------------------------------------------------------
366
367/// Parse a `Portfile.toml` string into a [`Port`].
368///
369/// Expected format:
370/// ```toml
371/// [port]
372/// name = "curl"
373/// version = "8.5.0"
374/// description = "Command-line URL transfer tool"
375/// homepage = "https://curl.se"
376/// license = "MIT"
377/// category = "net"
378/// build_type = "autotools"
379///
380/// [sources]
381/// urls = ["https://curl.se/download/curl-8.5.0.tar.gz"]
382/// checksums = ["aabbccdd..."]
383///
384/// [dependencies]
385/// build = ["openssl", "zlib"]
386///
387/// [build]
388/// steps = ["./configure --prefix=/usr", "make -j4"]
389/// ```
390#[cfg(feature = "alloc")]
391fn parse_portfile(content: &str) -> Result<Port, KernelError> {
392    let toml = toml_parser::parse_toml(content)?;
393
394    // [port] section
395    let port_table =
396        toml.get("port")
397            .and_then(|v| v.as_table())
398            .ok_or(KernelError::InvalidArgument {
399                name: "portfile",
400                value: "missing_port_section",
401            })?;
402
403    let name =
404        port_table
405            .get("name")
406            .and_then(|v| v.as_str())
407            .ok_or(KernelError::InvalidArgument {
408                name: "portfile",
409                value: "missing_port_name",
410            })?;
411
412    let version =
413        port_table
414            .get("version")
415            .and_then(|v| v.as_str())
416            .ok_or(KernelError::InvalidArgument {
417                name: "portfile",
418                value: "missing_port_version",
419            })?;
420
421    let description = port_table
422        .get("description")
423        .and_then(|v| v.as_str())
424        .unwrap_or("");
425
426    let homepage = port_table
427        .get("homepage")
428        .and_then(|v| v.as_str())
429        .unwrap_or("");
430
431    let license = port_table
432        .get("license")
433        .and_then(|v| v.as_str())
434        .unwrap_or("");
435
436    let category = port_table
437        .get("category")
438        .and_then(|v| v.as_str())
439        .unwrap_or("misc");
440
441    let build_type_str = port_table
442        .get("build_type")
443        .and_then(|v| v.as_str())
444        .unwrap_or("make");
445
446    let build_type = BuildType::parse(build_type_str).unwrap_or(BuildType::Make);
447
448    // [sources] section
449    let mut sources = Vec::new();
450    let mut checksums: Vec<[u8; 32]> = Vec::new();
451
452    if let Some(src_table) = toml.get("sources").and_then(|v| v.as_table()) {
453        if let Some(urls) = src_table.get("urls").and_then(|v| v.as_array()) {
454            for url_val in urls {
455                if let Some(url) = url_val.as_str() {
456                    sources.push(String::from(url));
457                }
458            }
459        }
460        if let Some(chk_arr) = src_table.get("checksums").and_then(|v| v.as_array()) {
461            for chk_val in chk_arr {
462                if let Some(hex) = chk_val.as_str() {
463                    checksums.push(parse_hex_checksum(hex));
464                }
465            }
466        }
467    }
468
469    // [dependencies] section
470    let mut dependencies = Vec::new();
471    if let Some(dep_table) = toml.get("dependencies").and_then(|v| v.as_table()) {
472        if let Some(build_deps) = dep_table.get("build").and_then(|v| v.as_array()) {
473            for dep_val in build_deps {
474                if let Some(dep) = dep_val.as_str() {
475                    dependencies.push(String::from(dep));
476                }
477            }
478        }
479        // Also accept "runtime" dependencies merged into the same list
480        if let Some(runtime_deps) = dep_table.get("runtime").and_then(|v| v.as_array()) {
481            for dep_val in runtime_deps {
482                if let Some(dep) = dep_val.as_str() {
483                    dependencies.push(String::from(dep));
484                }
485            }
486        }
487    }
488
489    // [build] section
490    let mut build_steps = Vec::new();
491    if let Some(build_table) = toml.get("build").and_then(|v| v.as_table()) {
492        if let Some(steps) = build_table.get("steps").and_then(|v| v.as_array()) {
493            for step_val in steps {
494                if let Some(step) = step_val.as_str() {
495                    build_steps.push(String::from(step));
496                }
497            }
498        }
499    }
500
501    Ok(Port {
502        name: String::from(name),
503        version: String::from(version),
504        description: String::from(description),
505        homepage: String::from(homepage),
506        sources,
507        checksums,
508        build_type,
509        build_steps,
510        dependencies,
511        category: String::from(category),
512        license: String::from(license),
513    })
514}
515
516/// Parse a hex-encoded SHA-256 checksum string into a 32-byte array.
517/// Returns all zeros if the string is invalid or too short.
518#[cfg(feature = "alloc")]
519fn parse_hex_checksum(hex: &str) -> [u8; 32] {
520    let mut result = [0u8; 32];
521    let hex = hex.trim();
522    let bytes = hex.as_bytes();
523
524    let mut i = 0;
525    let mut out = 0;
526    while i + 1 < bytes.len() && out < 32 {
527        let high = hex_nibble(bytes[i]);
528        let low = hex_nibble(bytes[i + 1]);
529        result[out] = (high << 4) | low;
530        i += 2;
531        out += 1;
532    }
533
534    result
535}
536
537/// Convert a single hex ASCII character to its 4-bit value.
538#[cfg(feature = "alloc")]
539fn hex_nibble(b: u8) -> u8 {
540    match b {
541        b'0'..=b'9' => b - b'0',
542        b'a'..=b'f' => b - b'a' + 10,
543        b'A'..=b'F' => b - b'A' + 10,
544        _ => 0,
545    }
546}
547
548// ---------------------------------------------------------------------------
549// Build orchestration
550// ---------------------------------------------------------------------------
551
552/// Build a port inside the given environment.
553///
554/// This is the kernel-side orchestration framework. Actual compilation
555/// takes place in user-space processes; the kernel validates checksums,
556/// sets up the build environment, and sequences the build steps.
557#[cfg(feature = "alloc")]
558pub fn build_port(port: &Port, env: &mut BuildEnvironment) -> Result<(), KernelError> {
559    let _label = build_type_label(port.build_type);
560    crate::println!(
561        "[PORTS] Building {} {} ({})",
562        port.name,
563        port.version,
564        _label
565    );
566
567    // Step 0: Normalize environment for reproducibility
568    crate::pkg::reproducible::normalize_environment(env);
569
570    // Step 1: Verify source checksums
571    verify_checksums(port)?;
572
573    // Step 2: Configure (based on BuildType)
574    configure_port(port, env)?;
575
576    // Step 3: Execute build steps
577    execute_build(port, env)?;
578
579    // Step 4: Package the result
580    package_result(port, env)?;
581
582    // Step 5: Record build manifest for reproducibility verification
583    let pkg_dir = env.pkg_dir.clone();
584    match crate::pkg::reproducible::create_build_manifest(port, env, &pkg_dir) {
585        Ok(_manifest) => {
586            crate::println!(
587                "[PORTS] Build manifest recorded ({} inputs, {} outputs)",
588                _manifest.inputs.source_hashes.len(),
589                _manifest.outputs.file_count
590            );
591        }
592        Err(_e) => {
593            crate::println!("[PORTS] Warning: could not create build manifest: {:?}", _e);
594        }
595    }
596
597    crate::println!("[PORTS] Successfully built {} {}", port.name, port.version);
598    Ok(())
599}
600
601/// Verify that source checksums match expectations.
602///
603/// Reads each source archive from VFS at the expected download path and
604/// computes SHA-256 to compare against `port.checksums[i]`. If the VFS is
605/// not available or a file has not been downloaded yet, a warning is logged
606/// and verification is skipped for that source (non-fatal).
607#[cfg(feature = "alloc")]
608fn verify_checksums(port: &Port) -> Result<(), KernelError> {
609    if port.sources.is_empty() {
610        return Err(KernelError::InvalidArgument {
611            name: "port_sources",
612            value: "no_sources_defined",
613        });
614    }
615
616    // If checksums are provided, their count must match sources
617    if !port.checksums.is_empty() && port.checksums.len() != port.sources.len() {
618        return Err(KernelError::InvalidArgument {
619            name: "port_checksums",
620            value: "checksum_count_mismatch",
621        });
622    }
623
624    for (i, source) in port.sources.iter().enumerate() {
625        if source.is_empty() {
626            return Err(KernelError::InvalidArgument {
627                name: "port_source_url",
628                value: "empty_url",
629            });
630        }
631
632        if i >= port.checksums.len() {
633            // No checksum provided for this source -- skip
634            continue;
635        }
636
637        let zero_checksum = [0u8; 32];
638        if port.checksums[i] == zero_checksum {
639            crate::println!(
640                "[PORTS] WARNING: zero checksum for source {}, skipping verify",
641                i
642            );
643            continue;
644        }
645
646        // Extract filename from URL (last path component)
647        let _filename = source.rsplit('/').next().unwrap_or("source.tar.gz");
648        let _archive_path = alloc::format!(
649            "/tmp/ports-build/{}-{}/src/{}",
650            port.name,
651            port.version,
652            _filename
653        );
654
655        // Try to read the source archive from VFS for real SHA-256 verification
656        let _verified = verify_source_from_vfs(&_archive_path, &port.checksums[i], i)?;
657    }
658
659    Ok(())
660}
661
662/// Read a source archive from VFS and verify its SHA-256 checksum.
663///
664/// Returns `true` if verification succeeded, `false` if the file was not
665/// available (VFS missing or file not found -- non-fatal). Returns an error
666/// only if the file exists but the checksum does not match.
667#[cfg(feature = "alloc")]
668#[cfg_attr(
669    not(target_arch = "x86_64"),
670    allow(unused_variables, clippy::unnecessary_wraps)
671)]
672fn verify_source_from_vfs(
673    archive_path: &str,
674    expected: &[u8; 32],
675    source_index: usize,
676) -> Result<bool, KernelError> {
677    let vfs_lock = match crate::fs::try_get_vfs() {
678        Some(lock) => lock,
679        None => {
680            crate::println!(
681                "[PORTS] WARNING: VFS not available, skipping checksum verify for source {}",
682                source_index
683            );
684            return Ok(false);
685        }
686    };
687
688    let vfs = vfs_lock.read();
689    let node = match vfs.resolve_path(archive_path) {
690        Ok(n) => n,
691        Err(_) => {
692            crate::println!(
693                "[PORTS] WARNING: source file not found at {}, skipping verify",
694                archive_path
695            );
696            return Ok(false);
697        }
698    };
699
700    // Read the file size from metadata, then read the file contents
701    let metadata = node.metadata().map_err(|_| KernelError::InvalidState {
702        expected: "readable source file",
703        actual: "metadata unavailable",
704    })?;
705
706    let file_size = metadata.size;
707    if file_size == 0 {
708        crate::println!(
709            "[PORTS] WARNING: empty source file at {}, skipping verify",
710            archive_path
711        );
712        return Ok(false);
713    }
714
715    // Read file contents into buffer
716    let mut buf = vec![0u8; file_size];
717    let bytes_read = node
718        .read(0, &mut buf)
719        .map_err(|_| KernelError::InvalidState {
720            expected: "readable source file",
721            actual: "read failed",
722        })?;
723
724    // Compute SHA-256 and compare
725    let hash = crate::crypto::hash::sha256(&buf[..bytes_read]);
726    if hash.as_bytes() != expected {
727        crate::println!(
728            "[PORTS] ERROR: checksum mismatch for source {} at {}",
729            source_index,
730            archive_path
731        );
732        return Err(KernelError::PermissionDenied {
733            operation: "verify source checksum",
734        });
735    }
736
737    crate::println!(
738        "[PORTS] Checksum verified for source {} (SHA-256 match)",
739        source_index
740    );
741    Ok(true)
742}
743
744/// Execute a build command in the port's build environment.
745///
746/// In a running system, this spawns a user-space process via
747/// `crate::process::creation::create_process()`. The kernel provides the
748/// framework; actual compilation requires a functional user-space.
749#[cfg(feature = "alloc")]
750#[cfg_attr(
751    not(target_arch = "x86_64"),
752    allow(unused_variables, clippy::for_kv_map)
753)]
754fn execute_command(
755    cmd: &str,
756    env: &BuildEnvironment,
757    working_dir: &str,
758) -> Result<i32, KernelError> {
759    // TODO(user-space): Wire to real process execution
760    // When user-space is functional:
761    // 1. create_process(cmd, entry_point)
762    // 2. Set environment variables from env.env_vars
763    // 3. Set working directory
764    // 4. Wait for exit status
765    // 5. Return exit code
766
767    crate::println!("[PORTS] exec: {} (in {})", cmd, working_dir);
768
769    // Log environment variables being passed
770    for (_key, _value) in &env.env_vars {
771        crate::println!("[PORTS]   {}={}", _key, _value);
772    }
773
774    // Simulate successful execution for kernel-space testing
775    Ok(0)
776}
777
778/// Generate the configure command for the port's build type and execute it.
779#[cfg(feature = "alloc")]
780fn configure_port(port: &Port, env: &BuildEnvironment) -> Result<(), KernelError> {
781    let configure_cmd = port.build_type.configure_command();
782    if configure_cmd.is_empty() {
783        crate::println!("[PORTS] No configure step for build type");
784        return Ok(());
785    }
786
787    crate::println!(
788        "[PORTS] Configure: {} (in {})",
789        configure_cmd,
790        env.source_dir
791    );
792
793    let exit_code = execute_command(configure_cmd, env, &env.source_dir)?;
794    if exit_code != 0 {
795        return Err(KernelError::InvalidState {
796            expected: "configure exit code 0",
797            actual: "configure command failed",
798        });
799    }
800
801    Ok(())
802}
803
804/// Execute the build steps (either from Portfile or from BuildType defaults).
805///
806/// Each step is executed via [`execute_command`]. Build output is directed
807/// to `/var/log/ports/{name}-{version}-build.log`. The build is aborted on
808/// the first non-zero exit code.
809#[cfg(feature = "alloc")]
810#[cfg_attr(
811    not(target_arch = "x86_64"),
812    allow(unused_variables, clippy::for_kv_map)
813)]
814fn execute_build(port: &Port, env: &BuildEnvironment) -> Result<(), KernelError> {
815    let steps: Vec<&str> = if port.build_steps.is_empty() {
816        // Use default build command for the build type
817        let cmd = port.build_type.build_command();
818        if cmd.is_empty() {
819            vec![]
820        } else {
821            vec![cmd]
822        }
823    } else {
824        port.build_steps.iter().map(|s| s.as_str()).collect()
825    };
826
827    if steps.is_empty() {
828        crate::println!("[PORTS] No build steps to execute");
829        return Ok(());
830    }
831
832    let _log_path = alloc::format!("/var/log/ports/{}-{}-build.log", port.name, port.version);
833    crate::println!("[PORTS] Build output will be logged to {}", _log_path);
834    crate::println!("[PORTS] Build timeout: {} ms", env.build_timeout_ms);
835
836    for (i, step) in steps.iter().enumerate() {
837        crate::println!(
838            "[PORTS] Step {}/{}: {} (in {})",
839            i + 1,
840            steps.len(),
841            step,
842            env.build_dir
843        );
844
845        let exit_code = execute_command(step, env, &env.build_dir)?;
846        if exit_code != 0 {
847            crate::println!(
848                "[PORTS] ERROR: build step {}/{} failed with exit code {}",
849                i + 1,
850                steps.len(),
851                exit_code
852            );
853            return Err(KernelError::InvalidState {
854                expected: "build step exit code 0",
855                actual: "build step failed",
856            });
857        }
858    }
859
860    Ok(())
861}
862
863/// Package the built output into a .vpkg archive.
864///
865/// Walks `env.pkg_dir` via VFS (when available) to collect installed files,
866/// generates [`PackageMetadata`](super::PackageMetadata) and file manifest
867/// entries, and logs the vpkg destination path.
868#[cfg(feature = "alloc")]
869#[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
870fn package_result(port: &Port, env: &BuildEnvironment) -> Result<(), KernelError> {
871    crate::println!("[PORTS] Packaging {} from {}", port.name, env.pkg_dir);
872
873    // Run the install command to populate pkg_dir
874    let install_cmd = port.build_type.install_command();
875    if !install_cmd.is_empty() {
876        crate::println!("[PORTS] Install command: {}", install_cmd);
877        let exit_code = execute_command(install_cmd, env, &env.build_dir)?;
878        if exit_code != 0 {
879            return Err(KernelError::InvalidState {
880                expected: "install exit code 0",
881                actual: "install command failed",
882            });
883        }
884    }
885
886    // Collect installed files from pkg_dir via VFS
887    let _file_records = collect_installed_files(&env.pkg_dir);
888
889    // Generate package metadata
890    let _metadata = super::PackageMetadata {
891        name: port.name.clone(),
892        version: parse_port_version(&port.version),
893        author: String::new(),
894        description: port.description.clone(),
895        license: port.license.clone(),
896        dependencies: port
897            .dependencies
898            .iter()
899            .map(|dep| super::Dependency {
900                name: dep.clone(),
901                version_req: String::from(">=0.0.0"),
902            })
903            .collect(),
904        conflicts: Vec::new(),
905    };
906
907    let _vpkg_path = alloc::format!("/var/cache/packages/{}-{}.vpkg", port.name, port.version);
908    crate::println!(
909        "[PORTS] Package metadata: {} v{} ({} files tracked)",
910        port.name,
911        port.version,
912        _file_records.len()
913    );
914    crate::println!("[PORTS] vpkg destination: {}", _vpkg_path);
915
916    // TODO(user-space): create_package() when VFS file write is complete
917    // This would serialize _metadata + file contents into the .vpkg archive
918    // at _vpkg_path and register it in the local package database.
919
920    Ok(())
921}
922
923/// Collect installed files from the package staging directory via VFS.
924///
925/// Returns file manifest records for each file found. If the VFS is not
926/// available or the directory does not exist, returns an empty list with
927/// a warning log.
928#[cfg(feature = "alloc")]
929#[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
930fn collect_installed_files(pkg_dir: &str) -> Vec<super::manifest::FileRecord> {
931    use super::manifest::{FileRecord, FileType};
932
933    let mut records = Vec::new();
934
935    let vfs_lock = match crate::fs::try_get_vfs() {
936        Some(lock) => lock,
937        None => {
938            crate::println!(
939                "[PORTS] WARNING: VFS not available, cannot scan {} for installed files",
940                pkg_dir
941            );
942            return records;
943        }
944    };
945
946    let vfs = vfs_lock.read();
947    let node = match vfs.resolve_path(pkg_dir) {
948        Ok(n) => n,
949        Err(_) => {
950            crate::println!(
951                "[PORTS] WARNING: pkg_dir {} not found, no files to package",
952                pkg_dir
953            );
954            return records;
955        }
956    };
957
958    // Read directory entries from the staging area
959    match node.readdir() {
960        Ok(entries) => {
961            for entry in &entries {
962                let file_path = alloc::format!("{}/{}", pkg_dir, entry.name);
963                let file_type = FileType::from_path(&file_path);
964
965                // Try to get file size from metadata
966                let size = if let Ok(child) = vfs.resolve_path(&file_path) {
967                    child.metadata().map(|m| m.size as u64).unwrap_or(0)
968                } else {
969                    0
970                };
971
972                // Compute FNV-1a checksum if we can read file contents
973                let checksum = if let Ok(child) = vfs.resolve_path(&file_path) {
974                    let mut buf = vec![0u8; size as usize];
975                    if let Ok(n) = child.read(0, &mut buf) {
976                        super::manifest::fnv1a_hash(&buf[..n])
977                    } else {
978                        0
979                    }
980                } else {
981                    0
982                };
983
984                records.push(FileRecord {
985                    path: file_path,
986                    size,
987                    checksum,
988                    file_type,
989                });
990            }
991            crate::println!(
992                "[PORTS] Collected {} installed files from {}",
993                records.len(),
994                pkg_dir
995            );
996        }
997        Err(_) => {
998            crate::println!(
999                "[PORTS] WARNING: cannot list directory {}, skipping file collection",
1000                pkg_dir
1001            );
1002        }
1003    }
1004
1005    records
1006}
1007
1008/// Parse a port version string (e.g., "8.5.0") into a
1009/// [`Version`](super::Version).
1010#[cfg(feature = "alloc")]
1011fn parse_port_version(version_str: &str) -> super::Version {
1012    let parts: Vec<&str> = version_str.split('.').collect();
1013
1014    let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
1015    let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
1016    let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
1017
1018    super::Version {
1019        major,
1020        minor,
1021        patch,
1022    }
1023}
1024
1025/// Fetch source archives from URLs using the repository HTTP client.
1026///
1027/// Downloads each source to `/tmp/ports-build/{name}-{version}/src/{filename}`.
1028/// Requires a functional network stack for actual HTTP downloads.
1029#[cfg(feature = "alloc")]
1030#[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
1031pub fn fetch_source(port: &Port, env: &BuildEnvironment) -> Result<(), KernelError> {
1032    if port.sources.is_empty() {
1033        return Ok(());
1034    }
1035
1036    for (i, url) in port.sources.iter().enumerate() {
1037        // Extract filename from the URL (last path component)
1038        let _filename = url.rsplit('/').next().unwrap_or("source.tar.gz");
1039        let _dest_path = alloc::format!("{}/{}", env.source_dir, _filename);
1040
1041        crate::println!(
1042            "[PORTS] Fetching source {}/{}: {} -> {}",
1043            i + 1,
1044            port.sources.len(),
1045            url,
1046            _dest_path
1047        );
1048
1049        // TODO(user-space): actual HTTP download requires network stack
1050        // When the network stack is functional:
1051        // 1. Create HttpClient with repository base URL
1052        // 2. GET the source URL
1053        // 3. Write response body to _dest_path via VFS
1054        // 4. Verify checksum after download
1055    }
1056
1057    Ok(())
1058}
1059
1060/// Human-readable label for a build type.
1061#[cfg(feature = "alloc")]
1062fn build_type_label(bt: BuildType) -> &'static str {
1063    match bt {
1064        BuildType::Autotools => "autotools",
1065        BuildType::CMake => "cmake",
1066        BuildType::Meson => "meson",
1067        BuildType::Cargo => "cargo",
1068        BuildType::Make => "make",
1069        BuildType::Custom => "custom",
1070    }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    #[allow(unused_imports)]
1076    use alloc::vec;
1077
1078    use super::*;
1079
1080    // ---- BuildType ----
1081
1082    #[test]
1083    fn test_build_type_parse() {
1084        assert_eq!(BuildType::parse("autotools"), Some(BuildType::Autotools));
1085        assert_eq!(BuildType::parse("Autotools"), Some(BuildType::Autotools));
1086        assert_eq!(BuildType::parse("cmake"), Some(BuildType::CMake));
1087        assert_eq!(BuildType::parse("CMake"), Some(BuildType::CMake));
1088        assert_eq!(BuildType::parse("CMAKE"), Some(BuildType::CMake));
1089        assert_eq!(BuildType::parse("meson"), Some(BuildType::Meson));
1090        assert_eq!(BuildType::parse("Meson"), Some(BuildType::Meson));
1091        assert_eq!(BuildType::parse("cargo"), Some(BuildType::Cargo));
1092        assert_eq!(BuildType::parse("Cargo"), Some(BuildType::Cargo));
1093        assert_eq!(BuildType::parse("make"), Some(BuildType::Make));
1094        assert_eq!(BuildType::parse("custom"), Some(BuildType::Custom));
1095        assert_eq!(BuildType::parse("unknown"), None);
1096    }
1097
1098    #[test]
1099    fn test_build_type_configure_command() {
1100        assert!(BuildType::Autotools
1101            .configure_command()
1102            .contains("./configure"));
1103        assert!(BuildType::CMake.configure_command().contains("cmake"));
1104        assert!(BuildType::Meson.configure_command().contains("meson"));
1105        assert!(BuildType::Cargo.configure_command().contains("cargo build"));
1106        assert!(BuildType::Make.configure_command().is_empty());
1107        assert!(BuildType::Custom.configure_command().is_empty());
1108    }
1109
1110    #[test]
1111    fn test_build_type_build_command() {
1112        assert!(BuildType::Autotools.build_command().contains("make"));
1113        assert!(BuildType::CMake.build_command().contains("cmake --build"));
1114        assert!(BuildType::Meson.build_command().contains("ninja"));
1115        assert!(BuildType::Cargo.build_command().is_empty());
1116        assert!(BuildType::Make.build_command().contains("make"));
1117        assert!(BuildType::Custom.build_command().is_empty());
1118    }
1119
1120    #[test]
1121    fn test_build_type_install_command() {
1122        assert!(BuildType::Autotools
1123            .install_command()
1124            .contains("make install"));
1125        assert!(BuildType::CMake
1126            .install_command()
1127            .contains("cmake --install"));
1128        assert!(BuildType::Meson.install_command().contains("ninja"));
1129        assert!(BuildType::Cargo.install_command().contains("cargo install"));
1130        assert!(BuildType::Custom.install_command().is_empty());
1131    }
1132
1133    // ---- Port ----
1134
1135    #[test]
1136    fn test_port_new() {
1137        let port = Port::new(String::from("curl"), String::from("8.5.0"));
1138        assert_eq!(port.name, "curl");
1139        assert_eq!(port.version, "8.5.0");
1140        assert!(port.description.is_empty());
1141        assert!(port.homepage.is_empty());
1142        assert!(port.sources.is_empty());
1143        assert!(port.checksums.is_empty());
1144        assert_eq!(port.build_type, BuildType::Make);
1145        assert!(port.build_steps.is_empty());
1146        assert!(port.dependencies.is_empty());
1147        assert_eq!(port.category, "misc");
1148        assert!(port.license.is_empty());
1149    }
1150
1151    // ---- BuildEnvironment ----
1152
1153    #[test]
1154    fn test_build_environment_new() {
1155        let port = Port::new(String::from("curl"), String::from("8.5.0"));
1156        let env = BuildEnvironment::new(&port);
1157        assert!(env.build_root.contains("curl-8.5.0"));
1158        assert!(env.source_dir.contains("/src"));
1159        assert!(env.build_dir.contains("/build"));
1160        assert!(env.pkg_dir.contains("/pkg"));
1161        assert_eq!(env.build_timeout_ms, 300_000);
1162        assert_eq!(env.get_env("PORT_NAME"), Some("curl"));
1163        assert_eq!(env.get_env("PORT_VERSION"), Some("8.5.0"));
1164    }
1165
1166    #[test]
1167    fn test_build_environment_get_set_env() {
1168        let port = Port::new(String::from("test"), String::from("1.0"));
1169        let mut env = BuildEnvironment::new(&port);
1170        assert!(env.get_env("MY_VAR").is_none());
1171        env.set_env(String::from("MY_VAR"), String::from("my_val"));
1172        assert_eq!(env.get_env("MY_VAR"), Some("my_val"));
1173    }
1174
1175    // ---- PortManager ----
1176
1177    #[test]
1178    fn test_port_manager_new() {
1179        let pm = PortManager::new();
1180        assert_eq!(pm.port_count(), 0);
1181        assert!(pm.list_ports().is_empty());
1182    }
1183
1184    #[test]
1185    fn test_port_manager_register_and_get() {
1186        let mut pm = PortManager::new();
1187        let port = Port::new(String::from("curl"), String::from("8.5.0"));
1188        pm.register_port(port);
1189        assert_eq!(pm.port_count(), 1);
1190        let p = pm.get_port("curl").unwrap();
1191        assert_eq!(p.version, "8.5.0");
1192    }
1193
1194    #[test]
1195    fn test_port_manager_get_nonexistent() {
1196        let pm = PortManager::new();
1197        assert!(pm.get_port("nonexistent").is_none());
1198    }
1199
1200    #[test]
1201    fn test_port_manager_search() {
1202        let mut pm = PortManager::new();
1203        let mut p1 = Port::new(String::from("curl"), String::from("8.5.0"));
1204        p1.description = String::from("URL transfer tool");
1205        pm.register_port(p1);
1206
1207        let mut p2 = Port::new(String::from("wget"), String::from("1.0.0"));
1208        p2.description = String::from("Network downloader");
1209        pm.register_port(p2);
1210
1211        let results = pm.search("curl");
1212        assert_eq!(results.len(), 1);
1213        assert_eq!(results[0].name, "curl");
1214
1215        // Search by description
1216        let results = pm.search("downloader");
1217        assert_eq!(results.len(), 1);
1218        assert_eq!(results[0].name, "wget");
1219
1220        // Case insensitive
1221        let results = pm.search("CURL");
1222        assert_eq!(results.len(), 1);
1223    }
1224
1225    #[test]
1226    fn test_port_manager_resolve_build_deps_simple() {
1227        let mut pm = PortManager::new();
1228        let mut app = Port::new(String::from("app"), String::from("1.0.0"));
1229        app.dependencies = vec![String::from("lib")];
1230        pm.register_port(app.clone());
1231        pm.register_port(Port::new(String::from("lib"), String::from("1.0.0")));
1232
1233        let deps = pm.resolve_build_deps(&app).unwrap();
1234        assert_eq!(deps.len(), 1);
1235        assert_eq!(deps[0], "lib");
1236    }
1237
1238    #[test]
1239    fn test_port_manager_resolve_build_deps_cycle() {
1240        let mut pm = PortManager::new();
1241        let mut a = Port::new(String::from("a"), String::from("1.0.0"));
1242        a.dependencies = vec![String::from("b")];
1243        let mut b = Port::new(String::from("b"), String::from("1.0.0"));
1244        b.dependencies = vec![String::from("a")];
1245        pm.register_port(a.clone());
1246        pm.register_port(b);
1247
1248        let result = pm.resolve_build_deps(&a);
1249        assert!(result.is_err());
1250    }
1251
1252    #[test]
1253    fn test_port_manager_resolve_build_deps_missing() {
1254        let mut pm = PortManager::new();
1255        let mut app = Port::new(String::from("app"), String::from("1.0.0"));
1256        app.dependencies = vec![String::from("missing-dep")];
1257        pm.register_port(app.clone());
1258
1259        let result = pm.resolve_build_deps(&app);
1260        assert!(result.is_err());
1261    }
1262
1263    // ---- parse_portfile ----
1264
1265    #[test]
1266    fn test_parse_portfile_minimal() {
1267        let content = r#"
1268[port]
1269name = "curl"
1270version = "8.5.0"
1271"#;
1272        let port = parse_portfile(content).unwrap();
1273        assert_eq!(port.name, "curl");
1274        assert_eq!(port.version, "8.5.0");
1275        assert_eq!(port.build_type, BuildType::Make); // default
1276        assert_eq!(port.category, "misc"); // default
1277    }
1278
1279    #[test]
1280    fn test_parse_portfile_full() {
1281        let content = r#"
1282[port]
1283name = "curl"
1284version = "8.5.0"
1285description = "URL transfer tool"
1286homepage = "https://curl.se"
1287license = "MIT"
1288category = "net"
1289build_type = "autotools"
1290
1291[dependencies]
1292build = ["openssl", "zlib"]
1293"#;
1294        let port = parse_portfile(content).unwrap();
1295        assert_eq!(port.name, "curl");
1296        assert_eq!(port.version, "8.5.0");
1297        assert_eq!(port.description, "URL transfer tool");
1298        assert_eq!(port.homepage, "https://curl.se");
1299        assert_eq!(port.license, "MIT");
1300        assert_eq!(port.category, "net");
1301        assert_eq!(port.build_type, BuildType::Autotools);
1302        assert_eq!(port.dependencies.len(), 2);
1303        assert_eq!(port.dependencies[0], "openssl");
1304        assert_eq!(port.dependencies[1], "zlib");
1305    }
1306
1307    #[test]
1308    fn test_parse_portfile_missing_port_section() {
1309        let content = r#"
1310[other]
1311name = "test"
1312"#;
1313        assert!(parse_portfile(content).is_err());
1314    }
1315
1316    #[test]
1317    fn test_parse_portfile_missing_name() {
1318        let content = r#"
1319[port]
1320version = "1.0.0"
1321"#;
1322        assert!(parse_portfile(content).is_err());
1323    }
1324
1325    #[test]
1326    fn test_parse_portfile_missing_version() {
1327        let content = r#"
1328[port]
1329name = "test"
1330"#;
1331        assert!(parse_portfile(content).is_err());
1332    }
1333
1334    // ---- hex_nibble ----
1335
1336    #[test]
1337    fn test_hex_nibble() {
1338        assert_eq!(hex_nibble(b'0'), 0);
1339        assert_eq!(hex_nibble(b'9'), 9);
1340        assert_eq!(hex_nibble(b'a'), 10);
1341        assert_eq!(hex_nibble(b'f'), 15);
1342        assert_eq!(hex_nibble(b'A'), 10);
1343        assert_eq!(hex_nibble(b'F'), 15);
1344        assert_eq!(hex_nibble(b'z'), 0); // invalid
1345    }
1346
1347    // ---- parse_hex_checksum ----
1348
1349    #[test]
1350    fn test_parse_hex_checksum() {
1351        let hex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
1352        let result = parse_hex_checksum(hex);
1353        assert_eq!(result[0], 0x01);
1354        assert_eq!(result[15], 0x10);
1355        assert_eq!(result[31], 0x20);
1356    }
1357
1358    #[test]
1359    fn test_parse_hex_checksum_short() {
1360        let hex = "0102";
1361        let result = parse_hex_checksum(hex);
1362        assert_eq!(result[0], 0x01);
1363        assert_eq!(result[1], 0x02);
1364        assert_eq!(result[2], 0x00); // rest zeros
1365    }
1366
1367    // ---- build_type_label ----
1368
1369    #[test]
1370    fn test_build_type_label() {
1371        assert_eq!(build_type_label(BuildType::Autotools), "autotools");
1372        assert_eq!(build_type_label(BuildType::CMake), "cmake");
1373        assert_eq!(build_type_label(BuildType::Meson), "meson");
1374        assert_eq!(build_type_label(BuildType::Cargo), "cargo");
1375        assert_eq!(build_type_label(BuildType::Make), "make");
1376        assert_eq!(build_type_label(BuildType::Custom), "custom");
1377    }
1378
1379    // ---- parse_port_version ----
1380
1381    #[test]
1382    fn test_parse_port_version() {
1383        let v = parse_port_version("8.5.0");
1384        assert_eq!(v.major, 8);
1385        assert_eq!(v.minor, 5);
1386        assert_eq!(v.patch, 0);
1387    }
1388
1389    #[test]
1390    fn test_parse_port_version_partial() {
1391        let v = parse_port_version("3.1");
1392        assert_eq!(v.major, 3);
1393        assert_eq!(v.minor, 1);
1394        assert_eq!(v.patch, 0);
1395    }
1396}