From a20c61cc88eda1e76e8e24fac6a8953c87242eb6 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 5 Aug 2026 19:21:00 +0800 Subject: [PATCH] docs: add and clean up AI-generated rustdoc comments (grok-4.5) --- src/cli/add.rs | 35 ++++++++++- src/cli/build.rs | 47 +++++++++++++++ src/cli/clean.rs | 39 ++++++++++++ src/cli/flag_update.rs | 25 ++++++++ src/cli/flags.rs | 38 ++++++++++++ src/cli/fmt.rs | 8 +++ src/cli/gen.rs | 59 ++++++++++++++++-- src/cli/help.rs | 6 ++ src/cli/init.rs | 10 +++- src/cli/lint.rs | 5 ++ src/cli/mod.rs | 3 + src/cli/new.rs | 9 ++- src/cli/run.rs | 19 ++++++ src/cli/setup.rs | 2 + src/cli/test.rs | 10 ++++ src/cli/tree.rs | 10 ++++ src/config.rs | 1 + src/core/build/archive.rs | 4 ++ src/core/build/builder/artifact.rs | 28 +++++++++ src/core/build/builder/cc_common.rs | 20 +++++++ src/core/build/builder/mod.rs | 15 +++++ src/core/build/builder/msvc/mod.rs | 15 +++++ src/core/build/cache.rs | 31 ++++++++++ src/core/build/common.rs | 65 ++++++++++++++++++++ src/core/build/engine.rs | 30 +++++++++- src/core/build/language/asm/common.rs | 34 +++++++++++ src/core/build/language/asm/fasm/mod.rs | 6 ++ src/core/build/language/asm/gas/mod.rs | 13 ++++ src/core/build/language/asm/masm/mod.rs | 14 +++++ src/core/build/language/asm/mod.rs | 7 +++ src/core/build/language/asm/nasm/mod.rs | 17 ++++++ src/core/build/language/c/mod.rs | 6 ++ src/core/build/language/cxx/mod.rs | 6 ++ src/core/build/language/cxx/qt.rs | 12 ++++ src/core/build/language/llvm_ir/mod.rs | 10 ++++ src/core/build/language/mod.rs | 10 ++++ src/core/build/mod.rs | 3 + src/core/build/report.rs | 7 +++ src/core/build/resolve.rs | 35 +++++++++++ src/core/build/steps.rs | 19 ++++++ src/core/build_config.rs | 23 ++++++- src/core/deps/common.rs | 1 + src/core/deps/lock.rs | 5 ++ src/core/deps/mod.rs | 23 +++++++ src/core/deps/register.rs | 28 +++++++++ src/core/mod.rs | 1 + src/core/registry.rs | 8 +++ src/core/runner.rs | 12 ++++ src/core/vcs/git.rs | 6 +- src/core/vcs/mod.rs | 7 ++- src/core/workspace.rs | 21 +++++++ src/main.rs | 5 ++ src/platform/bsd.rs | 6 ++ src/platform/linux.rs | 7 +++ src/platform/macos.rs | 9 +++ src/platform/mod.rs | 23 +++++++ src/platform/path_util.rs | 7 +++ src/platform/posix.rs | 12 ++++ src/platform/windows.rs | 11 ++++ src/utils/build.rs | 79 +++++++++++++++++++++++++ src/utils/fs.rs | 37 ++++++++++++ src/utils/git.rs | 4 ++ src/utils/log.rs | 3 + src/utils/mod.rs | 10 ++++ src/utils/text.rs | 4 ++ 65 files changed, 1070 insertions(+), 15 deletions(-) diff --git a/src/cli/add.rs b/src/cli/add.rs index e9b8e6c..474db51 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -23,16 +23,38 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use toml::Value; use toml::map::Map; +/// Parsed arguments for the `dcr add` command. +/// +/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set, +/// depending on how the dependency source was specified. pub struct AddArgs { + /// Dependency name as it will appear under `[dependencies]` in `dcr.toml`. pub name: String, + /// Local filesystem path when the source uses the `path:` prefix. pub path: Option, + /// Git repository URL when the source is a git/GitHub/GitLab URL or prefix. pub git: Option, + /// Optional git branch to pin the dependency to. pub branch: Option, + /// Optional git tag to pin the dependency to. pub tag: Option, + /// Optional git commit hash (revision) to pin the dependency to. pub rev: Option, + /// Version string resolved from the package registry when no explicit source is given. pub version_from_registry: Option, } +/// Adds a dependency to the current project's `dcr.toml`. +/// +/// Parses CLI arguments, locates the project root, builds a TOML dependency value +/// (path, git with optional ref, or registry version), and writes it under +/// `dependencies.`. +/// +/// # Parameters +/// - `args`: Tokens after `dcr add` (name, source, optional `--branch`/`--tag`/`--rev`). +/// +/// # Returns +/// Process exit code: `0` on success, non-zero on usage or I/O errors. pub fn add(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -91,6 +113,7 @@ pub fn add(args: &[String]) -> i32 { } }; + // Build the TOML value written under dependencies. let dep_value = if let Some(git_url) = add_args.git { if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() { let mut table = Map::new(); @@ -135,6 +158,11 @@ pub fn add(args: &[String]) -> i32 { 0 } +/// Parses CLI arguments into an [`AddArgs`] value. +/// +/// Expects ` [source] [--branch|--tag|--rev ...]`. If `source` is omitted, +/// the package registry is queried for a default version. Source strings may use +/// `path:`, `github:`, `gitlab:`, or `git:` prefixes, or a full git URL. fn parse_add_args(args: &[String]) -> Result { if args.is_empty() { error("Usage: dcr add [--branch | --tag | --rev ]"); @@ -168,7 +196,7 @@ fn parse_add_args(args: &[String]) -> Result { } source_spec = s.clone(); } else { - // No source provided — try registry auto-lookup + // No source provided — resolve version from the package registry match register::resolve_package_from_registry(&name) { Ok(info) => { let version = info @@ -235,6 +263,7 @@ fn parse_add_args(args: &[String]) -> Result { let mut path = None; let mut git = None; + // Map source prefixes / bare URLs to path or git fields if let Some(p) = source_spec.strip_prefix("path:") { path = Some(p.to_string()); } else if let Some(g) = source_spec.strip_prefix("github:") { @@ -243,10 +272,10 @@ fn parse_add_args(args: &[String]) -> Result { git = Some(format!("https://gitlab.com/{}", g)); } else if let Some(g) = source_spec.strip_prefix("git:") { if g.contains('/') && !g.contains('.') { - // git:user/repo -> default to github + // git:user/repo with no host — default to GitHub git = Some(format!("https://github.com/{}", g)); } else { - // git:host.com/user/repo + // git:host.com/user/repo or an already-qualified URL let url = if g.starts_with("http") || g.starts_with("git@") { g.to_string() } else { diff --git a/src/cli/build.rs b/src/cli/build.rs index 66551c6..2975bfe 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -35,19 +35,31 @@ use std::sync::{Arc, atomic::AtomicBool}; pub use crate::core::build::get_build_string_with_profile; +/// Formats a left-aligned status verb with the given style for CLI output. fn status(verb: &str, style: &str) -> String { colored(&format!("{verb:<9}"), style) } +/// CLI build reporter that prints human-readable progress to stderr/stdout. struct CliReporter; impl CliReporter { + /// Prints a single status line with a styled verb and free-form rest text. + /// + /// # Parameters + /// - `verb`: Short status label (e.g. `"target"`, `"dep"`). + /// - `style`: Color/style constant for the verb. + /// - `rest`: Remaining message text after the verb. fn line(&self, verb: &str, style: &str, rest: &str) { eprintln!(" {} {}", status(verb, style), rest); } } impl BuildReporter for CliReporter { + /// Handles build events by printing progress, status lines, and compiler output. + /// + /// # Parameters + /// - `event`: Build pipeline event to report to the user. fn on_event(&mut self, event: BuildEvent<'_>) { match event { BuildEvent::TargetStart { @@ -80,6 +92,7 @@ impl BuildReporter for CliReporter { rebuilt, } => { common::finish_progress_line(); + // Only announce deps that were actually rebuilt this run. if rebuilt { self.line("ready", BOLD_GREEN, &format!("{name} v{version}")); } @@ -87,6 +100,7 @@ impl BuildReporter for CliReporter { BuildEvent::Compiling { name, version } => { common::finish_progress_line(); let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version); + // Terminals use an in-place progress line; non-TTY gets a plain log line. common::set_progress_label(Some(label.clone())); if !std::io::stderr().is_terminal() { eprintln!("{label}"); @@ -101,6 +115,7 @@ impl BuildReporter for CliReporter { self.line("done", BOLD_GREEN, &format!("in {secs}s")); } BuildEvent::CompilerOutput { stream, text } => { + // Pause the progress spinner so compiler text is not overwritten. common::interrupt_progress_for_output(); if stream == "stderr" { eprint!("{text}"); @@ -112,6 +127,17 @@ impl BuildReporter for CliReporter { } } +/// Runs the project build command from the CLI. +/// +/// Parses flags, locates the project root, optionally cleans, then invokes the +/// core build pipeline. On success, may write `.dcr` metadata and print the +/// main artifact path when requested. +/// +/// # Parameters +/// - `args`: Tokens after `dcr build` (profile, target, force, clean, etc.). +/// +/// # Returns +/// Process exit code: `0` on success or help, non-zero on failure. pub fn build(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -137,6 +163,7 @@ pub fn build(args: &[String]) -> i32 { return 0; } + // Shared cancel flag so Ctrl-C can stop an in-progress build cooperatively. let cancel = Arc::new(AtomicBool::new(false)); let cancel_clone = cancel.clone(); ctrlc::set_handler(move || { @@ -166,6 +193,7 @@ pub fn build(args: &[String]) -> i32 { Err(code) => return code, }; + // Prefer CLI --target; otherwise fall back to profile target from dcr.toml. if flags.target.is_none() { let config_path = root.join("dcr.toml"); if let Ok(config) = Config::open(config_path.to_str().unwrap()) @@ -194,6 +222,7 @@ pub fn build(args: &[String]) -> i32 { let mut reporter = CliReporter; match run_build(&root, &req, &mut reporter) { Ok(_) => { + // Persist IDE/metadata and optionally echo the main artifact path. if let Some(info) = resolve_post_build_info(&root, &flags.profile, flags.target.as_deref()) { @@ -219,6 +248,19 @@ pub fn build(args: &[String]) -> i32 { } } +/// Collects project and artifact metadata after a successful build. +/// +/// Reads `dcr.toml`, resolves language/compiler/kind/target/output settings for +/// the given profile, and computes absolute artifact and target directory paths. +/// +/// # Parameters +/// - `root`: Project root with `dcr.toml`. +/// - `profile`: Build profile used for the just-finished build. +/// - `cli_target`: Optional target from the CLI (overrides config when set). +/// +/// # Returns +/// `Some(ProjectInfo)` for a normal package; `None` for workspace-only roots +/// or when required config/paths cannot be resolved. fn resolve_post_build_info( root: &Path, profile: &str, @@ -262,6 +304,7 @@ fn resolve_post_build_info( tc_as.as_deref(), ); let out_dir = get_string_with_profile(&config, "out_dir", profile); + // Explicit target (CLI or config) changes how the artifact directory is laid out. let has_explicit = cli_target.is_some_and(|t| !t.is_empty()) || !build_target.trim().is_empty(); let target_dir = resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit); @@ -319,6 +362,10 @@ fn resolve_post_build_info( }) } +/// Best-effort absolute main artifact path when full post-build info is unavailable. +/// +/// Assumes a binary (`"bin"`) kind and uses the package name plus profile, +/// target, and `out_dir` settings from config. fn fallback_artifact_path(root: &Path, profile: &str, target: Option<&str>) -> Option { let config = Config::open(root.join("dcr.toml").to_str()?).ok()?; let name = get_config_str(&config, "package.name"); diff --git a/src/cli/clean.rs b/src/cli/clean.rs index aea2f5f..2c02b95 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -25,6 +25,15 @@ use glob::glob; use std::fs; use std::path::Path; +/// Entry point for the `dcr clean` command. +/// +/// Removes build artifacts from the project (and optionally workspace members). +/// +/// # Parameters +/// - `args`: Tokens after `dcr clean` (`--debug`/`--release`, `--target`, `--all`). +/// +/// # Returns +/// Process exit code: `0` on success, `1` on failure. pub fn clean(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -70,6 +79,7 @@ pub fn clean(args: &[String]) -> i32 { } }; + // Run cleanup relative to the project root so paths resolve consistently. match with_dir(&root, || clean_from_root(&root, &flags)) { Ok(()) => 0, Err(msg) => { @@ -79,12 +89,20 @@ pub fn clean(args: &[String]) -> i32 { } } +/// Parsed flags controlling which artifacts `clean` should remove. struct CleanFlags { + /// Optional build profile name (e.g. `debug`, `release`). profile: Option, + /// Optional target triple to clean under `target//...`. target: Option, + /// When true, also clean each workspace member that has a `target` directory. all: bool, } +/// Parses CLI arguments into [`CleanFlags`]. +/// +/// Recognizes `--all`, `--target `, and profile flags such as `--debug` +/// / `--release` (validated via [`default_profile_flags`]). fn parse_clean_flags(args: &[String]) -> Result { let mut profile: Option = None; let mut target: Option = None; @@ -104,6 +122,7 @@ fn parse_clean_flags(args: &[String]) -> Result { continue; } if arg.starts_with("--") { + // Accept only known profile names (e.g. debug/release), not arbitrary flags. let candidate = arg.trim_start_matches("--").to_string(); if !default_profile_flags(&candidate).is_empty() { if profile.is_some() { @@ -122,9 +141,14 @@ fn parse_clean_flags(args: &[String]) -> Result { }) } +/// Cleans the project at `root`, and optionally all workspace members. +/// +/// Resolves the effective target triple from flags, `dcr.toml`, or host defaults, +/// then removes the appropriate `target` subdirectories and custom clean paths. fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; + // Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD. let target = flags.target.clone().or_else(|| { config .get("build.target") @@ -165,6 +189,12 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { clean_project_at(root, flags.profile.as_deref(), target.as_deref()) } +/// Removes build artifacts for a single project directory. +/// +/// When `profile` is set, only that profile's directory under `target` (optionally +/// scoped by `target` triple) is removed, plus custom paths for that profile. +/// When `profile` is `None`, the entire `target` tree is removed and custom paths +/// are cleaned for both `debug` and `release`. fn clean_project_at( project_root: &Path, profile: Option<&str>, @@ -192,6 +222,7 @@ fn clean_project_at( }; let target_items = check_dir(Some("target")).unwrap_or_default(); let parent_dir = target.unwrap_or(""); + // Existence check differs: with a triple, look for target/; otherwise target/. let dir_exists = if target.is_some() { target_items.contains(&parent_dir.to_string()) } else { @@ -230,6 +261,10 @@ fn clean_project_at( }) } +/// Deletes extra paths listed under `build.clean` in `dcr.toml` for a profile. +/// +/// Each pattern may contain `{profile}` and version placeholders, which are +/// expanded before matching with `glob`. Matched files and directories are removed. fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> { let patterns = match config.get("build.clean") { Some(v) => v @@ -260,6 +295,10 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> { Ok(()) } +/// Expands version-related placeholders in a clean path pattern. +/// +/// Reads `package.version` from `config`, parses it, and applies +/// [`crate::utils::build::substitute_version_vars`]. fn substitute_clean_vars(value: &str, config: &Config) -> String { let version = config .get("package.version") diff --git a/src/cli/flag_update.rs b/src/cli/flag_update.rs index 5b4c5b4..8e0fa0d 100644 --- a/src/cli/flag_update.rs +++ b/src/cli/flag_update.rs @@ -27,18 +27,32 @@ use std::time::{SystemTime, UNIX_EPOCH}; const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest"; +/// Metadata for the latest GitHub release of DCR. #[derive(Debug, Deserialize)] struct Release { + /// Release tag, typically including a leading `v` (e.g. `v1.2.3`). tag_name: String, + /// Downloadable assets attached to the release. assets: Vec, } +/// A single downloadable asset from a GitHub release. #[derive(Debug, Deserialize)] struct ReleaseAsset { + /// Asset file name as published on GitHub. name: String, + /// Direct URL used to download the asset. browser_download_url: String, } +/// Handles the `dcr --update` flag: checks GitHub for a newer release and +/// replaces the running binary when an update is available. +/// +/// # Parameters +/// - `args`: Remaining tokens after `--update` (only `--help` is accepted). +/// +/// # Returns +/// `0` on success / already up to date / pacman-managed refuse; `1` on error. pub fn flag_update(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -62,6 +76,7 @@ pub fn flag_update(args: &[String]) -> i32 { } }; + // Refuse self-update when the binary is owned by pacman/AUR. if let Some(package_name) = pacman_owned_package(¤t_exe) { warn("This dcr binary is managed by pacman/AUR"); println!( @@ -81,6 +96,7 @@ pub fn flag_update(args: &[String]) -> i32 { } }; + // Normalize the tag by stripping a leading `v` before comparison. let latest_version = release.tag_name.trim_start_matches('v'); if latest_version == current_version { println!("Latest version is already installed: {current_version}"); @@ -113,6 +129,7 @@ pub fn flag_update(args: &[String]) -> i32 { } set_executable_permissions(&temp_path); + // Replace the running executable in place, then clean up the temp file. if self_replace::self_replace(&temp_path).is_err() { let _ = fs::remove_file(&temp_path); error("Failed to replace current binary"); @@ -124,6 +141,7 @@ pub fn flag_update(args: &[String]) -> i32 { 0 } +/// Fetches the latest DCR release metadata from the GitHub Releases API. fn fetch_latest_release() -> Result { let response = ureq::get(LATEST_RELEASE_URL) .set("User-Agent", "dcr-updater") @@ -138,6 +156,7 @@ fn fetch_latest_release() -> Result { .map_err(|_| "GitHub API response has an unexpected format".to_string()) } +/// Downloads a release asset from the given URL into memory. fn download_asset(url: &str) -> Result, String> { let response = ureq::get(url) .set("User-Agent", "dcr-updater") @@ -155,9 +174,11 @@ fn download_asset(url: &str) -> Result, String> { Ok(data) } +/// Builds the list of expected asset file names for the given build target. fn asset_candidates(target: &str) -> Vec { let mut names = vec![format!("dcr-{target}")]; + // Windows releases may publish the binary with an `.exe` suffix. if target.contains("-windows-") || target.ends_with("-windows") { names.push(format!("dcr-{target}.exe")); } @@ -165,6 +186,7 @@ fn asset_candidates(target: &str) -> Vec { names } +/// Creates a unique temporary path next to the current executable for the download. fn temp_binary_path(current_exe: &Path) -> PathBuf { let stamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -177,6 +199,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf { current_exe.with_extension(extension) } +/// Sets executable permissions on Unix; no-op on other platforms. fn set_executable_permissions(_path: &Path) { #[cfg(unix)] { @@ -189,6 +212,7 @@ fn set_executable_permissions(_path: &Path) { } } +/// On Linux, returns the pacman package that owns `path`, if any. #[cfg(target_os = "linux")] fn pacman_owned_package(path: &Path) -> Option { let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?; @@ -204,6 +228,7 @@ fn pacman_owned_package(path: &Path) -> Option { Some(package_name) } +/// Non-Linux stub: package-manager ownership is not checked. #[cfg(not(target_os = "linux"))] fn pacman_owned_package(_path: &Path) -> Option { None diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 14fe15e..257eddf 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -18,17 +18,36 @@ use crate::config::{PROFILE, flags}; use crate::utils::log::warn; +/// Flags shared by the `build` and `run` CLI commands. pub struct BuildRunFlags { + /// Build profile name (for example `debug` or `release`). pub profile: String, + /// Optional target triple or platform name. pub target: Option, + /// Optional workspace member name (`--workspace `). pub workspace: Option, + /// Force a rebuild even when artifacts appear up to date. pub force: bool, + /// Clean build outputs before building. pub clean: bool, + /// Enable verbose logging during the build or run. pub verbose: bool, + /// Print the final artifact path to the user. pub print_artifact_path: bool, + /// Arguments forwarded to the binary after a `--` separator. pub bin_args: Vec, } +/// Splits CLI arguments at the first `--` separator. +/// +/// Everything before `--` is treated as DCR flags; everything after is +/// forwarded to the built binary as `bin_args`. +/// +/// # Parameters +/// - `args`: Full argument list for the command (without the program name). +/// +/// # Returns +/// `(dcr_args, bin_args)`. If `--` is absent, `bin_args` is empty. pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) { match args.iter().position(|a| a == "--") { Some(i) => (&args[..i], &args[i + 1..]), @@ -36,6 +55,19 @@ pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) { } } +/// Parses build/run flags from a list of CLI arguments. +/// +/// Recognizes boolean flags (`--force`, `--clean`, `--verbose`, +/// `--print-artifact-path`), value flags (`--workspace`, `--target`), and +/// profile names registered via [`flags`]. Arguments after `--` become +/// [`BuildRunFlags::bin_args`]. +/// +/// # Parameters +/// - `args`: CLI arguments for build/run (may include `--`). +/// +/// # Returns +/// - `Ok(BuildRunFlags)` on success. +/// - `Err(1)` for unknown flags, missing values, or a duplicate profile. pub fn parse_build_run_flags(args: &[String]) -> Result { let (dcr_args, bin_args_slice) = split_double_dash(args); let mut profile = PROFILE.to_string(); @@ -48,6 +80,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { let mut iter = dcr_args.iter(); while let Some(arg) = iter.next() { + // DCR build/run options are long flags only (`--name`). if !arg.starts_with("--") { warn("Unknown argument"); return Err(1); @@ -87,6 +120,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { } continue; } + // Profile names come from config::flags; only one profile may be set. if flags(candidate).is_some() { if profile != PROFILE { warn("Duplicate profile flag"); @@ -115,10 +149,12 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { mod tests { use super::*; + /// Converts string literals into owned `String` CLI arguments for tests. fn s(args: &[&str]) -> Vec { args.iter().map(|a| a.to_string()).collect() } + /// Checks that without `--`, the full list stays on the DCR side. #[test] fn split_double_dash_empty_tail() { let args = s(&["--release", "--force"]); @@ -127,6 +163,7 @@ mod tests { assert!(right.is_empty()); } + /// Checks that arguments after `--` are split into the bin-args slice. #[test] fn split_double_dash_with_bin_args() { let args = s(&["--release", "--", "--test_help", "x"]); @@ -135,6 +172,7 @@ mod tests { assert_eq!(right, &s(&["--test_help", "x"])); } + /// Checks profile parsing and that bin args after `--` are forwarded. #[test] fn parse_run_flags_forwards_after_double_dash() { let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap(); diff --git a/src/cli/fmt.rs b/src/cli/fmt.rs index 551eae1..0f91e7c 100644 --- a/src/cli/fmt.rs +++ b/src/cli/fmt.rs @@ -20,6 +20,12 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use glob::glob; use std::process::Command; +/// Formats all C/C++ source files using clang-format. +/// +/// Scans the `src/` and `tests/` directories recursively for files with common +/// C/C++ extensions and formats them in place with clang-format. +/// +/// If the first argument is `--help`, prints usage information and returns 0. pub fn fmt(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -31,6 +37,7 @@ pub fn fmt(args: &[String]) -> i32 { return 0; } + // Collect all matching files from src/ and tests/ using glob patterns let patterns = [ "src/**/*.c", "src/**/*.cpp", @@ -66,6 +73,7 @@ pub fn fmt(args: &[String]) -> i32 { return 0; } + // Format the collected files println!( " {} {} files", colored("Formatting", BOLD_GREEN), diff --git a/src/cli/gen.rs b/src/cli/gen.rs index 234d342..d75d0ca 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -34,14 +34,20 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; -/// Like `deps::resolve_deps` but does NOT require lib directories to exist. -/// Used by `gen` commands where the project may not have been built yet. +/// Path-dep include/lib/libs metadata for `dcr gen` (project may not be built yet). +/// +/// Explicit include/lib lists may point at missing paths; default `include`/`lib` +/// candidates are added only when those directories exist. struct GenDeps { include_dirs: Vec, lib_dirs: Vec, libs: Vec, } +/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation. +/// +/// Skips `system` deps and non-path entries. Falls back to common include/lib +/// directory names only when those directories exist. fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps { let deps_val = match config.get("dependencies") { Some(v) => v, @@ -168,6 +174,7 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> // ── public API ─────────────────────────────────────────────────────────────── +/// Per-project (or workspace member) metadata collected for `dcr gen`. pub struct ProjectInfo { pub name: String, pub version: String, @@ -201,8 +208,13 @@ pub struct ProjectInfo { pub workspace_root: Option, } -// ── entry-point for `dcr gen` ──────────────────────────────────────────────── - +/// Entry point for `dcr gen`: dispatches to project-info / compile-commands / vscode / clion. +/// +/// # Parameters +/// - `args`: First token is the subcommand; the rest are subcommand flags. +/// +/// # Returns +/// Process exit code from the chosen subcommand (`0` for help). pub fn r#gen(args: &[String]) -> i32 { let subcommand = match args.first() { Some(s) if s == "--help" => { @@ -253,6 +265,7 @@ pub fn r#gen(args: &[String]) -> i32 { // ── shared: collect per-project data ───────────────────────────────────────── +/// Collects [`ProjectInfo`] with cwd set to the project root so relative paths resolve. fn collect_project_info( root: &Path, profile: &str, @@ -265,6 +278,9 @@ fn collect_project_info( result } +/// Reads `dcr.toml` and resolves compiler, deps, sources, and artifact paths for one project. +/// +/// Called once per project/member from [`collect_all`] (not recursive). fn collect_project_info_inner( root: &Path, profile: &str, @@ -576,6 +592,10 @@ fn find_in_path(name: &str) -> Option { None } +/// Resolves a tool name (e.g. moc, uic, rcc) for generated metadata. +/// +/// Prefers a PATH hit via [`find_in_path`]; otherwise returns the trimmed name +/// as given (absolute or relative). Empty input yields `None`. fn resolve_tool_path(name: &str) -> Option { let t = name.trim(); if t.is_empty() { @@ -584,6 +604,8 @@ fn resolve_tool_path(name: &str) -> Option { find_in_path(t).or_else(|| Some(t.to_string())) } +/// Picks a debugger name: configured value if set, else first of lldb/gdb on PATH, +/// else `cppvsdbg` on Windows or `lldb` elsewhere (even if not on PATH). fn resolve_debugger(configured: Option<&str>) -> String { if let Some(d) = configured { let t = d.trim(); @@ -617,6 +639,7 @@ fn utc_now_iso() -> String { format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z") } +/// Converts days since the Unix epoch to `(year, month, day)` (proleptic Gregorian). fn civil_from_days(days: i64) -> (i32, u32, u32) { let z = days + 719468; let era = if z >= 0 { z } else { z - 146096 } / 146097; @@ -631,6 +654,14 @@ fn civil_from_days(days: i64) -> (i32, u32, u32) { (y as i32, m, d) } +/// Writes `.dcr/build-info.json` and `.dcr/toolchain.json` from project metadata. +/// +/// # Parameters +/// - `root`: Project root (creates `.dcr/` if needed). +/// - `info`: Collected project metadata (compiler, artifact, toolchain tools). +/// +/// # Returns +/// `Ok(())` on success, or an error string if directory/file writes fail. pub fn write_dcr_metadata(root: &Path, info: &ProjectInfo) -> Result<(), String> { let dcr = ensure_dcr_dir(root).map_err(|e| format!("Failed to create .dcr/: {e}"))?; @@ -723,6 +754,7 @@ fn ensure_clangd(root: &Path, quiet: bool) { let _ = std::fs::write(&path, desired); } +/// Collects [`ProjectInfo`] for the root package and workspace members (skips failed members). fn collect_all(root: &Path, profile: &str) -> Result, String> { let root = canonicalize_path(root); let config = { @@ -776,6 +808,7 @@ fn collect_all(root: &Path, profile: &str) -> Result, String> { // ── dcr gen project-info ───────────────────────────────────────────────────── +/// `dcr gen project-info`: print project metadata JSON; writes `.dcr` meta from the last entry. fn gen_project_info(args: &[String]) -> i32 { let (root, profile, quiet) = match parse_gen_args(args) { Ok(v) => v, @@ -811,6 +844,7 @@ fn gen_project_info(args: &[String]) -> i32 { 0 } +/// Pretty multi-line JSON object for one [`ProjectInfo`] (used inside a JSON array). fn project_info_to_json(info: &ProjectInfo) -> String { let mut out = String::new(); out.push_str(" {\n"); @@ -896,6 +930,7 @@ fn project_info_to_json(info: &ProjectInfo) -> String { // ── dcr gen compile-commands ───────────────────────────────────────────────── +/// `dcr gen compile-commands`: write `.dcr/compile_commands.json`. fn gen_compile_commands(args: &[String]) -> i32 { let (root, profile, quiet) = match parse_gen_args(args) { Ok(v) => v, @@ -913,6 +948,9 @@ fn gen_compile_commands(args: &[String]) -> i32 { gen_compile_commands_inner(&root, &profile, &all, quiet) } +/// Writes compile_commands.json, build metadata, and ensures `.clangd`. +/// +/// Used by `compile-commands`, `vscode`, and `clion`. fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 { if let Err(e) = ensure_dcr_dir(root) { error(&format!("Failed to create .dcr/: {e}")); @@ -942,6 +980,7 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q } } +/// Builds a clangd/cpptools `compile_commands.json` array (one entry per source file). fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String { let mut out = String::from("[\n"); let mut first = true; @@ -972,6 +1011,8 @@ fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String { out } +/// Assembles one compiler argv: compiler, `-c`, optional `-x` asm, source/obj paths, +/// `-fPIC`, `-std=`, profile defaults, cflags, and include paths. fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec { let mut cmd: Vec = Vec::new(); let compiler = if info.compiler.is_empty() { @@ -1027,7 +1068,7 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec } } - // Default profile flags (mirrors unix_cc.rs defaults) + // Default profile flags (same idea as default_profile_flags) match profile { "release" => { cmd.push("-O3".to_string()); @@ -1076,6 +1117,7 @@ fn strip_src_prefix(p: &Path) -> PathBuf { // ── dcr gen vscode ─────────────────────────────────────────────────────────── +/// `dcr gen vscode`: compile_commands plus `.vscode/` tasks, launch, settings, extensions. fn gen_vscode(args: &[String]) -> i32 { let (root, profile, quiet) = match parse_gen_args(args) { Ok(v) => v, @@ -1216,6 +1258,8 @@ fn gen_tasks_json() -> String { .to_string() } +/// Absolute program path for a launch config; reuses `artifact_path` when profiles match, +/// otherwise recomputes from kind/target/out_dir/filename. fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String { if info.profile == profile && let Some(ref p) = info.artifact_path @@ -1257,6 +1301,7 @@ fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String { } } +/// VS Code debug type from debugger name: `cppdbg`, `cppvsdbg`, or `lldb`. fn debug_type_for(info: &ProjectInfo) -> &'static str { let d = info.debugger.to_lowercase(); if d.contains("gdb") || d == "cppdbg" { @@ -1268,6 +1313,7 @@ fn debug_type_for(info: &ProjectInfo) -> &'static str { } } +/// `launch.json` content: one debug config per `bin` target for debug and release. fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String { let mut configs = Vec::new(); @@ -1361,6 +1407,7 @@ fn gen_settings_json(root: &Path) -> String { // ── dcr gen clion ───────────────────────────────────────────────────────────── +/// `dcr gen clion`: compile_commands plus `.idea/` tools, targets, misc, run configs. fn gen_clion(args: &[String]) -> i32 { let (root, profile, quiet) = match parse_gen_args(args) { Ok(v) => v, @@ -1571,6 +1618,7 @@ fn gen_clion_misc_xml(root: &Path) -> String { ) } +/// XML for one CLion run configuration under `.idea/runConfigurations/`. fn gen_clion_run_config(info: &ProjectInfo, _root: &Path, profile: &str) -> String { let bin_path = resolve_launch_program(info, profile); let bin = xml_escape(&bin_path); @@ -1623,6 +1671,7 @@ fn xml_escape(s: &str) -> String { // ── helpers ─────────────────────────────────────────────────────────────────── +/// Parses `--debug` / `--release` / `--quiet` and resolves the project root. fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> { let mut profile = "debug".to_string(); let mut quiet = false; diff --git a/src/cli/help.rs b/src/cli/help.rs index 0f3d771..2a973de 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -17,6 +17,12 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; +/// Displays the help information for the DCR CLI tool. +/// +/// Prints usage, commands, flags, options, examples, and a tip. +/// +/// # Returns +/// Always `0`. pub fn help() -> i32 { println!("DCR (Dexoron Cargo Realization)"); println!("C project manager inspired by Cargo."); diff --git a/src/cli/init.rs b/src/cli/init.rs index c98c4b8..0c0537c 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -25,6 +25,10 @@ use std::fs; use std::io::Write; use toml::Value; +/// Initializes the current working directory as a new DCR project. +/// +/// Verifies that the current directory is empty, sets up the project structure by creating +/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable. pub fn init(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -36,6 +40,7 @@ pub fn init(args: &[String]) -> i32 { return 0; } + // Parse options (--vcs) and collect positional arguments. let mut vcs_str = None; let mut clean_args = Vec::new(); let mut iter = args.iter(); @@ -59,6 +64,7 @@ pub fn init(args: &[String]) -> i32 { return 1; } + // Resolve target project name from current directory name and enforce empty directory check. let items = check_dir(None).unwrap_or_default(); let project_name = std::env::current_dir() .ok() @@ -84,6 +90,7 @@ pub fn init(args: &[String]) -> i32 { .unwrap_or_else(|_| ".".to_string()); println!("Initializing the project in {cwd}"); + // Create and initialize default manifest (dcr.toml). let mut config = match Config::new("./dcr.toml") { Ok(cfg) => cfg, Err(_) => { @@ -104,6 +111,7 @@ pub fn init(args: &[String]) -> i32 { colored("dcr.toml", BOLD_CYAN) ); + // Create src directory and write template main.c file. if fs::create_dir("src").is_err() { error("Failed to create src/"); return 1; @@ -125,7 +133,7 @@ pub fn init(args: &[String]) -> i32 { colored("src/main.c", BOLD_CYAN) ); - // settings VCS + // Resolve target VCS provider or detect existing repositories. let mut vcs_kind = VcsKind::Git; if let Some(ref vcs_val) = vcs_str { match VcsKind::parse(vcs_val) { diff --git a/src/cli/lint.rs b/src/cli/lint.rs index 3556ccb..b47b2ec 100644 --- a/src/cli/lint.rs +++ b/src/cli/lint.rs @@ -24,6 +24,9 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc}; use std::path::{Path, PathBuf}; use std::process::Command; +/// Runs clang-tidy on all C/C++ source files. +/// +/// Scans source roots (configurable via build.roots) and tests/ directory. pub fn lint(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -72,6 +75,7 @@ pub fn lint(args: &[String]) -> i32 { let build_roots = get_list_with_profile(&config, "roots", profile); let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); + // Construct source roots from config let mut source_roots: Vec = Vec::new(); for raw in &build_roots { let p = Path::new(raw); @@ -126,6 +130,7 @@ pub fn lint(args: &[String]) -> i32 { let mut total_issues = 0u32; let mut failed = 0u32; + // Execute clang-tidy for each file and count issues for file in &files { let mut cmd = Command::new("clang-tidy"); if do_fix { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 08cfe4b..81a0eb6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -15,6 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// DCR CLI module. +/// +/// This module declares submodules for all DCR commands. pub mod add; pub mod build; pub mod clean; diff --git a/src/cli/new.rs b/src/cli/new.rs index d984bf5..3592a39 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -25,6 +25,10 @@ use std::fs; use std::io::Write; use toml::Value; +/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure. +/// +/// Handles command-line argument parsing, directory creation, default configuration generation (`dcr.toml`), +/// template source file generation (`src/main.c`), and initial VCS setup. pub fn new(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -38,6 +42,7 @@ pub fn new(args: &[String]) -> i32 { let items = check_dir(None).unwrap_or_default(); + // Parse CLI options (--vcs) while extracting positional arguments for the project name. let mut vcs_str = None; let mut clean_args = Vec::new(); let mut iter = args.iter(); @@ -103,6 +108,7 @@ pub fn new(args: &[String]) -> i32 { project_name ); + // Initialize and write project configuration manifest (dcr.toml). let toml_path = format!("./{project_name}/dcr.toml"); let mut config = match Config::new(&toml_path) { Ok(cfg) => cfg, @@ -124,6 +130,7 @@ pub fn new(args: &[String]) -> i32 { colored("dcr.toml", BOLD_CYAN) ); + // Generate src directory and populate initial main.c file. if fs::create_dir_all(format!("./{project_name}/src")).is_err() { error("Failed to create src/"); return 1; @@ -146,7 +153,7 @@ pub fn new(args: &[String]) -> i32 { colored("src/main.c", BOLD_CYAN) ); - // settings VCS + // Resolve target VCS mode based on CLI flags or parent directory state. let mut vcs_kind = VcsKind::Git; if let Some(ref vcs_val) = vcs_str { match VcsKind::parse(vcs_val) { diff --git a/src/cli/run.rs b/src/cli/run.rs index 4a460fb..fd11bf6 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -27,6 +27,8 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use std::path::Path; use std::process::Command; +/// Retrieves the run command from the config, preferring target-specific, +/// then profile-specific, then the base `run.cmd`. fn get_run_cmd( config: &Config, profile: &str, @@ -55,6 +57,14 @@ fn get_run_cmd( } } +/// Handles the `dcr run` subcommand: parses flags, finds the project root, +/// builds if needed, and runs the resulting binary or configured command. +/// +/// # Parameters +/// - `args`: Tokens after `dcr run` (build flags; after `--` → binary args). +/// +/// # Returns +/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure. pub fn run(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -177,6 +187,7 @@ pub fn run(args: &[String]) -> i32 { } } +/// Builds and runs a single project at `root` (optionally as a workspace member). fn run_project( root: &Path, flags: &crate::cli::flags::BuildRunFlags, @@ -290,6 +301,7 @@ fn run_project( Err("Fix errors in the code to run the project".to_string()) } +/// Builds the list of arguments for the build command based on the provided flags. fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec { let mut args = Vec::new(); args.push(format!("--{}", flags.profile)); @@ -313,6 +325,7 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec { args } +/// Constructs build arguments specifically for a workspace member. fn args_for_workspace_build( flags: &crate::cli::flags::BuildRunFlags, member_name: &str, @@ -337,6 +350,7 @@ fn args_for_workspace_build( args } +/// Executes the given shell command and returns its exit code. fn run_shell(cmd: &str) -> i32 { let status = if cfg!(target_os = "windows") { Command::new("cmd").arg("/C").arg(cmd).status() @@ -350,6 +364,7 @@ fn run_shell(cmd: &str) -> i32 { } } +/// Runs the command, appending escaped binary arguments if provided. fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 { if bin_args.is_empty() { return run_shell(cmd); @@ -362,6 +377,7 @@ fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 { run_shell(&full) } +/// Formats the run command string for console output, appending arguments if any. fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String { if bin_args.is_empty() { return cmd.to_string(); @@ -374,6 +390,7 @@ fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String { display } +/// Formats the binary path for console display, appending arguments if any. fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String { if bin_args.is_empty() { return bin_path.to_string(); @@ -386,6 +403,7 @@ fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String { display } +/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix. fn shell_escape(arg: &str) -> String { if cfg!(target_os = "windows") { // cmd.exe: wrap in double quotes and escape embedded quotes. @@ -406,6 +424,7 @@ fn shell_escape(arg: &str) -> String { } } +/// Replaces version info and profile in the command template. fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String { let info = parse_version_info(version); substitute_vars(cmd, &info, profile, "") diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 5adcc4a..c4b7842 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -17,6 +17,7 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; +/// Sets up DCR registries by loading the registry manager and printing the list of registries. pub fn setup(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -29,6 +30,7 @@ pub fn setup(args: &[String]) -> i32 { } println!("Setting up DCR registries..."); + // Load and process the registries, returning 0 on success or 1 on error match crate::core::registry::RegistryManager::load() { Ok(manager) => { println!("Loaded {} registries.", manager.config.registry.len()); diff --git a/src/cli/test.rs b/src/cli/test.rs index 412921b..f536d1f 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -31,6 +31,7 @@ use std::process::Command; const BOLD_BLUE: &str = "\x1b[1m\x1b[94m"; +/// Runs project tests and prints a unified testsuite report. pub fn test(args: &[String]) -> i32 { let mut init_header = false; let mut profile = "debug"; @@ -101,6 +102,7 @@ pub fn test(args: &[String]) -> i32 { } } +/// Executes the testsuite for the given build profile. fn run_testsuite(profile: &str) -> Result { if build::build(&[format!("--{profile}")]) != 0 { return Ok(1); @@ -260,6 +262,7 @@ fn run_testsuite(profile: &str) -> Result { Ok(0) } +/// Collects all .c source files from the tests/ directory. fn collect_test_sources() -> Result, String> { let tests_dir = Path::new("./tests"); if !tests_dir.is_dir() { @@ -278,6 +281,7 @@ fn collect_test_sources() -> Result, String> { Ok(sources) } +/// Returns a list of include directories for test compilation. fn test_include_dirs(config: &Config, profile: &str) -> Vec { let mut dirs = vec![ "./tests".to_string(), @@ -288,6 +292,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec { dirs } +/// Compiles a single test source file to an object file. fn compile_test_source( compiler: &str, standard: &str, @@ -317,6 +322,7 @@ fn compile_test_source( Ok(()) } +/// Links the test object file into an executable binary. fn link_test_binary( compiler: &str, ldflags: &[String], @@ -343,6 +349,7 @@ fn link_test_binary( Ok(()) } +/// Prints the stdout and stderr from a command execution. fn print_command_output(output: &std::process::Output) { if !output.stdout.is_empty() { print!("{}", String::from_utf8_lossy(&output.stdout)); @@ -352,6 +359,7 @@ fn print_command_output(output: &std::process::Output) { } } +/// Creates the default test files (dcr_test.h and test.c) in the tests/ directory. fn ensure_test_header() -> Result<(), String> { fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?; fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H) @@ -363,6 +371,7 @@ fn ensure_test_header() -> Result<(), String> { Ok(()) } +/// Extracts test case names from a C source file by parsing TEST_CASE macros. fn extract_test_names_path(path: &Path) -> Vec { let Ok(content) = fs::read_to_string(path) else { return Vec::new(); @@ -379,6 +388,7 @@ fn extract_test_names_path(path: &Path) -> Vec { names } +/// Prints a test result field with color coding. fn print_field(label: &str, value: i32, color: &str) { if value == 0 { println!("{}: {}", label, value); diff --git a/src/cli/tree.rs b/src/cli/tree.rs index eaff5b3..4c24517 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -22,6 +22,10 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; use toml::Value; +/// Displays the dependency tree of the current project. +/// +/// This function checks for the --help flag and prints usage information if present. +/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree. pub fn tree(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); @@ -62,6 +66,9 @@ pub fn tree(args: &[String]) -> i32 { 0 } +/// Recursively prints the dependency tree starting from the given dependencies table. +/// +/// Sorts dependencies for consistent ordering and handles path resolution for sub-dependencies. fn print_deps( deps: &toml::value::Table, base_path: &Path, @@ -81,6 +88,7 @@ fn print_deps( let mut sub_deps = None; let mut resolved_path = None; + // Resolve dependency path if it's a path-based dependency if let Some(path) = &dep_path { let dcr_toml = path.join("dcr.toml"); if dcr_toml.exists() @@ -117,6 +125,7 @@ fn print_deps( println!("{}{}{}{}", prefix, connector, name, version); + // Recurse only if sub-dependencies exist and this dep hasn't been seen if let Some(s_deps) = sub_deps && !seen.contains(*name) { @@ -130,6 +139,7 @@ fn print_deps( } } +/// Resolves the filesystem path for a dependency entry if it specifies a "path:" prefix or table with "path" key. fn resolve_dep_path(value: &Value, base_path: &Path) -> Option { match value { Value::String(s) => s diff --git a/src/config.rs b/src/config.rs index c98cb03..b107964 100644 --- a/src/config.rs +++ b/src/config.rs @@ -96,6 +96,7 @@ int main(int argc, char **argv) { } "#; +/// Returns build flags for the given profile as a static slice, or None if empty. pub fn flags(profile: &str) -> Option<&'static [&'static str]> { let f = crate::utils::build::default_profile_flags(profile); if f.is_empty() { None } else { Some(f) } diff --git a/src/core/build/archive.rs b/src/core/build/archive.rs index 9e047ea..8907a2c 100644 --- a/src/core/build/archive.rs +++ b/src/core/build/archive.rs @@ -22,6 +22,7 @@ use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::path::Path; +/// File wrapper that shifts all seek offsets (used to skip the boot sector on a FAT image). struct OffsetFile { inner: T, offset: u64, @@ -73,6 +74,7 @@ impl Seek for OffsetFile { } } +/// Parses a size string like `1.5m` or `500k` into bytes (1024-based; empty → 0). fn parse_size(s: &str) -> Result { let s = s.trim(); if s.is_empty() { @@ -101,6 +103,7 @@ fn parse_size(s: &str) -> Result { Ok((val * multiplier) as u64) } +/// Creates a FAT image from layout config, substituting `{profile}` and optional boot sector. pub fn pack_archive( project_root: &Path, config: &ArchiveConfig, @@ -142,6 +145,7 @@ pub fn pack_archive( if offset == 0 && let Some(boot_rel) = &config.bootsector { + // Load and write bootsector if present let boot_rel_substituted = boot_rel.replace("{profile}", profile); let boot_path = project_root.join(&boot_rel_substituted); if boot_path.is_file() { diff --git a/src/core/build/builder/artifact.rs b/src/core/build/builder/artifact.rs index 9cf1d30..afedf3a 100644 --- a/src/core/build/builder/artifact.rs +++ b/src/core/build/builder/artifact.rs @@ -23,6 +23,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Instant; +/// Returns the artifact directory path based on the build context. pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf { match ctx.target_dir { Some(dir) => Path::new(dir).to_path_buf(), @@ -30,6 +31,17 @@ pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf { } } +/// Resolves the artifact path for a given kind/profile/name. +/// +/// # Parameters +/// - `kind`: Artifact kind (`bin`, `staticlib`, `sharedlib`, `flat-bin`, `efi`, `elf`, …). +/// - `profile`: Build profile (`debug` / `release` / custom). +/// - `project_name`: Default basename when `output_filename` is unset. +/// - `target_dir`: Optional artifact directory override. +/// - `output_filename` / `output_extension`: Optional name/ext overrides. +/// +/// # Returns +/// Relative/absolute path string, or `None` for `none` / `custom` kinds. pub fn resolve_artifact_path( kind: &str, profile: &str, @@ -75,6 +87,7 @@ pub fn resolve_artifact_path( Some(path) } +/// Converts a relative or absolute path to an absolute path by joining with project root if needed. pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf { let p = Path::new(relative_or_abs); let joined = if p.is_absolute() { @@ -88,6 +101,7 @@ pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> Pat crate::utils::fs::canonicalize_path(&joined) } +/// Returns the output path for a flat binary artifact. pub(crate) fn flat_output_path(ctx: &BuildContext) -> String { let name = ctx.output_filename.unwrap_or(ctx.project_name); let ext = ctx.output_extension.unwrap_or("bin"); @@ -99,6 +113,7 @@ pub(crate) fn flat_output_path(ctx: &BuildContext) -> String { artifact_dir(ctx).join(file).to_string_lossy().to_string() } +/// Returns the output path for a flat binary artifact from a source file. pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String { let stem = Path::new(source) .file_stem() @@ -113,6 +128,7 @@ pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> Strin artifact_dir(ctx).join(file).to_string_lossy().to_string() } +// Helper to find a suitable objcopy tool in PATH fn resolve_objcopy() -> Result { for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] { if Command::new(candidate) @@ -131,6 +147,7 @@ fn resolve_objcopy() -> Result { ) } +/// Converts an object/ELF to a raw binary via `objcopy -O binary` (skips if up to date). pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> { if let Some(parent) = Path::new(output).parent() { std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?; @@ -147,6 +164,7 @@ pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> R common::run_command_sync_output(&mut cmd) } +/// Links objects into an intermediate ELF, then converts it to a flat binary via objcopy. pub(crate) fn link_flat_binary( ctx: &BuildContext, objects: &[String], @@ -208,6 +226,7 @@ pub(crate) fn link_flat_binary( Ok(common::elapsed_secs(start_time)) } +/// Archives objects into a static library. pub(crate) fn archive_static( ctx: &BuildContext, objects: &[String], @@ -234,6 +253,7 @@ pub(crate) fn archive_static( run(ctx, cmd, start_time) } +/// Links objects into a binary or library based on artifact kind. pub(crate) fn link_binary( ctx: &BuildContext, objects: &[String], @@ -299,6 +319,7 @@ pub(crate) fn link_binary( run(ctx, cmd, start_time) } +/// Returns the default archiver name based on compiler and platform. fn default_archiver(compiler: &str) -> &'static str { let c = compiler.to_lowercase(); if cfg!(target_os = "windows") { @@ -314,11 +335,13 @@ fn default_archiver(compiler: &str) -> &'static str { } } +/// Checks if the archiver is an MSVC lib tool. fn is_msvc_lib(archiver: &str) -> bool { let a = archiver.to_lowercase(); a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe") } +/// Runs a command and handles success/failure with timing. fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { eprintln!("[dcr] {:?}", cmd); @@ -345,12 +368,14 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result Result { let compiler = if ctx.compiler.is_empty() { "cc" } else { ctx.compiler }; + // Reject unsupported flat-bin with Qt if is_flat_bin(ctx.kind) && ctx.qt { return Err("flat-bin is not supported with build.qt = true".to_string()); } @@ -69,6 +73,9 @@ pub fn build(ctx: &BuildContext) -> Result { artifact::link_binary(ctx, &objects, compiler, start_time) } +/// Collects source files from the provided source roots for the build context. +/// +/// Delegates to common source collection after resolving language-specific extensions. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { let extensions = source_extensions(ctx.language); common::collect_sources( @@ -79,14 +86,19 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Source file extensions for `language` (owned `Vec` of static slices). fn source_extensions(language: &str) -> Vec<&'static str> { crate::core::build::common::source_extensions(language) } +/// Retrieves default compiler flags for the specified build profile. fn default_flags(profile: &str) -> &'static [&'static str] { crate::utils::build::default_profile_flags(profile) } +/// Compiles all provided sources into object files in parallel. +/// +/// Prepares object file paths and spawns parallel tasks for each source compilation. fn build_objects( compiler: &str, sources: &[String], @@ -100,6 +112,7 @@ fn build_objects( .map(|s| common::object_path(obj_dir, s, obj_ext)) .collect(); + // Compile objects in parallel common::parallel_build( sources.len(), |i| { @@ -117,6 +130,9 @@ fn build_objects( Ok(objects) } +/// Compiles a single source file to an object file. +/// +/// Creates object directory if needed, checks for rebuild necessity, constructs and runs the compiler command with appropriate flags. fn build_object( compiler: &str, source: &str, @@ -128,6 +144,7 @@ fn build_object( fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } + // Skip if object is already up to date if !common::needs_rebuild(source, obj_path) { return Ok(()); } @@ -183,6 +200,8 @@ fn build_object( let use_dcr_defaults = ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind)); + + // Use default flags only if no custom flags and not special target if use_dcr_defaults { for flag in default_flags(ctx.profile) { cmd.arg(flag); @@ -211,6 +230,7 @@ fn build_object( common::run_command_sync_output(&mut cmd) } +/// Returns the assembly language flag for the compiler based on source extension. fn asm_lang_flag(source: &str) -> Option<&'static str> { crate::core::build::common::asm_lang_flag(source) } diff --git a/src/core/build/builder/mod.rs b/src/core/build/builder/mod.rs index e13fbbe..1ba5a33 100644 --- a/src/core/build/builder/mod.rs +++ b/src/core/build/builder/mod.rs @@ -22,6 +22,7 @@ pub mod msvc; use crate::core::build::language::asm::{fasm, gas, masm, nasm}; use crate::core::build::language::llvm_ir; +/// Configuration context for the build process, including compiler, target, and output settings. pub struct BuildContext<'a> { pub profile: &'a str, pub project_name: &'a str, @@ -56,6 +57,7 @@ pub struct BuildContext<'a> { pub qt: bool, } +/// Trait defining the build interface for different compilers and languages. pub trait Builder { #[allow(dead_code)] fn id(&self) -> &'static str; @@ -63,12 +65,19 @@ pub trait Builder { fn collect_sources(&self, ctx: &BuildContext) -> Result, String>; } +/// Concrete builder for C/C++ languages using gcc/clang. struct Cc; +/// Concrete builder for MSVC and clang-cl. struct Msvc; +/// Concrete builder for NASM assembly. struct Nasm; +/// Concrete builder for GNU Assembler (gas). struct Gas; +/// Concrete builder for MASM assembler. struct Masm; +/// Concrete builder for FASM assembler. struct Fasm; +/// Concrete builder for LLVM IR. struct Llc; impl Builder for Cc { @@ -156,6 +165,7 @@ impl Builder for Llc { } pub fn builder_for(compiler: &str) -> &'static dyn Builder { + // Normalize compiler name to lowercase for matching. let c = compiler.to_lowercase(); if c.contains("clang-cl") { return &Msvc; @@ -181,6 +191,7 @@ pub fn builder_for(compiler: &str) -> &'static dyn Builder { &Cc } +/// Main build entrypoint that validates the compiler and delegates to the selected builder. pub fn build(ctx: &BuildContext) -> Result { if !check_compiler_exists(ctx.compiler) { return Err(format!( @@ -188,9 +199,11 @@ pub fn build(ctx: &BuildContext) -> Result { ctx.compiler )); } + // Delegate to the concrete builder's build method. builder_for(ctx.compiler).build(ctx) } +/// True if `compiler --version` can be spawned (exit status is ignored). fn check_compiler_exists(compiler: &str) -> bool { let name = if compiler.is_empty() { "cc" @@ -209,10 +222,12 @@ fn check_compiler_exists(compiler: &str) -> bool { .is_ok() } +/// Collects source code files by delegating to the builder selected for the compiler. pub fn collect_sources(ctx: &BuildContext) -> Result, String> { builder_for(ctx.compiler).collect_sources(ctx) } +/// Unit test verifying that builder_for correctly identifies the builder for various compiler names. #[cfg(test)] mod tests { use super::*; diff --git a/src/core/build/builder/msvc/mod.rs b/src/core/build/builder/msvc/mod.rs index d6f8cb9..b0d6dc4 100644 --- a/src/core/build/builder/msvc/mod.rs +++ b/src/core/build/builder/msvc/mod.rs @@ -15,6 +15,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// MSVC build backend for DCR. +/// +/// This module provides the MSVC-specific build implementation for C and C++ +/// projects, including object compilation and linking for executables, static +/// libraries, shared libraries, and flat binaries. use crate::core::build::builder::BuildContext; use crate::core::build::builder::artifact; use crate::core::build::common; @@ -25,6 +30,7 @@ use std::path::Path; use std::process::Command; use std::time::Instant; +/// Builds the project target using the MSVC compiler. pub fn build(ctx: &BuildContext) -> Result { let compiler = if ctx.compiler.is_empty() { "cl" @@ -159,6 +165,7 @@ pub fn build(ctx: &BuildContext) -> Result { } } +/// Links the flat binary output for MSVC build targets. fn link_flat_msvc( ctx: &BuildContext, compiler: &str, @@ -214,6 +221,7 @@ fn link_flat_msvc( Ok(common::elapsed_secs(start_time)) } +/// Collects all source files for the build based on language extensions. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { let extensions = source_extensions(ctx.language); common::collect_sources( @@ -224,10 +232,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Source file extensions for `language` (owned `Vec` of static slices). fn source_extensions(language: &str) -> Vec<&'static str> { crate::core::build::common::source_extensions(language) } +/// Maps the language and standard version to the corresponding MSVC flag string. fn msvc_standard_flag(language: &str, standard: &str) -> Result { let lang = language.to_lowercase(); let std = standard.to_lowercase(); @@ -251,6 +261,7 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result Err("Unsupported language".to_string()) } +/// Returns the MSVC architecture flag based on the target platform. fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> { let raw = platform?.trim(); if raw.is_empty() { @@ -268,6 +279,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> { } } +/// Returns default compiler flags for the specified build profile. fn default_flags(profile: &str) -> &'static [&'static str] { match profile { "release" => &["/O2", "/DNDEBUG"], @@ -276,6 +288,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] { } } +/// Compiles all sources into object files using parallel processing. fn build_objects( compiler: &str, sources: &[String], @@ -297,6 +310,7 @@ fn build_objects( Ok(objects) } +/// Compiles a single source file to an object file. fn build_object( compiler: &str, source: &str, @@ -369,6 +383,7 @@ fn build_object( let mut headers = Vec::new(); let mut clean_stdout = String::new(); + // Parse compiler output to extract include file notes for dependency generation for line in stdout.lines() { if let Some(stripped) = line.strip_prefix("Note: including file:") { headers.push(stripped.trim().to_string()); diff --git a/src/core/build/cache.rs b/src/core/build/cache.rs index 2d78473..d1184db 100644 --- a/src/core/build/cache.rs +++ b/src/core/build/cache.rs @@ -21,6 +21,17 @@ use sha2::{Digest, Sha256}; use std::fs; use std::path::{Path, PathBuf}; +/// Computes a build fingerprint by hashing context fields and file metadata (SHA-256). +/// +/// Includes `dcr.toml` (required) and `dcr.lock` when present, plus path/size/mtime +/// for each source, header, and library file. +/// +/// # Parameters +/// - `ctx`: Build context (profile, tools, flags, dirs, …). +/// - `sources` / `headers` / `lib_files`: Inputs whose metadata enters the hash. +/// +/// # Returns +/// Hex-encoded fingerprint, or an error if `dcr.toml` or a listed file cannot be read. pub(crate) fn compute_build_fingerprint( ctx: &BuildContext, sources: &[String], @@ -61,12 +72,14 @@ pub(crate) fn compute_build_fingerprint( for value in ctx.ldflags { hasher.update(value.as_bytes()); } + // Include project metadata from dcr.toml in the fingerprint let toml = fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?; hasher.update(toml.as_bytes()); if let Ok(lock) = fs::read_to_string("dcr.lock") { hasher.update(lock.as_bytes()); } + // Include source, header, and library files in the fingerprint for source in sources { let path = Path::new(source); update_hasher_with_file(&mut hasher, path)?; @@ -80,6 +93,14 @@ pub(crate) fn compute_build_fingerprint( Ok(to_hex(&hasher.finalize())) } +/// Whether the build can be skipped: main artifact exists and cache hash matches. +/// +/// # Parameters +/// - `ctx`: Used to locate the output artifact and `.dcr-build.hash`. +/// - `fingerprint`: Fresh hash from [`compute_build_fingerprint`]. +/// +/// # Returns +/// `true` only when both the artifact and matching cache file are present. pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool { let output = build_output_path(ctx); if !Path::new(&output).is_file() { @@ -90,6 +111,7 @@ pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool { cached.trim() == fingerprint } +/// Persists the build fingerprint to the cache file on disk. pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> { let cache_path = build_cache_path(ctx.profile, ctx.target_dir); if let Some(parent) = cache_path.parent() { @@ -99,6 +121,7 @@ pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> .map_err(|err| format!("Failed to write cache: {err}")) } +/// Returns the path to the build cache hash file based on profile and target directory. fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf { match target_dir { Some(dir) => Path::new(dir).join(".dcr-build.hash"), @@ -106,6 +129,7 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf { } } +/// Resolves the output path for the build artifact using the provided context. pub fn build_output_path(ctx: &BuildContext) -> String { crate::core::build::builder::artifact::resolve_artifact_path( ctx.kind, @@ -124,6 +148,7 @@ pub fn build_output_path(ctx: &BuildContext) -> String { }) } +/// Collects header files from source roots and include directories, filtering out excluded paths. pub(crate) fn collect_header_files( ctx: &BuildContext, project_root: &Path, @@ -165,6 +190,7 @@ pub(crate) fn collect_header_files( Ok(out) } +/// Recursively scans a directory to collect header files, respecting exclusion rules. fn collect_header_files_rec( dir: &Path, out: &mut Vec, @@ -197,11 +223,13 @@ fn collect_header_files_rec( Ok(()) } +/// Checks if the given path has a header file extension. fn is_header_file(path: &Path) -> bool { let ext = path.extension().and_then(|v| v.to_str()).unwrap_or(""); matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc") } +/// Collects library files from lib directories using platform-specific candidates. pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec { let mut out = Vec::new(); for dir in ctx.lib_dirs { @@ -218,6 +246,7 @@ pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec { out } +/// Candidate library filenames for the **host** OS (`cfg!(target_os = …)`). fn lib_candidates(name: &str) -> Vec { if cfg!(target_os = "windows") { return vec![format!("{name}.lib")]; @@ -236,6 +265,7 @@ fn lib_candidates(name: &str) -> Vec { ] } +/// Updates the hasher with the file's path, size, and last modification time. fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> { hasher.update(path.to_string_lossy().as_bytes()); let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?; @@ -248,6 +278,7 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin Ok(()) } +/// Converts a byte slice containing the SHA256 hash to a hexadecimal string. fn to_hex(bytes: &[u8]) -> String { crate::utils::fs::to_hex(bytes) } diff --git a/src/core/build/common.rs b/src/core/build/common.rs index c560b4d..38e5598 100644 --- a/src/core/build/common.rs +++ b/src/core/build/common.rs @@ -27,18 +27,25 @@ static OUTPUT_MUTEX: OnceLock> = OnceLock::new(); static PROGRESS_LABEL: OnceLock>> = OnceLock::new(); static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false); +/// Returns a static mutex used to synchronize access to stderr during progress reporting. +/// This ensures thread-safe output when multiple build threads write to the terminal. pub fn get_output_lock() -> &'static Mutex<()> { OUTPUT_MUTEX.get_or_init(|| Mutex::new(())) } +/// Initializes the shared progress label storage. fn progress_label_lock() -> &'static Mutex> { PROGRESS_LABEL.get_or_init(|| Mutex::new(None)) } +/// Sets the label text that will be shown next to the compile progress counter. +/// The label is cleared after the progress line is finished. pub fn set_progress_label(label: Option) { *progress_label_lock().lock().unwrap() = label; } +/// Finishes the current progress line by clearing the label and flushing the line if it was dirty. +/// This is called when a build step completes normally. pub fn finish_progress_line() { let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst); *progress_label_lock().lock().unwrap() = None; @@ -52,6 +59,8 @@ pub fn finish_progress_line() { } } +/// Interrupts the current progress line (if any) and flushes it to stderr. +/// Used when a build step produces output that should interrupt the progress display. pub fn interrupt_progress_for_output() { if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) { return; @@ -63,6 +72,9 @@ pub fn interrupt_progress_for_output() { } } +/// Reports the current compile progress to stderr in the format "label [done/total]". +/// Only prints if the terminal is attached and the label is set. +/// Updates the dirty flag so the line can be overwritten on the next call. fn report_compile_progress(done: usize, total: usize) { let label = progress_label_lock().lock().unwrap().clone(); let Some(label) = label else { @@ -82,6 +94,9 @@ fn report_compile_progress(done: usize, total: usize) { PROGRESS_DIRTY.store(true, Ordering::SeqCst); } +/// Recursively collects all source files under the given roots that match the provided extensions. +/// Directories listed in exclude_dirs are skipped unless overridden by include_paths. +/// Returns a sorted list of normalized source paths or an error string. pub fn collect_sources( roots: &[PathBuf], extensions: &[&str], @@ -104,6 +119,8 @@ pub fn collect_sources( Ok(sources) } +/// Recursively walks a directory tree to collect source files. +/// Skips excluded directories unless they are explicitly allowed by include_paths. fn collect_sources_rec( dir: &Path, extensions: &[&str], @@ -151,6 +168,8 @@ fn collect_sources_rec( Ok(()) } +/// Determines whether a path should be excluded from source collection. +/// Returns false if the path is explicitly allowed by include_paths (even if it matches an exclude). pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool { if matches_patterns(path, include_paths, true) { return false; @@ -161,6 +180,8 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin exclude_dirs.iter().any(|dir| path_is_under(path, dir)) } +/// Checks if a path is under a given prefix, handling both absolute and relative paths +/// and normalizing Windows-style separators for cross-platform comparison. fn path_is_under(path: &Path, prefix: &Path) -> bool { if path.starts_with(prefix) { return true; @@ -180,6 +201,9 @@ fn path_is_under(path: &Path, prefix: &Path) -> bool { } } +/// Matches a path against include/exclude glob and path patterns. +/// Positive patterns allow files; negative patterns (starting with !) exclude them. +/// Returns true if the path matches any rule. fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool { if patterns.is_empty() { return false; @@ -244,10 +268,20 @@ fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool { false } +/// Returns true if the string contains any glob magic characters (*, ?, or [). pub fn has_glob_magic(value: &str) -> bool { value.chars().any(|c| matches!(c, '*' | '?' | '[')) } +/// Returns a GCC/Clang `-x` language for assembly sources. +/// +/// # Parameters +/// - `source`: Source file path (extension decides the dialect). +/// +/// # Returns +/// - `Some("assembler-with-cpp")` for `.S` +/// - `Some("assembler")` for `.s` / `.asm` +/// - `None` for other extensions pub fn asm_lang_flag(source: &str) -> Option<&'static str> { let ext = Path::new(source).extension().and_then(|v| v.to_str())?; match ext { @@ -257,6 +291,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> { } } +/// Returns the list of file extensions associated with the given language token(s). +/// Splits comma-separated tokens and falls back to .c if no language is recognized. pub fn source_extensions(language: &str) -> Vec<&'static str> { let mut out = Vec::new(); for part in language.split(',').map(|s| s.trim()) { @@ -270,10 +306,13 @@ pub fn source_extensions(language: &str) -> Vec<&'static str> { out } +/// Returns elapsed time in seconds with two decimal places for display purposes. pub fn elapsed_secs(start: Instant) -> f64 { ((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0 } +/// Spawns a fixed number of worker threads to parallelize a build task. +/// Reports progress after each task completes. pub fn parallel_build(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String> where F: Fn(usize) -> Result<(), String> + Sync + Send, @@ -330,6 +369,8 @@ where Ok(()) } +/// Runs a command, prints stdout/stderr to the terminal (interrupting progress), +/// and returns `Ok(())` or `Err` based on exit status. pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> { let program = cmd.get_program().to_string_lossy().into_owned(); let output = cmd.output().map_err(|err| { @@ -357,6 +398,8 @@ pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> { } } +/// Normalizes a source path to be relative to the current working directory when possible. +/// Returns the original path unchanged if it cannot be made relative. pub fn normalize_source_path(path: &Path) -> String { if !path.is_absolute() { return path.to_string_lossy().to_string(); @@ -369,6 +412,8 @@ pub fn normalize_source_path(path: &Path) -> String { path.to_string_lossy().to_string() } +/// Computes the output object file path by stripping the leading ./ and adjusting the extension. +/// Places the object in the specified obj_dir while preserving directory structure. pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String { let src_path = Path::new(source); let stripped = src_path.strip_prefix("./").unwrap_or(src_path); @@ -401,6 +446,16 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String { out.to_string_lossy().replace('\\', "/") } +/// Checks whether a source file needs to be rebuilt. +/// +/// Compares mtimes of the source, object, and deps listed in the sibling `.d` file. +/// +/// # Parameters +/// - `source`: Source path. +/// - `object`: Object path (`.d` is derived by changing the extension). +/// +/// # Returns +/// `true` if the object is missing, older than source/deps, or a dep is missing. pub fn needs_rebuild(source: &str, object: &str) -> bool { let src_time = fs::metadata(source).and_then(|m| m.modified()); let obj_time = fs::metadata(object).and_then(|m| m.modified()); @@ -436,6 +491,14 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool { false } +/// Checks whether the output executable needs to be relinked. +/// +/// # Parameters +/// - `objects`: Object file paths that feed the link step. +/// - `output_path`: Linked artifact path. +/// +/// # Returns +/// `true` if the output is missing or any object is newer than it. pub fn needs_link(objects: &[String], output_path: &str) -> bool { let out_meta = match fs::metadata(output_path) { Ok(m) => m, @@ -461,6 +524,8 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool { false } +/// Parses a dependency file (.d) produced by GCC or MSVC compilers. +/// Extracts all dependency paths, handling escaped newlines and Windows-style paths. fn parse_d_file(content: &str) -> Vec { let mut deps = Vec::new(); let mut target_end = 0; diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs index 807ebbd..8389362 100644 --- a/src/core/build/engine.rs +++ b/src/core/build/engine.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Core build engine module for DCR. +/// This module provides the main build logic for projects and workspaces. use crate::core::build::builder::{BuildContext, build as build_project, collect_sources}; use crate::core::build::cache::{ collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build, @@ -49,6 +51,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Instant; +/// Ensures that target directories exist for the build output. fn ensure_target_dirs( project_root: &Path, items: &[String], @@ -68,6 +71,7 @@ fn ensure_target_dirs( } } +/// Runs the project build and returns the elapsed time in seconds or an error. fn run_compile( ctx: &BuildContext, _cancel: &Arc, @@ -89,8 +93,16 @@ fn run_compile( } } -// Library entry point: run a build described by `req`, streaming progress to -// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own. +/// Library entry point: run a build described by `req`, streaming progress to +/// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own. +/// +/// # Parameters +/// - `root`: Project or workspace root containing `dcr.toml`. +/// - `req`: Profile, target, force/verbose, optional member, cancel flag. +/// - `rep`: Progress sink for build events. +/// +/// # Returns +/// `BuildOutcome` with elapsed seconds, or `BuildError` on failure/cancel. pub fn run_build( root: &Path, req: &BuildRequest, @@ -110,6 +122,7 @@ pub fn run_build( Ok(BuildOutcome { secs }) } +/// Orchestrates the build for the specified profile and target, handling workspaces and projects. #[allow(unused_variables, clippy::too_many_arguments)] fn build_all( root: &Path, @@ -279,6 +292,7 @@ fn build_all( Ok(elapsed) } +/// Builds each workspace member in order (flat loop, not recursive into nested workspaces). #[allow(clippy::too_many_arguments)] fn build_workspace( workspace: &crate::core::workspace::Workspace, @@ -308,6 +322,7 @@ fn build_workspace( Ok(()) } +/// Builds a single project at the given root, handling configuration and steps. #[allow(clippy::too_many_arguments)] fn build_project_at( project_root: &Path, @@ -904,6 +919,7 @@ fn build_project_at( Ok(()) } +/// Converts relative paths in flags to absolute paths based on project root for include and link flags. fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String { for prefix in prefixes { if let Some(rest) = flag.strip_prefix(prefix) { @@ -924,6 +940,7 @@ fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> S flag.to_string() } +/// Finds the nearest ancestor directory named "target" starting from the given dir. fn find_target_root(dir: &str, fallback: &Path) -> PathBuf { let p = Path::new(dir); for ancestor in p.ancestors() { @@ -934,6 +951,7 @@ fn find_target_root(dir: &str, fallback: &Path) -> PathBuf { fallback.to_path_buf() } +/// Packages the built library by copying headers and libs to the target include and lib directories. fn package_library( ctx: &BuildContext, headers: &[PathBuf], @@ -988,12 +1006,14 @@ fn package_library( Ok(()) } +/// Holds the resolved executable paths for Qt tools. pub(crate) struct ToolchainExecs { pub(crate) uic: String, pub(crate) moc: String, pub(crate) rcc: String, } +/// Resolves the paths for uic, moc, and rcc tools, preferring configured values or pkg-config/Qt6 variants. fn resolve_toolchain_execs( uic: &Option, moc: &Option, @@ -1008,6 +1028,7 @@ fn resolve_toolchain_execs( } } +/// Resolves a Qt tool path, falling back to Qt6 variants if not found in standard locations. fn resolve_qt_tool(configured: &Option, qt_bins: Option<&Path>, tool: &str) -> String { if let Some(value) = configured { return value.clone(); @@ -1030,6 +1051,7 @@ fn resolve_qt_tool(configured: &Option, qt_bins: Option<&Path>, tool: &s tool.to_string() } +/// Queries pkg-config for Qt6 host bin directories for the given packages. fn resolve_qt_host_bins(pkgs: &[String]) -> Option { let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect(); if qt_pkgs.is_empty() { @@ -1065,6 +1087,7 @@ fn resolve_qt_host_bins(pkgs: &[String]) -> Option { None } +/// Executes pkg-config to get a variable value for a package and returns the path if valid. fn query_pkg_config_var(pkg: &str, var: &str) -> Option { let output = std::process::Command::new("pkg-config") .arg(format!("--variable={var}")) @@ -1082,6 +1105,7 @@ fn query_pkg_config_var(pkg: &str, var: &str) -> Option { if path.is_dir() { Some(path) } else { None } } +/// Extracts tool binaries from a libexec directory or its bin subdirectory. fn qt_bins_from_libexec(libexec: &Path) -> Option { for tool in ["moc", "uic", "rcc"] { if libexec.join(tool).is_file() { @@ -1095,12 +1119,14 @@ fn qt_bins_from_libexec(libexec: &Path) -> Option { if bin.is_dir() { Some(bin) } else { None } } +/// Detects if a Qt6 tool variant (like uic6 or uic-qt6) is available on the PATH. fn detect_qt6_tool_variant(tool: &str) -> Option { [format!("{tool}6"), format!("{tool}-qt6")] .into_iter() .find(|candidate| is_on_path(candidate)) } +/// Checks if a command is available either as a file or in the PATH environment variable. fn is_on_path(cmd: &str) -> bool { if Path::new(cmd).is_file() { return true; diff --git a/src/core/build/language/asm/common.rs b/src/core/build/language/asm/common.rs index 9b1d20b..1a1cb97 100644 --- a/src/core/build/language/asm/common.rs +++ b/src/core/build/language/asm/common.rs @@ -5,6 +5,13 @@ use crate::utils::build::{is_compile_only, is_flat_bin}; use std::path::Path; use std::time::Instant; +/// Keeps only assembler-relevant flags from a cflags list. +/// +/// # Parameters +/// - `flags`: Full compiler flag list (often shared with C/C++). +/// +/// # Returns +/// Cloned flags that start with `-I`/`-i` or equal `-g`. pub fn filter_asm_flags(flags: &[String]) -> Vec { flags .iter() @@ -16,6 +23,19 @@ pub fn filter_asm_flags(flags: &[String]) -> Vec { .collect() } +/// Builds assembly sources with direct flat emission when `kind` is `flat-bin`. +/// +/// For non-flat kinds, assembles to objects and then links/archives as usual. +/// +/// # Parameters +/// - `ctx`: Build context (kind, sources, flags, …). +/// - `backend_name`: Label used in error messages (e.g. `"NASM"`). +/// - `default_assembler`: Assembler binary when `ctx.compiler` is empty. +/// - `extensions`: Source extensions to collect. +/// - `build_object`: Per-source assemble callback `(assembler, src, out, ctx)`. +/// +/// # Returns +/// Elapsed seconds, or an error string. pub fn build_assembly( ctx: &BuildContext, backend_name: &str, @@ -36,6 +56,15 @@ where ) } +/// Builds assembly sources; for `flat-bin`, assembles to objects then runs objcopy. +/// +/// For non-flat kinds, assembles to objects and then links/archives as usual. +/// +/// # Parameters +/// - Same as [`build_assembly`]; only the flat emission strategy differs. +/// +/// # Returns +/// Elapsed seconds, or an error string. pub fn build_assembly_via_objcopy( ctx: &BuildContext, backend_name: &str, @@ -56,12 +85,14 @@ where ) } +/// Flat-bin emission strategy: direct assembler output, or object then objcopy. #[derive(Clone, Copy)] enum FlatEmit { Direct, ViaObjcopy, } +/// Core assembly build with a chosen flat emission mode; `build_object` runs per source in parallel. fn build_assembly_with_mode( ctx: &BuildContext, backend_name: &str, @@ -74,6 +105,7 @@ where F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, { let lang = ctx.language.to_lowercase(); + // Validate language if lang.split(',').any(|p| { let t = p.trim(); t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll" @@ -104,6 +136,7 @@ where }; let objects = match (flat, flat_mode) { + // Dispatch based on flat binary kind and emission mode (true, FlatEmit::Direct) => { let outs: Vec = sources .iter() @@ -148,6 +181,7 @@ where artifact::link_binary(ctx, &objects, "cc", start_time) } +/// Compiles all sources to objects in parallel (non-flat path) and returns object paths. fn build_objects_common( assembler: &str, sources: &[String], diff --git a/src/core/build/language/asm/fasm/mod.rs b/src/core/build/language/asm/fasm/mod.rs index 3d05d48..ff928ec 100644 --- a/src/core/build/language/asm/fasm/mod.rs +++ b/src/core/build/language/asm/fasm/mod.rs @@ -5,10 +5,14 @@ use std::fs; use std::path::Path; use std::process::Command; +/// Builds an assembly file using the FASM assembler. +/// +/// Delegates to the shared assembly build logic with FASM configuration. pub fn build(ctx: &BuildContext) -> Result { asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object) } +/// Collects all source files for assembly files using FASM. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { common::collect_sources( ctx.source_roots, @@ -18,6 +22,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Assembles one FASM source into an object file. fn build_object( assembler: &str, source: &str, @@ -28,6 +33,7 @@ fn build_object( fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } + // Skip rebuilding if the object is up to date if !common::needs_rebuild(source, obj_path) { return Ok(()); } diff --git a/src/core/build/language/asm/gas/mod.rs b/src/core/build/language/asm/gas/mod.rs index 5adf735..6aab0bb 100644 --- a/src/core/build/language/asm/gas/mod.rs +++ b/src/core/build/language/asm/gas/mod.rs @@ -22,6 +22,10 @@ use std::fs; use std::path::Path; use std::process::Command; +/// Builds GAS (`.s`) sources via the GNU Assembler. +/// +/// Resolves `as` on PATH and delegates to the shared assembly builder. For +/// `flat-bin`, intermediates are converted with objcopy after assembly. pub fn build(ctx: &BuildContext) -> Result { // Use full path to as let as_path = std::process::Command::new("which") @@ -35,6 +39,9 @@ pub fn build(ctx: &BuildContext) -> Result { asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object) } +/// Collects assembly source files with .s extension from the build context's source roots. +/// +/// Excludes specified directories and respects include paths for filtering. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { common::collect_sources( ctx.source_roots, @@ -44,6 +51,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Assembles a single .s source file into an object file using the provided assembler. +/// +/// Creates parent directories for the output object, skips if up-to-date, +/// runs the assembler with flags from the build context, and handles verbose output. fn build_object( assembler: &str, source: &str, @@ -54,10 +65,12 @@ fn build_object( fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } + // Check if the object file is already up to date to avoid unnecessary rebuilds. if !common::needs_rebuild(source, obj_path) { return Ok(()); } + // Prepare the assembler command with source file and output path. let mut cmd = Command::new(assembler); cmd.arg(source).arg("-o").arg(obj_path); diff --git a/src/core/build/language/asm/masm/mod.rs b/src/core/build/language/asm/masm/mod.rs index 558093f..239092e 100644 --- a/src/core/build/language/asm/masm/mod.rs +++ b/src/core/build/language/asm/masm/mod.rs @@ -5,10 +5,15 @@ use std::fs; use std::path::Path; use std::process::Command; +/// Builds MASM (`.asm`) sources via `ml`. +/// +/// For `flat-bin`, intermediates are converted with objcopy after assembly. +/// Returns elapsed seconds or an error. pub fn build(ctx: &BuildContext) -> Result { asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object) } +/// Collects MASM assembly source files from the given source roots. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { common::collect_sources( ctx.source_roots, @@ -18,6 +23,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Builds a single MASM object file from the given source. +/// +/// Creates the parent directory for the output object if it doesn't exist, +/// skips the build if the object is already up to date, constructs the ml +/// command line with flags, prints the command in debug mode if enabled, +/// and executes it. fn build_object( assembler: &str, source: &str, @@ -25,10 +36,12 @@ fn build_object( ctx: &BuildContext, ) -> Result<(), String> { if let Some(parent) = Path::new(obj_path).parent() { + // Ensure the parent directory exists for the object file fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } if !common::needs_rebuild(source, obj_path) { + // Skip rebuild if the object file is already current return Ok(()); } @@ -40,6 +53,7 @@ fn build_object( .arg(source); for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) { + // Add filtered compiler flags to the command cmd.arg(flag); } diff --git a/src/core/build/language/asm/mod.rs b/src/core/build/language/asm/mod.rs index 04888fb..39122ce 100644 --- a/src/core/build/language/asm/mod.rs +++ b/src/core/build/language/asm/mod.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Assembly language module for the DCR build system. pub mod common; pub mod fasm; pub mod gas; @@ -23,15 +24,21 @@ pub mod nasm; use crate::core::build::language::Language; +/// Represents the assembly language support in the build system. pub struct Asm; impl Language for Asm { + /// Returns the identifier string for this language. fn id(&self) -> &'static str { "asm" } + + /// Returns the list of supported file extensions for assembly files. fn extensions(&self) -> &'static [&'static str] { &["s", "S", "asm"] } + + /// Checks whether the given token matches the assembly language, using case-insensitive comparison. fn matches_token(&self, token: &str) -> bool { token.eq_ignore_ascii_case("asm") } diff --git a/src/core/build/language/asm/nasm/mod.rs b/src/core/build/language/asm/nasm/mod.rs index 7a273f6..10aedfd 100644 --- a/src/core/build/language/asm/nasm/mod.rs +++ b/src/core/build/language/asm/nasm/mod.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Support for NASM assembly language in the DCR build system. use crate::core::build::builder::BuildContext; use crate::core::build::common; use crate::core::build::language::asm::common as asm; @@ -22,10 +23,16 @@ use std::fs; use std::path::Path; use std::process::Command; +/// Builds assembly sources using the NASM assembler. +/// +/// Delegates to the shared assembly builder with NASM-specific configuration. pub fn build(ctx: &BuildContext) -> Result { asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object) } +/// Collects source files for NASM assembly. +/// +/// Filters for .asm files only, as NASM uses that extension (distinct from GAS .s). pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { // NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed) common::collect_sources( @@ -36,6 +43,9 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Builds a single object file from an assembly source using NASM. +/// +/// Handles directory creation, rebuild checks, format selection, and command execution. fn build_object( assembler: &str, source: &str, @@ -43,13 +53,16 @@ fn build_object( ctx: &BuildContext, ) -> Result<(), String> { if let Some(parent) = Path::new(obj_path).parent() { + // Ensure output directory exists before building. fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } + // Skip building if the object is already up to date. if !common::needs_rebuild(source, obj_path) { return Ok(()); } + // Select appropriate object format for the target platform and binary type. let format = if crate::utils::build::is_flat_bin(ctx.kind) { "bin" } else { @@ -63,6 +76,7 @@ fn build_object( .arg("-o") .arg(obj_path); + // Append any additional assembly flags from build context. for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) { cmd.arg(flag); } @@ -74,6 +88,9 @@ fn build_object( common::run_command_sync_output(&mut cmd) } +/// Selects the NASM object file format string based on the target platform. +/// +/// Supports different formats for x86 on various OSes and architectures. fn nasm_format(platform: Option<&str>) -> &'static str { if let Some(p) = platform { let p = p.to_lowercase().replace('-', "_"); diff --git a/src/core/build/language/c/mod.rs b/src/core/build/language/c/mod.rs index c3a3da0..bce0593 100644 --- a/src/core/build/language/c/mod.rs +++ b/src/core/build/language/c/mod.rs @@ -17,15 +17,21 @@ use crate::core::build::language::Language; +/// Represents the C programming language support within the DCR build system. pub struct C; impl Language for C { + /// Returns the unique identifier for the C language. fn id(&self) -> &'static str { "c" } + + /// Returns an array containing the file extension associated with C source code. fn extensions(&self) -> &'static [&'static str] { &["c"] } + + /// Checks whether the given token matches the C language identifier in a case-insensitive manner. fn matches_token(&self, token: &str) -> bool { token.eq_ignore_ascii_case("c") } diff --git a/src/core/build/language/cxx/mod.rs b/src/core/build/language/cxx/mod.rs index 8e8220b..26fb22c 100644 --- a/src/core/build/language/cxx/mod.rs +++ b/src/core/build/language/cxx/mod.rs @@ -15,20 +15,26 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// C++ language support for the DCR build system. pub mod qt; use crate::core::build::language::Language; pub struct Cxx; +/// C++ language implementation. impl Language for Cxx { + /// Returns the language ID "cxx". fn id(&self) -> &'static str { "cxx" } + /// Returns the extensions for C++ files. fn extensions(&self) -> &'static [&'static str] { &["cpp", "cxx", "cc"] } + /// Checks if the token matches C++ language names, ignoring case. fn matches_token(&self, token: &str) -> bool { + // Case-insensitive comparison using to_lowercase matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx") } } diff --git a/src/core/build/language/cxx/qt.rs b/src/core/build/language/cxx/qt.rs index e932461..aa509ed 100644 --- a/src/core/build/language/cxx/qt.rs +++ b/src/core/build/language/cxx/qt.rs @@ -22,6 +22,16 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +/// Runs uic/rcc/moc on Qt UI, resource, and header sources under `build_dir/qt`. +/// +/// # Parameters +/// - `ctx`: Sources, excludes, and toolchain tool paths. +/// - `build_dir`: Directory that will contain the generated `qt/` tree. +/// +/// # Returns +/// - `Ok(Some(qt_dir))` if any generated files were produced. +/// - `Ok(None)` if there was nothing to process. +/// - `Err` if a Qt tool fails. pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result, String> { let qt_dir = build_dir.join("qt"); fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?; @@ -37,6 +47,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result Result Result<(), String> { let mut cmd = Command::new(cmd_path); cmd.args(args); diff --git a/src/core/build/language/llvm_ir/mod.rs b/src/core/build/language/llvm_ir/mod.rs index 425a82b..0b121dc 100644 --- a/src/core/build/language/llvm_ir/mod.rs +++ b/src/core/build/language/llvm_ir/mod.rs @@ -6,24 +6,32 @@ use std::fs; use std::path::Path; use std::process::Command; +/// Represents the LLVM IR language implementation for the build system. pub struct LlvmIr; impl Language for LlvmIr { + /// Returns the unique identifier for the LLVM IR language. fn id(&self) -> &'static str { "llvm_ir" } + + /// Returns the list of file extensions recognized for this language. fn extensions(&self) -> &'static [&'static str] { &["ll"] } + + /// Determines whether the provided token matches the LLVM IR language. fn matches_token(&self, token: &str) -> bool { matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll") } } +/// Builds LLVM IR source files into object files using llc. pub fn build(ctx: &BuildContext) -> Result { asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object) } +/// Collects source files for LLVM IR compilation from the given context. pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { common::collect_sources( ctx.source_roots, @@ -33,6 +41,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } +/// Builds a single LLVM IR source file into an object file. fn build_object( assembler: &str, source: &str, @@ -40,6 +49,7 @@ fn build_object( ctx: &BuildContext, ) -> Result<(), String> { if let Some(parent) = Path::new(obj_path).parent() { + // Create parent directory if it does not exist fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; } diff --git a/src/core/build/language/mod.rs b/src/core/build/language/mod.rs index d1966b7..2d21ef0 100644 --- a/src/core/build/language/mod.rs +++ b/src/core/build/language/mod.rs @@ -15,21 +15,29 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Core module for language support in the DCR build system. +/// Defines the Language trait and helper functions to register and lookup languages. pub mod asm; pub mod c; pub mod cxx; pub mod llvm_ir; +/// Trait representing a supported programming language. +/// Each implementation must specify its unique identifier, list of supported file extensions, +/// and a method to determine if a given token matches the language. pub trait Language { fn id(&self) -> &'static str; fn extensions(&self) -> &'static [&'static str]; fn matches_token(&self, token: &str) -> bool; } +/// Returns a fixed-size array containing static references to all supported language types. pub fn languages() -> [&'static dyn Language; 4] { [&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr] } +/// Looks up the language implementation that matches the given token by checking each language's +/// matches_token method. Returns the first match or None if no language matches. pub fn language_for_token(token: &str) -> Option<&'static dyn Language> { languages().into_iter().find(|l| l.matches_token(token)) } @@ -38,6 +46,7 @@ pub fn language_for_token(token: &str) -> Option<&'static dyn Language> { mod tests { use super::*; + /// Ensures that input tokens are resolved to the correct language IDs. #[test] fn tokens_resolve_to_languages() { assert_eq!(language_for_token("c").unwrap().id(), "c"); @@ -48,6 +57,7 @@ mod tests { assert!(language_for_token("rust").is_none()); } + /// Ensures that each language reports the expected set of file extensions. #[test] fn languages_expose_expected_extensions() { assert_eq!(c::C.extensions(), &["c"]); diff --git a/src/core/build/mod.rs b/src/core/build/mod.rs index 4139744..eda3d29 100644 --- a/src/core/build/mod.rs +++ b/src/core/build/mod.rs @@ -15,6 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Core build module for DCR — a Cargo-like C/C++ project manager. +/// This module defines the build engine and re-exports key components for +/// building C/C++ projects. #[cfg(feature = "archive")] pub mod archive; pub mod builder; diff --git a/src/core/build/report.rs b/src/core/build/report.rs index a23c2ca..016f74e 100644 --- a/src/core/build/report.rs +++ b/src/core/build/report.rs @@ -21,6 +21,7 @@ use std::sync::{Arc, atomic::AtomicBool}; +/// Represents a build request with options for the build process, including profile selection, target specification, force flag, verbosity, workspace, and a cancellation signal. pub struct BuildRequest { pub profile: String, pub target: Option, @@ -30,21 +31,25 @@ pub struct BuildRequest { pub cancel: Arc, } +/// Contains the result of a build operation, specifically the duration in seconds. #[allow(dead_code)] pub struct BuildOutcome { pub secs: f64, } +/// Wraps a string message as a build error. pub struct BuildError { pub message: String, } +/// Creates a BuildError from a String message. impl From for BuildError { fn from(message: String) -> Self { BuildError { message } } } +/// Represents events that the build engine can emit during compilation and packaging. pub enum BuildEvent<'a> { TargetStart { index: usize, @@ -81,6 +86,8 @@ pub enum BuildEvent<'a> { }, } +/// Trait for handling build events in different reporting backends. pub trait BuildReporter { + /// Callback invoked when a build event is generated. fn on_event(&mut self, event: BuildEvent<'_>); } diff --git a/src/core/build/resolve.rs b/src/core/build/resolve.rs index 3d68f2a..be11f41 100644 --- a/src/core/build/resolve.rs +++ b/src/core/build/resolve.rs @@ -18,6 +18,12 @@ use crate::core::build_config::Config; use crate::utils::build::{get_config_str, normalize_target_os, profile_table}; +/// Retrieves a raw TOML value from the config using a prioritized lookup order based on target, profile, and base sections. +/// +/// The keys are checked in this order: +/// {section}.{target}.{profile}.{field}, {section}.{profile}.{target}.{field}, {section}.{target}.{field}, {section}.{profile}.{field}, {section}.{field} +/// +/// Returns the value if found, otherwise None. pub(crate) fn get_config_value_raw( config: &Config, section: &str, @@ -57,6 +63,9 @@ pub(crate) fn get_config_value_raw( None } +/// Checks if the 'inherit' flag is enabled for the given section, profile, and target. +/// +/// Returns true by default if the flag is not set. pub(crate) fn get_inherit( config: &Config, section: &str, @@ -68,6 +77,9 @@ pub(crate) fn get_inherit( .unwrap_or(true) } +/// Extracts a trimmed non-empty string value from the config. +/// +/// Returns Some(value) if found, None otherwise. pub(crate) fn get_config_value( config: &Config, section: &str, @@ -80,6 +92,9 @@ pub(crate) fn get_config_value( .filter(|s| !s.is_empty()) } +/// Fetches a string value from the build section, inheriting from base if enabled. +/// +/// Returns the value or default empty string if not inheriting. pub(crate) fn get_string_with_profile_and_target( config: &Config, field: &str, @@ -94,10 +109,12 @@ pub(crate) fn get_string_with_profile_and_target( } } +/// Gets a build string value for the given profile without a target. pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String { get_string_with_profile_and_target(config, field, profile, None) } +/// Gets a string value from the language-specific build.{lang} section. pub(crate) fn get_lang_string( config: &Config, lang: &str, @@ -108,6 +125,7 @@ pub(crate) fn get_lang_string( get_config_value(config, &format!("build.{lang}"), field, profile, target) } +/// Gets a list of strings from the language-specific build.{lang} section. pub(crate) fn get_lang_list( config: &Config, lang: &str, @@ -121,6 +139,7 @@ pub(crate) fn get_lang_list( } } +/// Parses a TOML array into a Vec, erroring if not an array of strings. pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result, String> { let arr = value .as_array() @@ -135,6 +154,9 @@ pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result Result, String> { get_list_with_profile(config, "targets", profile) } +/// Parses the config value as a list of strings. +/// +/// # Parameters +/// - `config`: Project config. +/// - `key`: Dot-path key (e.g. `build.sources`). +/// +/// # Returns +/// - `Ok([])` if the key is missing. +/// - `Ok(vec)` for an array of strings. +/// - `Err` if present but not an array of strings. pub(crate) fn get_config_list(config: &Config, key: &str) -> Result, String> { let value = match config.get(key) { Some(v) => v, diff --git a/src/core/build/steps.rs b/src/core/build/steps.rs index a3c0048..c8a6ed9 100644 --- a/src/core/build/steps.rs +++ b/src/core/build/steps.rs @@ -30,6 +30,7 @@ pub(crate) struct BuildStep { cmd: String, } +/// Holds interpolated version and profile strings for build step substitution. pub(crate) struct StepVars<'a> { pub(crate) profile: &'a str, pub(crate) version: &'a str, @@ -40,6 +41,7 @@ pub(crate) struct StepVars<'a> { pub(crate) version_suffix_dash: &'a str, } +/// Parses TOML array of build step tables into Vec. fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result, String> { let arr = value .as_array() @@ -86,6 +88,7 @@ fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result Result, String> { let value = match config.get(key) { Some(v) => v, @@ -110,6 +114,7 @@ fn get_build_steps(config: &Config, key: &str) -> Result, String> use crate::core::build::report::{BuildEvent, BuildReporter}; use std::sync::{Arc, atomic::AtomicBool}; +/// Runs all build steps in sequence, checking for cancellation and reporting progress. pub(crate) fn run_build_steps( steps: &[BuildStep], tools: &ToolchainExecs, @@ -127,6 +132,7 @@ pub(crate) fn run_build_steps( Ok(()) } +/// Executes a single build step, handling glob expansion, output paths, and command substitution. fn run_build_step( step: &BuildStep, tools: &ToolchainExecs, @@ -137,6 +143,7 @@ fn run_build_step( ) -> Result<(), String> { let input_pattern = expand_step_value(&step.input, "", vars); let inputs = expand_glob(&input_pattern)?; + // Skip step if no matching input files found if inputs.is_empty() { return Ok(()); } @@ -170,6 +177,7 @@ fn run_build_step( Ok(()) } +/// Deletes files (not directories) matching the given glob patterns. pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> { for pattern in patterns { for path in expand_glob(pattern)? { @@ -181,6 +189,7 @@ pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> { Ok(()) } +/// Expands glob patterns to find matching files. pub(crate) fn expand_glob(pattern: &str) -> Result, String> { let mut out = Vec::new(); let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?; @@ -191,6 +200,7 @@ pub(crate) fn expand_glob(pattern: &str) -> Result, String> { Ok(out) } +/// Verifies that expected artifacts are present by expanding patterns. pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> { for pattern in patterns { let expanded = expand_step_value(pattern, "", vars); @@ -202,6 +212,7 @@ pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Resul Ok(()) } +/// True if the step should run: missing/unreadable output, unreadable input, or input newer than output. fn should_run_step(input: &Path, output: &Path) -> bool { let in_time = fs::metadata(input).and_then(|m| m.modified()); let out_time = fs::metadata(output).and_then(|m| m.modified()); @@ -212,6 +223,7 @@ fn should_run_step(input: &Path, output: &Path) -> bool { } } +/// Substitutes variables into step command template and replaces placeholders. fn substitute_step_cmd( template: &str, input: &Path, @@ -232,6 +244,7 @@ fn substitute_step_cmd( .replace("{stem}", stem) } +/// Builds a list of compiler flags from cflags, include dirs, and compiler type. pub(crate) fn build_step_flags( cflags: &[String], include_dirs: &[String], @@ -264,6 +277,7 @@ pub(crate) fn build_step_flags( .join(" ") } +/// Quotes argument if it contains whitespace or double quotes. fn quote_step_arg(arg: String) -> String { if !arg.chars().any(|c| c.is_whitespace() || c == '"') { return arg; @@ -272,6 +286,7 @@ fn quote_step_arg(arg: String) -> String { format!("\"{escaped}\"") } +/// Checks if compiler is MSVC based on name. fn is_msvc_compiler(compiler: &str) -> bool { let lower = compiler.to_lowercase(); lower.contains("cl.exe") @@ -280,6 +295,7 @@ fn is_msvc_compiler(compiler: &str) -> bool { || lower.contains("msvc") } +/// Runs shell command with output capture and cancellation support. fn run_shell_command( cmd: &str, cancel: &Arc, @@ -352,6 +368,7 @@ fn run_shell_command( Ok(status) } +/// Checks if any build step needs to be run by comparing timestamps. pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result { for step in steps { let input_pattern = expand_step_value(&step.input, "", vars); @@ -380,6 +397,7 @@ pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Resu Ok(false) } +/// Creates VersionInfo from StepVars. fn make_version_info(vars: &StepVars) -> VersionInfo { VersionInfo { full: vars.version.to_string(), @@ -391,6 +409,7 @@ fn make_version_info(vars: &StepVars) -> VersionInfo { } } +/// Expands step value by substituting vars and stem. fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String { let info = make_version_info(vars); let s = substitute_vars(template, &info, vars.profile, ""); diff --git a/src/core/build_config.rs b/src/core/build_config.rs index 96c6f42..5b8ac0a 100644 --- a/src/core/build_config.rs +++ b/src/core/build_config.rs @@ -39,6 +39,7 @@ const VALID_KINDS: &[&str] = &[ "flat-bin", ]; +/// Represents errors that can occur during configuration loading, parsing, or validation. #[derive(Debug)] pub enum ConfigError { Io(std::io::Error), @@ -80,6 +81,7 @@ impl From for ConfigError { } } +/// Core configuration structure that manages TOML parsing, typed views, and validation for DCR projects. pub struct Config { path: PathBuf, data: Value, @@ -91,6 +93,7 @@ pub struct Config { } #[allow(dead_code)] +/// Top-level typed configuration parsed from TOML, containing optional sections for package, build, dependencies, etc. #[derive(Debug, Clone, Deserialize)] pub struct DcrConfig { #[serde(default)] @@ -108,6 +111,7 @@ pub struct DcrConfig { } #[allow(dead_code)] +/// Configuration for archive output formats (e.g. EFI, bootable images). #[derive(Debug, Clone, Deserialize)] pub struct ArchiveConfig { pub output: String, @@ -121,12 +125,14 @@ pub struct ArchiveConfig { } #[allow(dead_code)] +/// Layout definition for archive sections (from/to paths). #[derive(Debug, Clone, Deserialize)] pub struct ArchiveLayout { pub from: String, pub to: String, } +/// Package metadata section (name, version, type). #[derive(Debug, Clone, Deserialize)] pub struct PackageConfig { pub name: String, @@ -144,6 +150,7 @@ fn default_compiler() -> String { } #[allow(dead_code)] +/// Build configuration section, including language, standards, compiler, and output settings. #[derive(Debug, Clone, Deserialize)] pub struct BuildConfig { #[serde(default = "default_language")] @@ -182,6 +189,7 @@ pub struct BuildConfig { pub qt: Option, } +/// Language configuration can be a single string or array of strings (e.g. for multi-language builds). #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] pub enum LanguageConfig { @@ -198,6 +206,7 @@ impl LanguageConfig { } } +/// Dependency configuration can be a version string or a full table of dependency details. #[allow(dead_code)] #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] @@ -206,6 +215,7 @@ pub enum DependencyConfig { Table(BTreeMap), } +/// Toolchain configuration for custom compilers, assemblers, and linkers. #[derive(Debug, Clone, Deserialize, Default)] pub struct ToolchainConfig { pub cc: Option, @@ -216,6 +226,7 @@ pub struct ToolchainConfig { pub ld: Option, } +/// Workspace member configuration for multi-project workspaces. #[allow(dead_code)] #[derive(Debug, Clone, Deserialize)] pub struct WorkspaceMemberConfig { @@ -227,6 +238,7 @@ pub struct WorkspaceMemberConfig { } impl Config { + /// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one. pub fn new(path: &str) -> Result { let path = PathBuf::from(path); let (data, doc) = if path.exists() { @@ -249,6 +261,7 @@ impl Config { Ok(cfg) } + /// Opens an existing Config from a TOML file path, failing if the file does not exist. pub fn open(path: &str) -> Result { let path = PathBuf::from(path); if !path.exists() { @@ -267,16 +280,19 @@ impl Config { } #[allow(dead_code)] + /// Returns the fully typed configuration struct. pub fn typed(&self) -> &DcrConfig { &self.typed } #[allow(dead_code)] + /// Returns the package configuration if present. pub fn package(&self) -> Option<&PackageConfig> { self.typed.package.as_ref() } #[allow(dead_code)] + /// Returns the build configuration if present. pub fn build_config(&self) -> Option<&BuildConfig> { self.typed.build.as_ref() } @@ -290,7 +306,7 @@ impl Config { && !self.typed.workspace.is_empty() } - /// Merge build fields from parent config into self where self has empty/default values. + /// Merges build fields from a parent config into self where self has empty/default values. /// Used for workspace member inheritance (build.inherit = true). pub fn merge_parent(&mut self, parent: &Config) { let fields: &[&str] = &[ @@ -368,24 +384,29 @@ impl Config { } } + /// Retrieves a nested value from the config data using dot-separated path. pub fn get(&self, key: &str) -> Option<&Value> { let parts: Vec<&str> = key.split('.').collect(); get_path(&self.data, &parts) } #[allow(dead_code)] + /// Adds a new key-value pair to the configuration (alias for set). pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { self.set(key, value) } + /// Updates a configuration value at the given key path. pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { self.set(key, value) } #[allow(dead_code)] + /// Checks if the configuration is valid without returning the error. pub fn check(&self) -> bool { self.validate().is_ok() } + /// Performs comprehensive validation of the configuration, including package, build, toolchain, and workspace rules. pub fn validate(&self) -> Result<(), ConfigError> { if self.is_workspace_only() { return self.validate_workspace(); diff --git a/src/core/deps/common.rs b/src/core/deps/common.rs index 97811d0..ddded48 100644 --- a/src/core/deps/common.rs +++ b/src/core/deps/common.rs @@ -1,3 +1,4 @@ +/// Represents resolved dependencies for a build target, including include directories, library directories, and libraries. #[derive(Debug, Clone, Default)] pub struct ResolvedDeps { pub include_dirs: Vec, diff --git a/src/core/deps/lock.rs b/src/core/deps/lock.rs index 1ac1db0..c404875 100644 --- a/src/core/deps/lock.rs +++ b/src/core/deps/lock.rs @@ -18,6 +18,7 @@ use std::fs; use std::path::Path; +/// Represents a locked dependency entry containing its name, version, source, and checksum. #[derive(Debug, Clone)] pub struct DepLock { pub name: String, @@ -26,6 +27,7 @@ pub struct DepLock { pub source: String, } +/// Writes the dcr.lock file in a Cargo-like format. The project entry comes first, followed by dependency entries. pub fn write_lock( project_root: &Path, project_name: &str, @@ -47,6 +49,7 @@ pub fn write_lock( } out.push('\n'); + // Write metadata for each package in the dependencies list for pkg in packages { out.push_str("[[package]]\n"); out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name))); @@ -60,6 +63,7 @@ pub fn write_lock( Ok(()) } +/// Converts a list of strings into a comma-separated TOML array by quoting each item. fn quote_list(items: &[String]) -> String { items .iter() @@ -68,6 +72,7 @@ fn quote_list(items: &[String]) -> String { .join(", ") } +/// Escapes backslashes and double quotes in a string to make it safe for TOML. fn escape_value(input: &str) -> String { input.replace('\\', "\\\\").replace('"', "\\\"") } diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index f6c163c..5a3c2b8 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Core dependency resolver for DCR projects. pub mod common; pub mod lock; pub mod register; @@ -24,6 +25,7 @@ use crate::core::deps::common::ResolvedDeps; use crate::core::deps::lock::{DepLock, write_lock}; use std::path::Path; +/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing. fn dep_version(path: &Path) -> String { Config::open(&path.join("dcr.toml").to_string_lossy()) .ok() @@ -35,6 +37,18 @@ fn dep_version(path: &Path) -> String { .unwrap_or_default() } +/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file. +/// +/// Registry and path deps contribute to `ResolvedDeps`. Git deps are recorded in the lock only +/// (no fetch or include/lib resolution). +/// +/// # Parameters +/// - `config`: Loaded `dcr.toml`. +/// - `_profile` / `_target`: Reserved for future profile/target-specific deps. +/// - `project_root`: Root used to resolve path deps and write `dcr.lock`. +/// +/// # Returns +/// Aggregated include/lib/libs, or an error string on resolve/lock failure. pub fn resolve_deps( config: &Config, _profile: &str, @@ -57,6 +71,7 @@ pub fn resolve_deps( let mut lock_packages: Vec = Vec::new(); if let Some(deps) = deps_table { + // Process each dependency declaration in the TOML table for (name, value) in deps { if register::is_registry_dep(value) { let pkg_info = register::resolve_package_from_registry(name)?; @@ -157,6 +172,7 @@ pub fn resolve_deps( } } + // Write the lock file with all resolved dependencies write_lock( project_root, &project_name, @@ -167,11 +183,13 @@ pub fn resolve_deps( Ok(resolved) } +/// Git dependency specification (url + optional version/ref for the lockfile). struct GitDep<'a> { url: &'a str, version: Option, } +/// Parses a git dependency from a TOML value table. fn git_dep(value: &toml::Value) -> Option> { if let Some(table) = value.as_table() && let Some(url) = table.get("git").and_then(|v| v.as_str()) @@ -185,6 +203,7 @@ fn git_dep(value: &toml::Value) -> Option> { None } +/// Extracts the path for a path-based dependency, supporting both table format and legacy string format. fn path_dep_path(value: &toml::Value) -> Option<&str> { if let Some(table) = value.as_table() { return table.get("path").and_then(|v| v.as_str()); @@ -192,12 +211,14 @@ fn path_dep_path(value: &toml::Value) -> Option<&str> { register::path_from_string_dep(value) } +/// Adds a directory path to the list if it exists on the filesystem. fn push_if_exists(paths: &mut Vec, path: &Path) { if path.exists() { paths.push(path.to_string_lossy().to_string()); } } +/// Adds common library directory candidates to the list if they exist. fn push_default_lib_dirs(paths: &mut Vec, dep_root: &Path) { for dir in ["lib", "lib64"] { push_if_exists(paths, &dep_root.join(dir)); @@ -210,6 +231,7 @@ mod tests { use super::*; use toml::Value; + /// Tests that path_dep_path correctly handles both TOML table and legacy string inputs for path dependencies. #[test] fn path_dep_path_supports_table_and_legacy_strings() { let table = Value::Table( @@ -232,6 +254,7 @@ mod tests { assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None); } + /// Verifies that push_default_lib_dirs includes the target/lib directory when it exists. #[test] fn default_lib_dirs_include_packaged_library_output() { let root = std::env::temp_dir().join(format!( diff --git a/src/core/deps/register.rs b/src/core/deps/register.rs index e03a175..4dfeef0 100644 --- a/src/core/deps/register.rs +++ b/src/core/deps/register.rs @@ -21,25 +21,30 @@ use std::fs; use std::path::{Path, PathBuf}; use toml::Value as TomlValue; +/// Represents a registry configuration entry with its URL and priority. #[derive(Debug, Serialize, Deserialize)] pub struct Registry { pub url: String, pub priority: i32, } +/// Configuration struct for DCR registries, mapping registry names to Registry details. #[derive(Debug, Serialize, Deserialize)] pub struct DcrConfig { pub registry: std::collections::HashMap, } +/// Helper function to get the user's home directory path. fn home_dir() -> Option { crate::utils::fs::home_dir() } +/// Helper to get the DCR config directory path. fn dcr_config_dir() -> Option { home_dir().map(|h| h.join(".dcr")) } +/// Loads DCR registry configuration from the TOML file in the user's home directory. pub fn get_registry_config() -> Option { let home = home_dir()?; let config_path = home.join(".dcr/config.toml"); @@ -51,6 +56,7 @@ pub fn get_registry_config() -> Option { toml::from_str(&content).ok() } +/// Returns the path to the registry index file, preferring the DCR_INDEX_PATH env var or falling back to ~/.dcr/index.json. pub fn get_index_path() -> PathBuf { std::env::var("DCR_INDEX_PATH") .map(PathBuf::from) @@ -61,6 +67,7 @@ pub fn get_index_path() -> PathBuf { }) } +/// Returns the root directory for registry cache, based on the index path's parent, creating it if necessary. pub fn get_registry_cache_root() -> PathBuf { let index_path = get_index_path(); if let Some(parent) = index_path.parent() { @@ -76,6 +83,7 @@ pub fn get_registry_cache_root() -> PathBuf { } } +/// Extracts the package root path from registry info. If the path points to a dcr.toml manifest, returns its parent directory. pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result { let raw_path = pkg_info .get("path") @@ -97,6 +105,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result PathBuf { let s = raw.trim(); if let Some(rest) = s.strip_prefix("file://") { @@ -130,14 +139,26 @@ pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf { } } +/// Returns the include directory path for a dependency root, typically under target/include. pub fn registry_include_dir(dep_root: &Path) -> PathBuf { dep_root.join("target").join("include") } +/// Returns the library directory path for a dependency root, typically under target/lib. pub fn registry_lib_dir(dep_root: &Path) -> PathBuf { dep_root.join("target").join("lib") } +/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`). +/// +/// Configured registries are sorted by priority, but the lookup currently always reads the +/// same local index path (per-registry URLs are unused). +/// +/// # Parameters +/// - `name`: Package name as in the index. +/// +/// # Returns +/// Package JSON object, or an error if config/index is missing or the name is absent. pub fn resolve_package_from_registry(name: &str) -> Result { let config = get_registry_config().ok_or("No registry config found")?; let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect(); @@ -165,6 +186,10 @@ pub fn resolve_package_from_registry(name: &str) -> Result { )) } +/// True if the TOML value is a registry-style dependency. +/// +/// Strings: not path-like and not git-like. Tables: have version/features/optional/registry +/// and no `git`/`path`/`url` keys. pub fn is_registry_dep(value: &TomlValue) -> bool { if let Some(raw) = value.as_str() { let raw = raw.trim(); @@ -183,6 +208,7 @@ pub fn is_registry_dep(value: &TomlValue) -> bool { } } +/// Extracts the path string from a dependency value if it's a path-like dependency. pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> { let raw = value.as_str()?.trim(); if let Some(path) = raw.strip_prefix("path:") { @@ -194,6 +220,7 @@ pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> { None } +/// Helper to check if a string represents a local file path. fn is_path_like_string(raw: &str) -> bool { raw.starts_with("path:") || raw.starts_with("./") @@ -203,6 +230,7 @@ fn is_path_like_string(raw: &str) -> bool { || raw.contains('\\') } +/// Helper to check if a string represents a git or remote dependency. fn is_git_like_string(raw: &str) -> bool { raw.starts_with("git:") || raw.starts_with("github:") diff --git a/src/core/mod.rs b/src/core/mod.rs index 638cdb9..b4cdc3c 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Core module for DCR, a Cargo-like C/C++ project manager. pub mod build; pub mod build_config; pub mod deps; diff --git a/src/core/registry.rs b/src/core/registry.rs index 50a2814..31d6bc5 100644 --- a/src/core/registry.rs +++ b/src/core/registry.rs @@ -15,33 +15,40 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Registry configuration management for DCR. use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; +/// Represents a registry configuration entry with its URL and priority. #[derive(Debug, Serialize, Deserialize)] pub struct Registry { pub url: String, pub priority: i32, } +/// Represents the overall configuration loaded from config.toml, mapping registry names to their details. #[derive(Debug, Serialize, Deserialize)] pub struct Config { pub registry: HashMap, } +/// Manages the DCR registry configuration, loading from ~/.dcr/config.toml. pub struct RegistryManager { pub config: Config, #[allow(dead_code)] pub path: PathBuf, } +/// Returns the user's home directory path, used to locate the config file. fn home_dir() -> Option { crate::utils::fs::home_dir() } impl RegistryManager { + /// Loads the registry configuration from ~/.dcr/config.toml. + /// Returns an error if the config file is missing. pub fn load() -> Result> { let dcr_dir = home_dir() .ok_or("Cannot determine home directory")? @@ -53,6 +60,7 @@ impl RegistryManager { } let content = fs::read_to_string(&config_path)?; + // Deserialize the TOML content into the Config struct let config: Config = toml::from_str(&content)?; Ok(Self { diff --git a/src/core/runner.rs b/src/core/runner.rs index 32fc08d..7d6255f 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -19,6 +19,16 @@ use crate::platform; use std::path::Path; use std::process::Command; +/// Runs the built binary for a project, forwarding `bin_args`. +/// +/// # Parameters +/// - `project_name`: Package/binary name used to resolve the artifact path. +/// - `profile`: Build profile segment under `target/`. +/// - `target_dir`: Optional artifact directory override. +/// - `bin_args`: Arguments passed to the process. +/// +/// # Returns +/// Child exit code, or `1` if the binary is missing or cannot be spawned. pub fn run_binary( project_name: &str, profile: &str, @@ -26,6 +36,7 @@ pub fn run_binary( bin_args: &[String], ) -> i32 { let bin_path = platform::bin_path(profile, project_name, target_dir); + // Check if the binary exists before attempting to run it if Path::new(&bin_path).exists() { let status = Command::new(&bin_path).args(bin_args).status(); match status { @@ -37,5 +48,6 @@ pub fn run_binary( } } } + // Return 1 if binary not found or execution failed 1 } diff --git a/src/core/vcs/git.rs b/src/core/vcs/git.rs index 7a4c9f3..b6f9bde 100644 --- a/src/core/vcs/git.rs +++ b/src/core/vcs/git.rs @@ -18,10 +18,14 @@ use std::fs; use std::path::Path; -// creating git init and .gitignore +/// Initializes a Git repository in the target directory and configures its `.gitignore` file. +/// +/// Executes `git init` via the VCS utility module, creates a default `.gitignore` if none exists, +/// or updates an existing `.gitignore` to ensure `.dcr/` and build outputs are ignored. pub fn init(path: &Path) -> Result<(), String> { crate::utils::git::git_init(path)?; + // Populate default .gitignore entries or append DCR metadata rules. let gitignore_path = path.join(".gitignore"); if !gitignore_path.exists() { fs::write(&gitignore_path, "/target\n.dcr/\n") diff --git a/src/core/vcs/mod.rs b/src/core/vcs/mod.rs index 8d2e593..234f3ca 100644 --- a/src/core/vcs/mod.rs +++ b/src/core/vcs/mod.rs @@ -15,10 +15,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// Module for version control system (VCS) integration. pub mod git; use std::path::Path; +/// Represents the version control system kind to use. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VcsKind { Git, @@ -26,6 +28,7 @@ pub enum VcsKind { } impl VcsKind { + /// Parses the given string into a VcsKind. pub fn parse(s: &str) -> Result { match s.to_lowercase().as_str() { "git" => Ok(Self::Git), @@ -35,6 +38,7 @@ impl VcsKind { } } +/// Initializes the VCS for the provided path. pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> { match kind { VcsKind::Git => git::init(path), @@ -42,8 +46,9 @@ pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> { } } -// look for git repo in parents to avoid nested repos +/// Finds an existing VCS in the parent directories of the given path. pub fn find_existing_vcs(path: &Path) -> Option { + // look for git repo in parents to avoid nested repos let mut current = if path.is_file() { path.parent() } else { diff --git a/src/core/workspace.rs b/src/core/workspace.rs index 5ba1c64..b009bc8 100644 --- a/src/core/workspace.rs +++ b/src/core/workspace.rs @@ -21,6 +21,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use toml::Value; +/// Represents a member of the workspace. #[derive(Debug, Clone)] pub struct WorkspaceMember { pub name: String, @@ -29,12 +30,14 @@ pub struct WorkspaceMember { pub main: bool, } +/// Represents the workspace as a collection of members. #[derive(Debug, Clone)] pub struct Workspace { pub members: Vec, } impl Workspace { + /// Returns the main workspace member, preferring the one with `main: true` or the first member. pub fn main_member(&self) -> Option<&WorkspaceMember> { self.members .iter() @@ -43,6 +46,17 @@ impl Workspace { } } +/// Parses the workspace configuration from the TOML config. +/// +/// # Parameters +/// - `config`: Root `dcr.toml` (may be workspace-only or mixed). +/// - `profile` / `target`: Select profile- and target-specific `[workspace.*]` tables. +/// - `root`: Workspace root used to resolve member paths. +/// +/// # Returns +/// - `Ok(None)` if no workspace table applies. +/// - `Ok(Some(Workspace))` with members (and optional main). +/// - `Err` on invalid member paths or config shape. pub fn parse_workspace( config: &Config, profile: &str, @@ -50,6 +64,7 @@ pub fn parse_workspace( root: &Path, ) -> Result, String> { let mut table = None; + // Create ordered list of possible config keys for lookup let combinations = if let Some(t) = target { let normalized_t = normalize_target_os(t); vec![ @@ -103,6 +118,7 @@ pub fn parse_workspace( Ok(Some(ws)) } +/// Resolves a path, making it absolute if relative to root. fn resolve_path(root: &Path, raw: &str) -> Result { let p = Path::new(raw); let full = if p.is_absolute() { @@ -113,6 +129,7 @@ fn resolve_path(root: &Path, raw: &str) -> Result { Ok(full) } +/// Parses dependencies list from TOML value into Vec. fn parse_deps(value: Option<&Value>) -> Result, String> { let Some(value) = value else { return Ok(Vec::new()); @@ -130,12 +147,14 @@ fn parse_deps(value: Option<&Value>) -> Result, String> { Ok(out) } +/// Performs a topological sort on members to handle dependencies and detect cycles. fn topo_sort(members: &[WorkspaceMember]) -> Result, String> { let mut map = HashMap::new(); for m in members { map.insert(m.name.clone(), m.clone()); } let mut state: HashMap = HashMap::new(); + // Use 0=unvisited, 1=visiting, 2=visited for cycle detection let mut order: Vec = Vec::new(); for name in map.keys() { @@ -146,12 +165,14 @@ fn topo_sort(members: &[WorkspaceMember]) -> Result, String Ok(order) } +/// Recursive function for DFS-based topological sort with cycle detection. fn visit( name: &str, map: &HashMap, state: &mut HashMap, order: &mut Vec, ) -> Result<(), String> { + // Check current visit state to detect cycles match state.get(name).copied().unwrap_or(0) { 1 => return Err(format!("workspace dependency cycle at {name}")), 2 => return Ok(()), diff --git a/src/main.rs b/src/main.rs index 2305790..0254792 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,11 @@ mod core; mod platform; mod utils; +/// Main entry point for DCR, a Cargo-like C/C++ project manager. +/// +/// Collects command-line arguments via `std::env::args()` and dispatches to the +/// appropriate subcommand handler. With fewer than two arguments, shows help and +/// exits with code 0; otherwise exits with the handler's exit code. fn main() { let args: Vec = std::env::args().collect(); diff --git a/src/platform/bsd.rs b/src/platform/bsd.rs index 3570992..9f6739c 100644 --- a/src/platform/bsd.rs +++ b/src/platform/bsd.rs @@ -18,10 +18,12 @@ use super::path_util::{default_bin_rel, default_lib_rel, join_dir}; use crate::utils::build::default_target_triple; +/// Returns the host target triple used for default artifact paths on BSD. fn host_triple() -> String { default_target_triple() } +/// Returns the path to a binary for the given profile, name, and optional target directory. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => join_dir(dir, name), @@ -29,6 +31,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to a static library for the given profile, name, and optional target directory. pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.a"); match target_dir { @@ -37,10 +40,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to an ELF binary for the given profile, name, and optional target directory. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { bin_path(profile, name, target_dir) } +/// Returns the path to an EFI binary for the given profile, name, and optional target directory. pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("{name}.efi"); match target_dir { @@ -49,6 +54,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to a shared library for the given profile, name, and optional target directory. pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.so"); match target_dir { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 91f8572..a6d9ee6 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -18,10 +18,13 @@ use super::path_util::{default_bin_rel, default_lib_rel, join_dir}; use crate::utils::build::default_target_triple; +/// Returns the default target triple string for the host platform. fn host_triple() -> String { default_target_triple() } +/// Constructs the path to a binary executable. +/// If `target_dir` is provided, joins it with the name; otherwise uses default_bin_rel with the host triple. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => join_dir(dir, name), @@ -29,6 +32,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to a static library archive. pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.a"); match target_dir { @@ -37,10 +41,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to an ELF binary. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { bin_path(profile, name, target_dir) } +/// Constructs the path to an EFI binary. pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("{name}.efi"); match target_dir { @@ -49,6 +55,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to a shared library. pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.so"); match target_dir { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 39e1fa8..5e57304 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -15,8 +15,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +/// macOS-specific utilities for constructing paths to build artifacts. +/// +/// This module defines functions to generate paths for binaries, static +/// libraries, shared libraries, ELF files, and EFI executables on macOS. use super::path_util::{default_bin_rel, default_lib_rel, join_dir}; +/// Constructs the path to a binary for the given profile, name, and optional target directory. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => join_dir(dir, name), @@ -24,6 +29,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to a static library archive for the given profile, name, and optional target directory. pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.a"); match target_dir { @@ -32,10 +38,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to an ELF binary for the given profile, name, and optional target directory. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { bin_path(profile, name, target_dir) } +/// Constructs the path to an EFI executable for the given profile, name, and optional target directory. pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("{name}.efi"); match target_dir { @@ -44,6 +52,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Constructs the path to a shared library for the given profile, name, and optional target directory. pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.dylib"); match target_dir { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 25ff20a..b7c6ff8 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -40,6 +40,17 @@ pub mod posix; #[cfg(target_os = "windows")] pub mod windows; +/// Host OS path for a binary artifact. +/// +/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple. +/// +/// # Parameters +/// - `profile`: Build profile directory segment. +/// - `name`: Binary basename (may already include extension on some hosts). +/// - `target_dir`: Optional absolute/relative artifact directory override. +/// +/// # Returns +/// Path string suitable for the current host layout. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { @@ -76,6 +87,9 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Host OS path for an ELF-style artifact (`profile`, `name`, optional `target_dir`). +/// +/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { @@ -112,6 +126,9 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Host OS path for an EFI (`.efi`) artifact (`profile`, `name`, optional `target_dir`). +/// +/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple. pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { @@ -148,6 +165,9 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Host OS path for a static library (`profile`, `name`, optional `target_dir`). +/// +/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple. pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { @@ -184,6 +204,9 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Host OS path for a shared library (`profile`, `name`, optional `target_dir`). +/// +/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple. pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { diff --git a/src/platform/path_util.rs b/src/platform/path_util.rs index eb6d815..6f21fd2 100644 --- a/src/platform/path_util.rs +++ b/src/platform/path_util.rs @@ -17,14 +17,19 @@ use std::path::{Path, PathBuf}; +/// Joins the given directory and file into a single path string, trimming trailing slashes +/// for cross-platform compatibility. pub fn join_dir(dir: &str, file: &str) -> String { let trimmed = dir.trim_end_matches(['/', '\\']); Path::new(trimmed).join(file).to_string_lossy().into_owned() } #[cfg_attr(not(windows), allow(dead_code))] +/// Adds `.exe` suffix to the name if it does not already have a recognized executable +/// extension (`.exe`, `.dll`, `.sys`, `.com`, `.efi`) or any extension at all. pub fn with_exe_suffix(name: &str) -> String { let lower = name.to_ascii_lowercase(); + // Check if name already qualifies for no suffix (has executable ext or any ext) if lower.ends_with(".exe") || lower.ends_with(".dll") || lower.ends_with(".sys") @@ -38,6 +43,7 @@ pub fn with_exe_suffix(name: &str) -> String { } } +/// Default relative binary path: `./target[/]//`. pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String { let mut p = PathBuf::from("./target"); if let Some(t) = triple_segment { @@ -46,6 +52,7 @@ pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) p.join(profile).join(name).to_string_lossy().into_owned() } +/// Default relative library path: `./target[/]//`. pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String { let mut p = PathBuf::from("./target"); if let Some(t) = triple_segment { diff --git a/src/platform/posix.rs b/src/platform/posix.rs index d6f9778..b689ce3 100644 --- a/src/platform/posix.rs +++ b/src/platform/posix.rs @@ -1,5 +1,9 @@ +/// POSIX platform-specific path utilities. +/// Provides functions to build paths for binaries, libraries, and EFI files based on profile and optional target directory. use super::path_util::{default_bin_rel, default_lib_rel, join_dir}; +/// Returns the path to a binary executable for the given profile, name, and optional target directory. +/// If a target directory is provided, it joins the directory with the name; otherwise, it uses the default binary relative path. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => join_dir(dir, name), @@ -7,6 +11,8 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to a static library for the given profile, name, and optional target directory. +/// The library file is named "lib{name}.a". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path. pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.a"); match target_dir { @@ -15,10 +21,14 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to an ELF binary for the given profile, name, and optional target directory. +/// Delegates to bin_path since ELF binaries are standard binaries. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { bin_path(profile, name, target_dir) } +/// Returns the path to an EFI executable for the given profile, name, and optional target directory. +/// The EFI file is named "{name}.efi". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path. pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("{name}.efi"); match target_dir { @@ -27,6 +37,8 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to a shared library for the given profile, name, and optional target directory. +/// The shared library file is named "lib{name}.so". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path. pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = format!("lib{name}.so"); match target_dir { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 532d937..ba007be 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -17,6 +17,7 @@ use super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix}; +/// Returns the path to the binary executable for the given profile, name, and optional target directory. pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { let file = with_exe_suffix(name); match target_dir { @@ -25,7 +26,10 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to the library file for the given profile, name, and optional target directory. +/// The library name is forced to end with ".lib". pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + // Ensure the library file name ends with ".lib" let file = if name.to_ascii_lowercase().ends_with(".lib") { name.to_string() } else { @@ -37,11 +41,15 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to the ELF binary for the given profile, name, and optional target directory. pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { bin_path(profile, name, target_dir) } +/// Returns the path to the EFI binary for the given profile, name, and optional target directory. +/// The binary name is forced to end with ".efi". pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + // Ensure the EFI file name ends with ".efi" let file = if name.to_ascii_lowercase().ends_with(".efi") { name.to_string() } else { @@ -53,7 +61,10 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +/// Returns the path to the shared library (DLL) for the given profile, name, and optional target directory. +/// The library name is forced to end with ".dll". pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + // Ensure the shared library file name ends with ".dll" let file = if name.to_ascii_lowercase().ends_with(".dll") { name.to_string() } else { diff --git a/src/utils/build.rs b/src/utils/build.rs index 38f6c17..9a4d0e8 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -19,6 +19,9 @@ use crate::core::build_config::Config; use crate::utils::log::warn; use std::path::{Path, PathBuf}; +/// Version information parsed from a version string. +/// +/// Contains the full version, major, minor, patch, suffix, and suffix_dash components. pub struct VersionInfo { pub full: String, pub major: String, @@ -28,6 +31,7 @@ pub struct VersionInfo { pub suffix_dash: String, } +/// Parses a version string into VersionInfo struct. pub fn parse_version_info(version: &str) -> VersionInfo { let mut full = version.trim().to_string(); if full.is_empty() { @@ -56,6 +60,7 @@ pub fn parse_version_info(version: &str) -> VersionInfo { } } +/// Substitutes profile, name, and version variables into a template string. pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String { let s = template .replace("{profile}", profile) @@ -63,6 +68,7 @@ pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: substitute_version_vars(&s, info) } +/// Substitutes version-specific variables into a template string. pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String { template .replace("{version}", &info.full) @@ -73,6 +79,7 @@ pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String { .replace("{version_suffix_dash}", &info.suffix_dash) } +/// Normalizes a target string to a relative path for artifact output directory. pub fn normalize_target(target: &str, profile: &str) -> Option { let trimmed = normalize_target_os(target.trim()); if trimmed.is_empty() { @@ -82,6 +89,7 @@ pub fn normalize_target(target: &str, profile: &str) -> Option { } } +/// Returns the native host target directory relative to project root for given profile. pub fn native_host_target_rel(profile: &str) -> PathBuf { if cfg!(any( target_os = "linux", @@ -98,6 +106,18 @@ pub fn native_host_target_rel(profile: &str) -> PathBuf { } } +/// Resolves the artifact output directory for a project/workspace build. +/// +/// # Parameters +/// - `project_root`: Package root. +/// - `workspace_root`: If set, artifacts go under the workspace `target/`. +/// - `profile`: Build profile directory segment. +/// - `build_target`: Target triple or short name (may be empty). +/// - `out_dir`: Explicit `out_dir` from config (wins when non-empty). +/// - `has_explicit_target`: Whether CLI/config set a target (changes layout). +/// +/// # Returns +/// Absolute directory path as a string. pub fn resolve_artifact_target_dir( project_root: &Path, workspace_root: Option<&Path>, @@ -145,19 +165,35 @@ pub fn resolve_artifact_target_dir( project_root.join(base).to_string_lossy().into_owned() } +/// Normalizes kind string to standard value, defaulting to "bin" if empty. pub fn normalize_kind(kind: &str) -> &str { let trimmed = kind.trim(); if trimmed.is_empty() { "bin" } else { trimmed } } +/// Checks if the kind is a flat binary artifact. pub fn is_flat_bin(kind: &str) -> bool { kind == "flat-bin" } +/// Returns true for kinds that skip the normal link/archive step: `none`, `custom`, or `flat-bin`. +/// +/// Callers that still need a flat-binary path use [`is_flat_bin`] separately +/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before an early return). +/// +/// # Parameters +/// - `kind`: Normalized artifact kind string. +/// +/// # Returns +/// `true` for `none`, `custom`, or `flat-bin`. +/// +/// Callers that still need a flat-binary link use [`is_flat_bin`] separately +/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before early return). pub fn is_compile_only(kind: &str) -> bool { matches!(kind, "none" | "custom" | "flat-bin") } +/// Normalizes platform string, returning None if empty. pub fn normalize_platform(platform: &str) -> Option<&str> { let trimmed = platform.trim(); if trimmed.is_empty() { @@ -167,6 +203,13 @@ pub fn normalize_platform(platform: &str) -> Option<&str> { } } +/// Expands short OS names (`linux`/`macos`/`windows`) to default triples; leaves others as-is. +/// +/// # Parameters +/// - `target`: Short name, full triple, or empty. +/// +/// # Returns +/// Canonical triple string, empty input unchanged, unknown bare names returned with a warning. pub fn normalize_target_os(target: &str) -> &str { match target { "" => "", @@ -184,6 +227,7 @@ pub fn normalize_target_os(target: &str) -> &str { } } +/// Returns the default target triple for the current host platform. pub fn default_target_triple() -> String { let arch = std::env::consts::ARCH; if cfg!(target_os = "linux") { @@ -214,6 +258,7 @@ pub fn default_target_triple() -> String { } } +/// Prepends a --target flag to flags vector if using Clang and not already present. pub fn prepend_clang_target_flag(flags: &mut Vec, target: Option<&str>, tool: &str) { let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else { return; @@ -231,6 +276,7 @@ pub fn prepend_clang_target_flag(flags: &mut Vec, target: Option<&str>, } } +/// Retrieves a string value from config, falling back to empty string if missing. pub fn get_config_str(config: &Config, key: &str) -> String { config .get(key) @@ -239,6 +285,7 @@ pub fn get_config_str(config: &Config, key: &str) -> String { .to_string() } +/// Retrieves profile-specific table from config. pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> { config .get("build") @@ -247,6 +294,7 @@ pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml:: .and_then(|v| v.as_table()) } +/// Retrieves an optional string value from config. pub fn get_config_opt(config: &Config, key: &str) -> Option { let value = config.get(key)?.as_str()?; let trimmed = value.trim(); @@ -257,6 +305,7 @@ pub fn get_config_opt(config: &Config, key: &str) -> Option { } } +/// Retrieves string value with profile override support. pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String { let base = get_config_str(config, &format!("build.{field}")); let Some(table) = profile_table(config, profile) else { @@ -271,6 +320,7 @@ pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> S } } +/// Retrieves list value from config. pub fn get_config_list(config: &Config, key: &str) -> Vec { config .get(key) @@ -284,6 +334,7 @@ pub fn get_config_list(config: &Config, key: &str) -> Vec { .unwrap_or_default() } +/// Retrieves list value with profile override support. pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec { let mut out = get_config_list(config, &format!("build.{field}")); if let Some(table) = profile_table(config, profile) @@ -299,6 +350,7 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec out } +/// Helper function to check if a string contains a pattern ignoring case. fn contains_ignore_case(text: &str, pattern: &str) -> bool { if pattern.len() > text.len() { return false; @@ -308,6 +360,7 @@ fn contains_ignore_case(text: &str, pattern: &str) -> bool { .any(|w| w.eq_ignore_ascii_case(pattern.as_bytes())) } +/// Checks if target is a bare-metal or embedded target. pub fn is_bare_metal_target(target: Option<&str>) -> bool { match target { Some(t) => { @@ -321,6 +374,7 @@ pub fn is_bare_metal_target(target: Option<&str>) -> bool { } } +/// Returns default flags for a given profile. pub fn default_profile_flags(profile: &str) -> &'static [&'static str] { match profile { "release" => &["-O3", "-DNDEBUG"], @@ -336,6 +390,7 @@ pub fn default_profile_flags(profile: &str) -> &'static [&'static str] { } } +/// Retrieves boolean value with profile override support. pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool { let base = config .get(&format!("build.{field}")) @@ -345,6 +400,7 @@ pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, defaul profile_val.or(base).unwrap_or(default) } +/// Retrieves language from config with profile support. pub fn get_language_with_profile(config: &Config, profile: &str) -> Result { if let Some(table) = profile_table(config, profile) && let Some(value) = table.get("language") @@ -357,10 +413,12 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result String { get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string()) } +/// Parses language value from config, supporting string or array format. pub fn parse_language_value(value: &toml::Value, key: &str) -> Result { if let Some(s) = value.as_str() { let trimmed = s.trim(); @@ -389,6 +447,7 @@ pub fn parse_language_value(value: &toml::Value, key: &str) -> Result Option { if let Ok(value) = std::env::var("DCR_COMPILER") { let trimmed = value.trim(); @@ -440,6 +500,7 @@ fn env_override_compiler(lang: &str) -> Option { None } +/// Checks toolchain overrides for compiler. fn toolchain_override_compiler( lang: &str, tc_cc: Option<&str>, @@ -458,6 +519,7 @@ fn toolchain_override_compiler( tc_cc.map(|v| v.to_string()) } +/// Maps assembler compiler name to canonical form. fn map_asm_compiler(compiler: &str) -> String { match compiler.to_lowercase().as_str() { "gas" | "gnu-as" => "as".to_string(), @@ -468,6 +530,7 @@ fn map_asm_compiler(compiler: &str) -> String { } } +/// Determines primary language from comma-separated list. pub fn primary_language(language: &str) -> String { let tokens: Vec<&str> = language .split(',') @@ -486,6 +549,7 @@ pub fn primary_language(language: &str) -> String { language.to_lowercase() } +/// Resolves tool from environment variable or fallback. pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option { if let Ok(value) = std::env::var(env_key) { let trimmed = value.trim(); @@ -496,6 +560,7 @@ pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option { fallback.map(|v| v.to_string()) } +/// Resolves pkg-config flags for packages. pub fn resolve_pkg_config_flags( pkgs: &[String], base_cflags: &[String], @@ -512,6 +577,7 @@ pub fn resolve_pkg_config_flags( Ok((cflags, ldflags)) } +/// Resolves pkg-config flags, falling back on error. pub fn resolve_pkg_config_flags_lossy( pkgs: &[String], base_cflags: &[String], @@ -526,6 +592,7 @@ pub fn resolve_pkg_config_flags_lossy( } } +/// Runs pkg-config command and returns output. pub fn run_pkg_config(pkg: &str, arg: &str) -> Result { let output = std::process::Command::new("pkg-config") .arg(arg) @@ -539,6 +606,7 @@ pub fn run_pkg_config(pkg: &str, arg: &str) -> Result { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } +// The split_flags function parses pkg-config output, handling quoted strings and whitespace to split into individual flags. fn split_flags(value: &str) -> Vec { let mut out = Vec::new(); let mut current = String::new(); @@ -578,6 +646,7 @@ fn split_flags(value: &str) -> Vec { mod tests { use super::*; + /// Test for normalize_target_short_names function. #[test] fn normalize_target_short_names() { assert_eq!(normalize_target_os(""), ""); @@ -591,6 +660,7 @@ mod tests { assert_eq!(normalize_target_os("unknown"), "unknown"); } + /// Test for default_target_triple_uses_host_arch function. #[test] fn default_target_triple_uses_host_arch() { let target = default_target_triple(); @@ -635,6 +705,7 @@ mod tests { } } + /// Test for resolve_artifact_target_dir_native_and_workspace function. #[test] fn resolve_artifact_target_dir_native_and_workspace() { let root = Path::new("/proj"); @@ -655,6 +726,7 @@ mod tests { assert!(out.contains("dist")); } + /// Test for prepend_clang_target_flag_adds_once_for_clang function. #[test] fn prepend_clang_target_flag_adds_once_for_clang() { let mut flags = vec!["-O2".to_string()]; @@ -669,6 +741,7 @@ mod tests { ); } + /// Test for prepend_clang_target_flag_ignores_non_clang_tools function. #[test] fn prepend_clang_target_flag_ignores_non_clang_tools() { let mut flags = vec!["-O2".to_string()]; @@ -676,6 +749,7 @@ mod tests { assert_eq!(flags, vec!["-O2".to_string()]); } + /// Test for parse_version_parts function. #[test] fn parse_version_parts() { let info = parse_version_info("1.2.3-beta"); @@ -687,6 +761,7 @@ mod tests { assert_eq!(info.suffix_dash, "-beta"); } + /// Test for normalize_target_with_profile function. #[test] fn normalize_target_with_profile() { let linux = normalize_target_os("linux"); @@ -701,6 +776,7 @@ mod tests { assert_eq!(normalize_target("", "debug"), None); } + /// Test for normalize_kind_empty_defaults_to_bin function. #[test] fn normalize_kind_empty_defaults_to_bin() { assert_eq!(normalize_kind(""), "bin"); @@ -709,6 +785,7 @@ mod tests { assert_eq!(normalize_kind(" elf "), "elf"); } + /// Test for normalize_platform_empty_is_none function. #[test] fn normalize_platform_empty_is_none() { assert_eq!(normalize_platform(""), None); @@ -717,6 +794,7 @@ mod tests { assert_eq!(normalize_platform(" efi "), Some("efi")); } + /// Test for default_profile_flags_by_profile function. #[test] fn default_profile_flags_by_profile() { assert!(default_profile_flags("debug").contains(&"-O0")); @@ -726,6 +804,7 @@ mod tests { assert!(default_profile_flags("unknown").is_empty()); } + /// Test for is_bare_metal_detects_embedded_targets function. #[test] fn is_bare_metal_detects_embedded_targets() { assert!(is_bare_metal_target(Some("aarch64-none-elf"))); diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 292ab46..a10972c 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -19,6 +19,10 @@ use std::io; use std::io::Write; use std::path::{Path, PathBuf}; +/// Resolves the path to the user's home directory across different operating systems. +/// +/// Checks the `HOME` environment variable (Unix-like systems) first, +/// then falls back to `USERPROFILE` (Windows). pub fn home_dir() -> Option { if let Ok(home) = std::env::var("HOME") { return Some(PathBuf::from(home)); @@ -29,6 +33,7 @@ pub fn home_dir() -> Option { None } +/// Converts a byte slice into a lowercase hexadecimal string. pub fn to_hex(bytes: &[u8]) -> String { let mut out = String::with_capacity(bytes.len() * 2); for b in bytes { @@ -37,6 +42,7 @@ pub fn to_hex(bytes: &[u8]) -> String { out } +/// Lists all entry names (files and directories) inside a target directory. #[allow(dead_code)] pub fn check_dir(dir: Option<&str>) -> io::Result> { let path: PathBuf = match dir { @@ -53,6 +59,7 @@ pub fn check_dir(dir: Option<&str>) -> io::Result> { Ok(items) } +/// Searches upwards from a starting path to locate the root directory containing `dcr.toml`. pub fn find_project_root(start: &Path) -> io::Result> { let mut current = start.to_path_buf(); loop { @@ -66,6 +73,15 @@ pub fn find_project_root(start: &Path) -> io::Result> { Ok(None) } +/// Executes a closure inside a specified directory and restores the original cwd afterwards. +/// +/// # Parameters +/// - `dir`: Directory to `cd` into for the duration of `f`. +/// - `f`: Work to run with that cwd (its `Result` is returned as-is). +/// +/// # Returns +/// The closure's result, or an error if getting/changing directory fails. +/// Cwd is restored even when `f` returns `Err`. pub fn with_dir(dir: &Path, f: F) -> Result where F: FnOnce() -> Result, @@ -73,10 +89,14 @@ where let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?; std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?; let result = f(); + // Restore previous working directory regardless of closure result. let _ = std::env::set_current_dir(prev); result } +/// Strips Windows verbatim/extended-length prefixes (e.g. `\\?\`, `\\.\`) and standardizes slashes. +/// +/// Normalize path separators for cross-platform safety and removes platform-specific extended prefixes. pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf { let s = path.to_string_lossy(); let mut cleaned = s.replace('\\', "/"); @@ -99,6 +119,9 @@ pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf { } } +/// Canonicalizes a path to its absolute form and cleans up Windows extended-length prefixes. +/// +/// Falls back to joining relative paths with the current working directory if canonicalization fails. pub fn canonicalize_path(path: &Path) -> PathBuf { let raw = std::fs::canonicalize(path).unwrap_or_else(|_| { if path.is_absolute() { @@ -112,6 +135,7 @@ pub fn canonicalize_path(path: &Path) -> PathBuf { strip_verbatim_prefix(raw) } +/// Resolves a path relative to a root directory and returns a canonicalized absolute path. pub fn absolute_join(root: &Path, path: &Path) -> PathBuf { let joined = if path.is_absolute() { path.to_path_buf() @@ -121,12 +145,21 @@ pub fn absolute_join(root: &Path, path: &Path) -> PathBuf { canonicalize_path(&joined) } +/// Ensures the `.dcr` internal metadata directory and its `ide` subfolder exist within a project root. pub fn ensure_dcr_dir(root: &Path) -> io::Result { let dcr = root.join(".dcr"); std::fs::create_dir_all(dcr.join("ide"))?; Ok(dcr) } +/// Writes `contents` via a `.tmp.` file then renames into place (best-effort replace). +/// +/// # Parameters +/// - `path`: Final destination path (parent dirs are created). +/// - `contents`: Bytes to write. +/// +/// # Returns +/// `Ok(())` after a successful rename (with fallbacks if the target already exists). pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; @@ -145,6 +178,7 @@ pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> { match std::fs::rename(&tmp, path) { Ok(()) => Ok(()), Err(e) => { + // Handle cross-device moves or existing file replacement fallbacks. if path.exists() { let _ = std::fs::remove_file(path); match std::fs::rename(&tmp, path) { @@ -162,6 +196,9 @@ pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> { } } +/// Ensures that `.gitignore` exists in the project directory and includes the `.dcr/` output rule. +/// +/// If `.gitignore` does not exist, creates one with standard `/target` and `.dcr/` entries. pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> { let path = project_dir.join(".gitignore"); if path.exists() { diff --git a/src/utils/git.rs b/src/utils/git.rs index 00b6cfe..fb0d790 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -1,6 +1,9 @@ use std::path::Path; use std::process::Command; +/// Checks if the `git` executable is available in the system's PATH. +/// +/// This function executes `git --version` and returns `true` if the command succeeds, `false` otherwise. pub fn is_git_available() -> bool { Command::new("git") .arg("--version") @@ -9,6 +12,7 @@ pub fn is_git_available() -> bool { .unwrap_or(false) } +/// Initializes a new Git repository at the specified path. pub fn git_init(path: &Path) -> Result<(), String> { if !is_git_available() { return Err("git executable not found in PATH".to_string()); diff --git a/src/utils/log.rs b/src/utils/log.rs index 8d3ad6d..5726cb8 100644 --- a/src/utils/log.rs +++ b/src/utils/log.rs @@ -17,6 +17,9 @@ use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored}; +/// Logs an error message to the standard error stream. +/// +/// The message is prefixed with a red "error" label for visibility. #[allow(dead_code)] pub fn error(msg: &str) { eprintln!("{}: {msg}", colored("error", BOLD_RED)); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1a955c3..f4c0ee2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -14,9 +14,19 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +// +/// Core utilities module providing access to build, filesystem, git, log, and text helpers. pub mod build; + +/// File system path utilities and operations. pub mod fs; + +/// Git version control system integration. pub mod git; + +/// Logging utilities for application events and debugging. pub mod log; + +/// Text processing and manipulation helpers. pub mod text; diff --git a/src/utils/text.rs b/src/utils/text.rs index f0a0ebf..74c4a18 100644 --- a/src/utils/text.rs +++ b/src/utils/text.rs @@ -38,11 +38,15 @@ pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m"; #[allow(dead_code)] pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m"; +/// Applies ANSI escape codes to format the message with the given style. +/// +/// This is a utility for colored output in terminal applications. #[allow(dead_code)] pub fn colored(msg: &str, style: &str) -> String { format!("{style}{msg}{RESET}") } +/// Prints `msg` to stdout with the given ANSI `style`. #[allow(dead_code)] pub fn printc(msg: &str, style: &str) { println!("{style}{msg}{RESET}");