// DCR — Cargo-like C/C++ project manager. // // Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::prelude::*; use crate::cli::clean::clean; use crate::cli::flags::parse_build_run_flags; use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata}; use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path}; use crate::core::build::common; use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build}; use crate::core::build_config::Config; use crate::utils::build::{ get_config_opt, get_config_str, get_language_with_profile_or_default, get_string_with_profile, normalize_kind, resolve_artifact_target_dir, resolve_compiler, }; use crate::utils::cli_styles::{ BOLD_CYAN, BOLD_YELLOW, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST, Style, }; use crate::utils::fs::{canonicalize_path, find_project_root}; use crate::utils::log::sprintln; use std::io::IsTerminal; use std::path::Path; 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: Style) -> String { format!("{verb:<9}").style(style).to_string() } /// 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: Style, 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 { index, total, target, } => { common::finish_progress_line(); self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}")); } BuildEvent::ProjectStart { name, profile, target, } => { common::finish_progress_line(); self.line( "project", BOLD_CYAN, &format!("{name} ({profile}, {target})"), ); } BuildEvent::DepBuilding { name, version } => { common::finish_progress_line(); self.line("dep", BOLD_YELLOW, &format!("{name} v{version}")); } BuildEvent::DepReady { name, version, rebuilt, } => { common::finish_progress_line(); // Only announce deps that were actually rebuilt this run. if rebuilt { self.line("ready", SUCCESS_ST, &format!("{name} v{version}")); } } BuildEvent::Compiling { name, version } => { common::finish_progress_line(); let label = format!(" {} {} v{}", status("compile", SUCCESS_ST), 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}"); } } BuildEvent::Packing { path } => { common::finish_progress_line(); self.line("pack", BOLD_CYAN, path); } BuildEvent::Finished { secs } => { common::finish_progress_line(); self.line("done", SUCCESS_ST, &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}"); } else { print!("{text}"); } } } } } /// 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") { sprintln!(HELP_SECTION_TITLE_ST, "USAGE:"); sprintln!( HELP_EXAMPLES_ST, " dcr build [--debug | --release] [--target ] [--force] [--clean] [--verbose] [--print-artifact-path]" ); println!(); sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:"); println!(" Compiles the project. Default profile is --debug."); println!(); sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:"); println!(" --debug Build with debug profile (default)"); println!(" --release Build with release profile"); println!(" --target Cross-compile for the given target"); println!(" --force Force a full rebuild"); println!(" --clean Clean before building"); println!(" --verbose Print detailed build output"); println!(" --workspace Build a specific workspace member"); println!( " --print-artifact-path Print absolute main artifact path on stdout after success" ); 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 || { cancel_clone.store(true, std::sync::atomic::Ordering::SeqCst); }) .ok(); let start_dir = match std::env::current_dir() { Ok(dir) => dir, Err(_) => { error!("Failed to determine current directory"); return 1; } }; let root = match find_project_root(&start_dir) { Ok(Some(dir)) => canonicalize_path(&dir), Ok(None) => { error!("dcr.toml file not found"); return 1; } Err(_) => { error!("Failed to find project root"); return 1; } }; let mut flags = match parse_build_run_flags(args) { Ok(v) => v, 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()) && !config.is_workspace_only() { let bt = get_build_string_with_profile(&config, "target", &flags.profile); if !bt.is_empty() { flags.target = Some(bt); } } } if flags.clean { let mut clean_args = Vec::new(); clean_args.push(format!("--{}", flags.profile)); let _ = clean(&clean_args); } let req = BuildRequest { profile: flags.profile.clone(), target: flags.target.clone(), force: flags.force, verbose: flags.verbose, workspace: flags.workspace.clone(), cancel, }; 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()) { let _ = write_dcr_metadata(&root, &info); if flags.print_artifact_path && let Some(ref path) = info.artifact_path { println!("{path}"); } } else if flags.print_artifact_path && let Some(path) = fallback_artifact_path(&root, &flags.profile, flags.target.as_deref()) { println!("{path}"); } 0 } Err(err) => { common::finish_progress_line(); error!("{}", err.message); 1 } } } /// 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, cli_target: Option<&str>, ) -> Option { let config = Config::open(root.join("dcr.toml").to_str()?).ok()?; if config.is_workspace_only() { return None; } let name = get_config_str(&config, "package.name"); let version = get_config_str(&config, "package.version"); let language = get_language_with_profile_or_default(&config, profile); let standard = get_string_with_profile(&config, "standard", profile); let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile); let compiler_s = get_string_with_profile(&config, "compiler", profile); let kind = normalize_kind(&get_string_with_profile(&config, "kind", profile)).to_string(); let build_target = cli_target .map(|s| s.to_string()) .filter(|s| !s.is_empty()) .unwrap_or_else(|| get_string_with_profile(&config, "target", profile)); let output_filename_s = get_string_with_profile(&config, "filename", profile); let output_extension_s = get_string_with_profile(&config, "extension", profile); let output_filename = if output_filename_s.is_empty() { None } else { Some(output_filename_s) }; let output_extension = if output_extension_s.is_empty() { None } else { Some(output_extension_s) }; let tc_cc = get_config_opt(&config, "toolchain.cc"); let tc_cxx = get_config_opt(&config, "toolchain.cxx"); let tc_as = get_config_opt(&config, "toolchain.as"); let resolved_compiler = resolve_compiler( &language, &compiler_s, tc_cc.as_deref(), tc_cxx.as_deref(), 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); let rel = resolve_artifact_path( &kind, profile, &name, Some(&target_dir), output_filename.as_deref(), output_extension.as_deref(), )?; let artifact_path = absolute_artifact_path(root, &rel) .to_string_lossy() .to_string(); let abs_target_dir = { let p = Path::new(&target_dir); let path = if p.is_absolute() { canonicalize_path(p) } else { canonicalize_path(&root.join(p)) }; path.to_string_lossy().into_owned() }; Some(ProjectInfo { name, version, root: root.to_path_buf(), profile: profile.to_string(), language, standard, cxx_standard, compiler: resolved_compiler, kind: kind.clone(), target: build_target, target_dir: Some(abs_target_dir), artifact_path: Some(artifact_path), artifact_kind: kind, sources: vec![], include_dirs: vec![], lib_dirs: vec![], libs: vec![], cflags: vec![], ldflags: vec![], source_roots: vec![], include_globs: vec![], exclude_globs: vec![], output_filename, output_extension, out_dir, debugger: "lldb".to_string(), moc: None, uic: None, rcc: None, workspace_root: None, }) } /// 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"); if name.is_empty() { return None; } let build_target = target.unwrap_or("").to_string(); let out_dir = get_string_with_profile(&config, "out_dir", profile); let has_explicit = !build_target.trim().is_empty(); let target_dir = resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit); let rel = resolve_artifact_path("bin", profile, &name, Some(&target_dir), None, None)?; Some( absolute_artifact_path(root, &rel) .to_string_lossy() .to_string(), ) }