smidr 0.2.0

Smidr is a cargo- inspired build tool for C projects, meant to bridge dependencies across different build systems into a single build.
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Max-Mend
// This file is part of smidr: https://github.com/Max-Mend/smidr

//! The in-memory model of a `smidr` project.
//!
//! [`Project`] is a read-only view of what already exists (or should
//! exist) on disk - [`Project::load`] reads `smidr.toml` and computes
//! standard paths, [`Project::source_files`] reads `src/`. Neither
//! creates or modifies any files. [`Project::init`] is the one exception:
//! it's the write side, used by `smidr new` to scaffold a brand new
//! project from scratch. All other file writes (compiled objects, the
//! linked binary, dependency installs) live in [`crate::builder`] and
//! [`crate::toolchain`], not here.

use crate::config::{BuildSection, CStandard, ManifestConfig, ProjectSection, ProjectType};
use crate::error::{BuildError, Result};
use crate::toolchain::BuildOutput;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

/// Templates for the `smidr new` command.
const MAIN_C_TEMPLATE: &str = include_str!("../templates/main.c");
const GITIGNORE_TEMPLATE: &str = include_str!("../templates/.gitignore");
const LIB_C_TEMPLATE: &str = include_str!("../templates/lib.c");
const LIB_H_TEMPLATE: &str = include_str!("../templates/lib.h");

/// A loaded (or freshly initialized) `smidr` project.
///
/// This is what [`crate::builder::build_project`] and
/// [`crate::builder::run_project`] take as input, and what accumulates
/// dependency build results in `resolved_deps` as they're resolved.
pub struct Project {
    /// The project's root directory (where `smidr.toml` lives).
    pub root: PathBuf,
    /// The fully parsed `smidr.toml` - `[project]`, `[build]`, and
    /// `[dependencies]`.
    pub config: ManifestConfig,

    /// `root/src` - where `.c` source files are discovered.
    pub src_dir: PathBuf,
    /// `root/target` - where object files and the final binary are written.
    pub build_dir: PathBuf,
    /// `root/target/deps` - where dependency install prefixes live (see
    /// [`Project::dep_prefix`]).
    pub install_dir: PathBuf,

    /// Build results for each resolved dependency, keyed by dependency
    /// name. Empty right after [`Project::load`] - populated
    /// incrementally as dependencies are resolved and built.
    pub resolved_deps: Vec<(String, BuildOutput)>,
}

impl Project {
    /// Load an existing project from `project_dir`: parse `smidr.toml`
    /// and compute the standard `src/`, `target/`, and `target/deps/`
    /// paths relative to it.
    ///
    /// # Errors
    /// Propagates [`BuildError::ManifestNotFound`] or [`BuildError::TomlDe`]
    /// from [`ManifestConfig::load`] if `smidr.toml` is missing or invalid.
    pub fn load(project_dir: &Path) -> Result<Self> {
        let config = ManifestConfig::load(project_dir)?;

        Ok(Self {
            root: project_dir.to_path_buf(),
            src_dir: project_dir.join("src"),
            build_dir: project_dir.join("target"),
            install_dir: project_dir.join("target/deps"),
            config,
            resolved_deps: Vec::new(),
        })
    }

    /// Scaffold a brand new project named `name` in the current directory:
    /// creates `src/`, `include/`, a starter `src/main.c`, a generated
    /// `smidr.toml`, and a `.gitignore`.
    ///
    /// # Errors
    /// Returns [`BuildError::InvalidProjectName`] if `name` is empty or
    /// contains a path separator or `.`/`..` (this also guards against
    /// writing outside the intended directory). Returns
    /// [`BuildError::ProjectAlreadyExists`] if `name` already exists on
    /// disk.
    pub fn init(
        name: &str,
        project_type: ProjectType,
        c_standard: Option<CStandard>,
    ) -> Result<()> {
        if name.is_empty()
            || name.contains('/')
            || name.contains('\\')
            || name == "."
            || name == ".."
        {
            return Err(BuildError::InvalidProjectName(name.to_string()));
        }

        let root = PathBuf::from(name);
        if root.exists() {
            return Err(BuildError::ProjectAlreadyExists(root));
        }

        std::fs::create_dir_all(root.join("src"))?;
        std::fs::create_dir_all(root.join("include"))?;

        match project_type {
            ProjectType::Binary => {
                std::fs::write(root.join("src/main.c"), MAIN_C_TEMPLATE)?;
            }
            ProjectType::StaticLibrary | ProjectType::SharedLibrary => {
                std::fs::write(root.join("src/lib.c"), LIB_C_TEMPLATE)?;
                std::fs::write(root.join("include/lib.h"), LIB_H_TEMPLATE)?;
            }
        }

        let config = ManifestConfig {
            project: ProjectSection {
                name: name.to_string(),
                version: "0.1.0".to_string(),
                authors: Vec::new(),
                authors_email: Vec::new(),
                description: None,
                license: None,
                project_type,
                c_standard: c_standard.unwrap_or_default(),
            },
            build: BuildSection {
                compiler: Default::default(),
                warnings: Default::default(),
                cflags: Vec::new(),
            },
            dependencies: BTreeMap::new(),
        };
        std::fs::write(root.join("Smidr.toml"), config.to_toml_string()?)?;
        std::fs::write(root.join(".gitignore"), GITIGNORE_TEMPLATE)?;

        println!("Created project: {}", name);
        Ok(())
    }

    /// Find every `.c` file directly inside `src_dir` (non-recursive).
    ///
    /// # Errors
    /// Returns [`BuildError::NoSourceFiles`] if `src/` exists but contains
    /// no `.c` files - this is treated as a build error rather than
    /// silently producing an empty binary.
    pub fn source_files(&self) -> Result<Vec<PathBuf>> {
        let mut sources = Vec::new();
        
        let entries = std::fs::read_dir(&self.src_dir).map_err(BuildError::Io)?;
        for entry in entries {
            let entry = entry.map_err(BuildError::Io)?;
            let path = entry.path();
            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("c") {
                sources.push(path);
            }
        }
        if sources.is_empty() {
            return Err(BuildError::NoSourceFiles);
        }
        Ok(sources)
    }

    /// The install prefix for a given dependency - where its build system
    /// (CMake, Meson, Make, or a custom script) is told to install its
    /// `include/` and `lib/` output.
    pub fn dep_prefix(&self, dep_name: &str) -> PathBuf {
        self.install_dir.join(dep_name)
    }
}