// 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 . /// 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, write_build_fingerprint, }; use crate::core::build::common; use crate::core::build::report::{ BuildError, BuildEvent, BuildOutcome, BuildReporter, BuildRequest, }; use crate::core::build::resolve::{ get_build_string_with_profile, get_config_value, get_lang_list, get_lang_string, get_list_with_profile_and_target, get_string_with_profile_and_target, get_targets, }; use crate::core::build::steps::{ StepVars, build_step_flags, build_steps_need_run, clean_generated_files, get_build_steps_with_profile, run_build_steps, verify_expectations, }; use crate::core::build_config::Config; use crate::core::deps::resolve_deps; use crate::core::workspace::parse_workspace; use crate::utils::build::{ default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform, normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language, resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, }; use crate::utils::fs::check_dir; use std::fs; use std::path::{Path, PathBuf}; 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], profile: &str, target_dir: Option, skip_local_target: bool, ) { if !skip_local_target && !items.contains(&"target".to_string()) { let _ = fs::create_dir_all(project_root.join("target")); } if let Some(dir) = &target_dir { let _ = fs::create_dir_all(dir); } else { let _ = fs::create_dir_all( project_root.join(crate::utils::build::native_host_target_rel(profile)), ); } } /// Runs the project build and returns the elapsed time in seconds or an error. fn run_compile( ctx: &BuildContext, _cancel: &Arc, _rep: &mut dyn BuildReporter, ) -> Result { let start_time = Instant::now(); let result = build_project(ctx); common::finish_progress_line(); match result { Ok(times) => { let times = if times == 0.0 { ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0 } else { times }; Ok(times) } Err(e) => Err(e), } } /// 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, rep: &mut dyn BuildReporter, ) -> Result { let secs = build_all( root, &req.profile, req.target.as_deref(), req.force, req.verbose, req.workspace.as_deref(), &req.cancel, rep, ) .map_err(BuildError::from)?; 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, profile: &str, target: Option<&str>, force: bool, verbose: bool, workspace: Option<&str>, cancel: &Arc, rep: &mut dyn BuildReporter, ) -> Result { let config = Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?; if config.is_workspace_only() { let ws = parse_workspace(&config, profile, target, root)? .ok_or_else(|| "Workspace root has no members defined".to_string())?; let members: Vec<_> = if let Some(filter_name) = workspace { ws.members .into_iter() .filter(|m| m.name == filter_name) .collect() } else { ws.members }; if members.is_empty() { return Err("No matching workspace members to build".to_string()); } for member in &members { build_project_at( &member.path, profile, target, target.is_some_and(|t| !t.trim().is_empty()), &[], force, verbose, Some(root), cancel, &mut *rep, )?; } if let Some(archive) = &config.typed().archive { #[cfg(feature = "archive")] { super::archive::pack_archive(root, archive, profile)?; } #[cfg(not(feature = "archive"))] { let _ = (archive, profile); return Err( "disk image packing requires building dcr with --features archive".to_string(), ); } } return Ok(0.0); } let project_name = get_config_str(&config, "package.name"); let project_version = get_config_str(&config, "package.version"); let targets_to_build: Vec<(Option, bool)> = if let Some(t) = target { let normalized = normalize_target_os(t); if normalized.is_empty() { vec![(Some(default_target_triple()), true)] } else { vec![(Some(normalized.to_string()), true)] } } else { let config_targets = get_targets(&config, profile)?; if config_targets.is_empty() { vec![(Some(default_target_triple()), false)] } else { config_targets .into_iter() .map(|t| { let normalized = normalize_target_os(&t); if normalized.is_empty() { (Some(default_target_triple()), true) } else { (Some(normalized.to_string()), true) } }) .collect() } }; let start_time = Instant::now(); for (i, (build_target, explicit_target)) in targets_to_build.iter().enumerate() { if cancel.load(Ordering::SeqCst) { return Err("Build interrupted".to_string()); } let target_label = build_target.as_ref().map_or("native", |t| t.as_str()); if targets_to_build.len() > 1 { rep.on_event(BuildEvent::TargetStart { index: i + 1, total: targets_to_build.len(), target: target_label, }); } else { rep.on_event(BuildEvent::ProjectStart { name: &project_name, profile, target: target_label, }); } if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? { if let Some(filter_name) = workspace { for member in ws.members.iter().filter(|m| m.name == filter_name) { build_project_at( &member.path, profile, build_target.as_deref(), *explicit_target, &[], force, verbose, Some(root), cancel, &mut *rep, )?; } } else { build_workspace( &ws, profile, build_target.as_deref(), *explicit_target, force, verbose, Some(root), cancel, &mut *rep, )?; let excludes: Vec = ws.members.iter().map(|m| m.path.clone()).collect(); build_project_at( root, profile, build_target.as_deref(), *explicit_target, &excludes, force, verbose, None, cancel, &mut *rep, )?; } } else { build_project_at( root, profile, build_target.as_deref(), *explicit_target, &[], force, verbose, None, cancel, &mut *rep, )?; } } let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; rep.on_event(BuildEvent::Finished { secs: elapsed }); 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, profile: &str, target: Option<&str>, explicit_target: bool, force: bool, verbose: bool, workspace_root: Option<&Path>, cancel: &Arc, rep: &mut dyn BuildReporter, ) -> Result<(), String> { for member in &workspace.members { build_project_at( &member.path, profile, target, explicit_target, &[], force, verbose, workspace_root, cancel, &mut *rep, )?; } 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, profile: &str, target: Option<&str>, explicit_target: bool, exclude_dirs: &[std::path::PathBuf], force: bool, verbose: bool, workspace_root: Option<&Path>, cancel: &Arc, rep: &mut dyn BuildReporter, ) -> Result<(), String> { let mut dependency_stack = Vec::new(); build_project_at_with_stack( project_root, profile, target, explicit_target, exclude_dirs, force, verbose, workspace_root, cancel, rep, &mut dependency_stack, ) } /// Builds a package while tracking the dependency chain to reject cycles. #[allow(clippy::too_many_arguments)] fn build_project_at_with_stack( project_root: &Path, profile: &str, target: Option<&str>, explicit_target: bool, exclude_dirs: &[std::path::PathBuf], force: bool, verbose: bool, workspace_root: Option<&Path>, cancel: &Arc, rep: &mut dyn BuildReporter, dependency_stack: &mut Vec, ) -> Result<(), String> { let root = project_root .canonicalize() .unwrap_or_else(|_| project_root.to_path_buf()); if let Some(index) = dependency_stack.iter().position(|item| item == &root) { let mut chain: Vec = dependency_stack[index..] .iter() .map(|item| item.display().to_string()) .collect(); chain.push(root.display().to_string()); return Err(format!("Dependency cycle detected: {}", chain.join(" -> "))); } dependency_stack.push(root); let result = build_project_at_inner( project_root, profile, target, explicit_target, exclude_dirs, force, verbose, workspace_root, cancel, rep, dependency_stack, ); dependency_stack.pop(); result } /// Builds one DCR package after the dependency-cycle guard has been installed. #[allow(clippy::too_many_arguments)] fn build_project_at_inner( project_root: &Path, profile: &str, target: Option<&str>, explicit_target: bool, exclude_dirs: &[std::path::PathBuf], force: bool, verbose: bool, workspace_root: Option<&Path>, cancel: &Arc, rep: &mut dyn BuildReporter, dependency_stack: &mut Vec, ) -> Result<(), String> { if cancel.load(Ordering::SeqCst) { return Err("Build interrupted".to_string()); } let items = check_dir(project_root.to_str()) .map_err(|_| "Failed to read project directory".to_string())?; if !items.contains(&"dcr.toml".to_string()) { return Err("dcr.toml file not found".to_string()); } let config_path = project_root.join("dcr.toml"); let mut config = Config::open(&config_path.to_string_lossy()).map_err(|err| err.to_string())?; // Inherit build fields from workspace root if configured if let Some(root) = workspace_root { let has_inherit = config .get("build.inherit") .and_then(|v| v.as_bool()) .unwrap_or(false); if has_inherit { let root_path = root.join("dcr.toml"); if root_path.is_file() { let parent_config = Config::open(&root_path.to_string_lossy()).map_err(|e| e.to_string())?; config.merge_parent(&parent_config); } } } let project_name = get_config_str(&config, "package.name"); let project_version = get_config_str(&config, "package.version"); let host_target = default_target_triple(); let build_target_config = get_build_string_with_profile(&config, "target", profile); let has_explicit_target = explicit_target || !build_target_config.is_empty(); let explicit_build_target = explicit_target .then_some(target) .flatten() .filter(|t| !t.trim().is_empty()) .map(|t| { let normalized = normalize_target_os(t); if normalized.is_empty() { host_target.clone() } else { normalized.to_string() } }); let configured_build_target = || { if build_target_config.is_empty() { None } else { let normalized = normalize_target_os(&build_target_config); if normalized.is_empty() { None } else { Some(normalized.to_string()) } } }; let implicit_build_target = || { target .filter(|t| !t.trim().is_empty()) .map(|t| normalize_target_os(t).to_string()) }; let build_target_owned = explicit_build_target .or_else(configured_build_target) .or_else(implicit_build_target) .unwrap_or(host_target); let build_target = Some(build_target_owned.as_str()); let build_language = get_language_with_profile(&config, profile)?; let build_qt = get_bool_with_profile(&config, "qt", profile, false); let lang_key = primary_language(&build_language); let project_compiler = get_lang_string(&config, &lang_key, "compiler", profile, build_target) .unwrap_or_else(|| { get_string_with_profile_and_target(&config, "compiler", profile, build_target) }); let build_standard = get_lang_string(&config, &lang_key, "standard", profile, build_target) .unwrap_or_else(|| { get_string_with_profile_and_target(&config, "standard", profile, build_target) }); let build_cxx_standard = get_lang_string(&config, &lang_key, "cxx_standard", profile, build_target).unwrap_or_else( || get_string_with_profile_and_target(&config, "cxx_standard", profile, build_target), ); let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target); let build_platform = get_string_with_profile_and_target(&config, "platform", profile, build_target); let mut build_type = get_string_with_profile_and_target(&config, "type", profile, build_target); if build_type.is_empty() { build_type = get_config_str(&config, "package.type"); } let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.cc")); let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.cxx")); let tc_as = get_config_value(&config, "toolchain", "as", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.as")); let tc_ar = get_config_value(&config, "toolchain", "ar", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.ar")); let tc_ld = get_config_value(&config, "toolchain", "ld", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.ld")); let tc_uic = get_config_value(&config, "toolchain", "uic", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.uic")); let tc_moc = get_config_value(&config, "toolchain", "moc", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.moc")); let tc_rcc = get_config_value(&config, "toolchain", "rcc", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.rcc")); let mut build_cflags = get_list_with_profile_and_target(&config, "cflags", profile, build_target)?; let mut build_ldflags = get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?; let build_ldscript = get_string_with_profile_and_target(&config, "ldscript", profile, build_target); if !build_ldscript.is_empty() { build_ldflags.push(format!("-T{}", build_ldscript)); } // Новые поля: filename и extension let output_filename = get_string_with_profile_and_target(&config, "filename", profile, build_target); let output_extension = get_string_with_profile_and_target(&config, "extension", profile, build_target); let build_excludes = get_list_with_profile_and_target(&config, "exclude", profile, build_target)?; let build_includes = get_list_with_profile_and_target(&config, "include", profile, build_target)?; let build_roots = get_list_with_profile_and_target(&config, "roots", profile, build_target)?; let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); let freestanding = get_bool_with_profile(&config, "freestanding", profile, false); let lto = get_bool_with_profile(&config, "lto", profile, false); let strip = get_bool_with_profile(&config, "strip", profile, false); let opt_level = get_string_with_profile_and_target(&config, "opt_level", profile, build_target); let debug = get_bool_with_profile(&config, "debug", profile, profile != "release"); let warnings = get_list_with_profile_and_target(&config, "warnings", profile, build_target)?; let panic_val = get_string_with_profile_and_target(&config, "panic", profile, build_target); let panic_abort = panic_val == "abort"; let codegen_units: usize = get_string_with_profile_and_target(&config, "codegen-units", profile, build_target) .parse() .unwrap_or(0); if build_cflags.is_empty() && !freestanding && !is_bare_metal_target(build_target) { if !opt_level.is_empty() { build_cflags.push(format!("-O{opt_level}")); } else { build_cflags.push(match profile { "release" => "-O3".to_string(), _ => "-O0".to_string(), }); } if debug { build_cflags.push("-g".to_string()); } if warnings.is_empty() { if profile == "debug" { build_cflags.push("-Wall".to_string()); build_cflags.push("-Wextra".to_string()); } } else { for w in &warnings { build_cflags.push(format!("-W{w}")); } } match profile { "debug" => { build_cflags.push("-fno-omit-frame-pointer".to_string()); build_cflags.push("-DDCR_DEBUG".to_string()); } "release" => { build_cflags.push("-DNDEBUG".to_string()); } _ => {} } } for flag in get_lang_list(&config, &lang_key, "flags", profile, build_target)? { build_cflags.push(flag); } if lto { build_cflags.push("-flto".to_string()); build_ldflags.push("-flto".to_string()); } if strip { build_ldflags.push("-s".to_string()); } let build_expects = get_list_with_profile_and_target(&config, "expect", profile, build_target)?; let pkg_configs = get_list_with_profile_and_target(&config, "pkg_config", profile, build_target)?; let build_generated = get_list_with_profile_and_target(&config, "generated", profile, build_target)?; let build_steps = get_build_steps_with_profile(&config, "steps", profile)?; let build_post_steps = get_build_steps_with_profile(&config, "post_steps", profile)?; let resolved_compiler = resolve_compiler( &build_language, &project_compiler, tc_cc.as_deref(), tc_cxx.as_deref(), tc_as.as_deref(), ); let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler); prepend_clang_target_flag( &mut build_ldflags, build_target, resolved_linker.as_deref().unwrap_or(&resolved_compiler), ); let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile); let target_dir_binding = resolve_artifact_target_dir( project_root, workspace_root, profile, &build_target_owned, &out_dir_config, has_explicit_target, ); let target_dir = if target_dir_binding.is_empty() { None } else { Some(target_dir_binding.clone()) }; ensure_target_dirs( project_root, &items, profile, target_dir, workspace_root.is_some(), ); let mut ws_include_dirs = Vec::new(); let mut ws_lib_dirs = Vec::new(); let mut ws_libs = Vec::new(); if let Some(wroot) = workspace_root { let wroot_toml = wroot.join("dcr.toml"); if wroot_toml.is_file() && let Ok(wroot_config) = Config::open(&wroot_toml.to_string_lossy()) && let Ok(Some(ws)) = crate::core::workspace::parse_workspace(&wroot_config, profile, build_target, wroot) { let current_abs = project_root .canonicalize() .unwrap_or_else(|_| project_root.to_path_buf()); if let Some(current_member) = ws .members .iter() .find(|m| m.path.canonicalize().unwrap_or_else(|_| m.path.clone()) == current_abs) { for dep_name in ¤t_member.deps { if let Some(dep_member) = ws.members.iter().find(|m| &m.name == dep_name) { let dep_inc = dep_member.path.join("include"); let dep_src = dep_member.path.join("src"); let dep_target_inc = dep_member.path.join("target").join("include"); let ws_target_inc = wroot.join("target").join("include"); if dep_inc.is_dir() { ws_include_dirs.push(dep_inc.to_string_lossy().to_string()); } if dep_src.is_dir() { ws_include_dirs.push(dep_src.to_string_lossy().to_string()); } if dep_target_inc.is_dir() { ws_include_dirs.push(dep_target_inc.to_string_lossy().to_string()); } if ws_target_inc.is_dir() { ws_include_dirs.push(ws_target_inc.to_string_lossy().to_string()); } let dep_target_lib = dep_member.path.join("target").join("lib"); if dep_target_lib.is_dir() { ws_lib_dirs.push(dep_target_lib.to_string_lossy().to_string()); } let target_str = build_target .map(normalize_target_os) .filter(|t| !t.is_empty()); let rel = match target_str { Some(ref t) => Path::new("target").join(t).join(profile), None => crate::utils::build::native_host_target_rel(profile), }; let dep_build_lib1 = dep_member.path.join(&rel); let dep_build_lib2 = wroot.join(&rel); if dep_build_lib1.is_dir() { ws_lib_dirs.push(dep_build_lib1.to_string_lossy().to_string()); } if dep_build_lib2.is_dir() { ws_lib_dirs.push(dep_build_lib2.to_string_lossy().to_string()); } let dep_toml = dep_member.path.join("dcr.toml"); let is_lib = if dep_toml.is_file() { if let Ok(dep_config) = Config::open(&dep_toml.to_string_lossy()) { let kind = get_build_string_with_profile(&dep_config, "kind", profile); kind == "staticlib" || kind == "sharedlib" } else { false } } else { false }; if is_lib { ws_libs.push(dep_member.name.clone()); } } } } } } let mut resolved = resolve_deps(&config, profile, build_target, project_root)?; resolved.include_dirs.extend(ws_include_dirs); resolved.lib_dirs.extend(ws_lib_dirs); resolved.libs.extend(ws_libs); let mut built_roots = std::collections::HashSet::new(); let mut transitive_roots = std::collections::HashSet::new(); let direct_package_roots = resolved.package_roots.clone(); for dep_root in &direct_package_roots { if !built_roots.insert(dep_root.clone()) { continue; } let dep_root = Path::new(dep_root); let dep_config = Config::open(&dep_root.join("dcr.toml").to_string_lossy()).map_err(|_| { format!( "DCR dependency is missing dcr.toml at {}", dep_root.display() ) })?; let name = get_config_str(&dep_config, "package.name"); let version = get_config_str(&dep_config, "package.version"); rep.on_event(BuildEvent::DepBuilding { name: &name, version: &version, }); build_project_at_with_stack( dep_root, profile, build_target, has_explicit_target, &[], force, verbose, None, cancel, &mut *rep, dependency_stack, )?; rep.on_event(BuildEvent::DepReady { name: &name, version: &version, rebuilt: true, }); extend_transitive_link_deps( dep_root, profile, build_target, &mut resolved, &mut transitive_roots, )?; } let (resolved_cflags, resolved_ldflags) = resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; let mut combined_excludes = Vec::new(); for dir in exclude_dirs { combined_excludes.push(dir.clone()); } let mut exclude_patterns = Vec::new(); for raw in build_excludes { let trimmed = raw.trim(); if trimmed.is_empty() { continue; } let normalized = trimmed.replace('\\', "/"); if common::has_glob_magic(&normalized) { exclude_patterns.push(normalized); continue; } let p = Path::new(trimmed); if p.is_absolute() { combined_excludes.push(p.to_path_buf()); exclude_patterns.push(normalized); } else { combined_excludes.push(project_root.join(p)); exclude_patterns.push(normalized); } } let mut combined_includes: Vec = Vec::new(); combined_includes.extend(exclude_patterns.iter().map(|v| format!("!{v}"))); combined_includes.extend(build_includes.iter().map(|v| v.replace('\\', "/"))); let mut source_roots: Vec = Vec::new(); for raw in &build_roots { let trimmed = raw.trim(); if trimmed.is_empty() { continue; } let p = Path::new(trimmed); if p.is_absolute() { source_roots.push(p.to_path_buf()); } else { source_roots.push(project_root.join(p)); } } if !src_disable && source_roots.is_empty() { source_roots.push(project_root.join("src")); } let mut merged_include_dirs = resolved.include_dirs.clone(); for raw in &build_includes { let trimmed = raw.trim(); if trimmed.is_empty() { continue; } let normalized = trimmed.replace('\\', "/"); if common::has_glob_magic(&normalized) { continue; } let p = Path::new(trimmed); let dir = if p.is_absolute() { p.to_path_buf() } else { project_root.join(p) }; if dir.is_dir() { merged_include_dirs.push(dir.to_string_lossy().to_string()); } } let version_info = parse_version_info(&project_version); let resolved_cflags: Vec = resolved_cflags .iter() .map(|f| substitute_vars(f, &version_info, profile, &project_name)) .map(|f| absolutize_flag_path(&f, project_root, &["-isystem", "-idirafter", "-I", "-i"])) .collect(); let modified_ldflags: Vec = resolved_ldflags .iter() .map(|f| substitute_vars(f, &version_info, profile, &project_name)) .map(|f| absolutize_flag_path(&f, project_root, &["-L", "-T"])) .collect(); let merged_include_dirs: Vec = merged_include_dirs .iter() .map(|f| substitute_vars(f, &version_info, profile, &project_name)) .collect(); let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs); let ctx = BuildContext { profile, project_name: &project_name, compiler: &resolved_compiler, language: &build_language, standard: &build_standard, cxx_standard: &build_cxx_standard, target: build_target, target_dir: if target_dir_binding.is_empty() { None } else { Some(target_dir_binding.as_str()) }, kind: normalize_kind(&build_kind), platform: normalize_platform(&build_platform), linker: resolved_linker.as_deref(), archiver: resolved_archiver.as_deref(), moc: Some(&tool_execs.moc), uic: Some(&tool_execs.uic), rcc: Some(&tool_execs.rcc), package_type: if build_type.is_empty() { None } else { Some(build_type.as_str()) }, freestanding, panic_abort, codegen_units, source_roots: &source_roots, exclude_dirs: &combined_excludes, include_paths: &combined_includes, include_dirs: &merged_include_dirs, lib_dirs: &resolved.lib_dirs, libs: &resolved.libs, cflags: &resolved_cflags, ldflags: &modified_ldflags, output_filename: if output_filename.is_empty() { None } else { Some(output_filename.as_str()) }, output_extension: if output_extension.is_empty() { None } else { Some(output_extension.as_str()) }, verbose, qt: build_qt, }; let step_flags = build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler); let step_vars = StepVars { profile, version: &version_info.full, version_major: &version_info.major, version_minor: &version_info.minor, version_patch: &version_info.patch, version_suffix: &version_info.suffix, version_suffix_dash: &version_info.suffix_dash, }; let mut steps_dirty = build_steps_need_run(&build_steps, &step_vars)?; if force { steps_dirty = true; } if steps_dirty { clean_generated_files(&build_generated)?; run_build_steps( &build_steps, &tool_execs, &step_flags, &step_vars, cancel, rep, )?; } let sources = collect_sources(&ctx)?; let headers = collect_header_files(&ctx, project_root)?; let lib_files = collect_lib_files(&ctx); let fingerprint = compute_build_fingerprint(&ctx, &sources, &headers, &lib_files)?; let mut skip = should_skip_build(&ctx, &fingerprint); let debug_enabled = std::env::var("DCR_DEBUG").is_ok(); if steps_dirty { skip = false; } if force { skip = false; } if skip && !debug_enabled { return Ok(()); } if !skip { rep.on_event(BuildEvent::Compiling { name: &project_name, version: &project_version, }); run_compile(&ctx, cancel, rep)?; if cancel.load(Ordering::SeqCst) { return Err("Build interrupted".to_string()); } write_build_fingerprint(&ctx, &fingerprint)?; } if ctx.package_type == Some("lib") || ctx.kind == "staticlib" || ctx.kind == "sharedlib" { package_library(&ctx, &headers, project_root)?; } let mut post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?; if force { post_steps_dirty = true; } if post_steps_dirty { run_build_steps( &build_post_steps, &tool_execs, &step_flags, &step_vars, cancel, rep, )?; } verify_expectations(&build_expects, &step_vars)?; if let Some(archive) = &config.typed().archive { let out = archive.output.replace("{profile}", profile); rep.on_event(BuildEvent::Packing { path: &out }); #[cfg(feature = "archive")] { super::archive::pack_archive(project_root, archive, profile)?; } #[cfg(not(feature = "archive"))] { let _ = (project_root, archive, profile, out); return Err( "disk image packing requires building dcr with --features archive".to_string(), ); } } Ok(()) } /// Adds link flags needed by dependencies of a static DCR library. /// Static archives do not contain their own dependency objects, so the final /// consumer must link every transitive package in dependency order. fn extend_transitive_link_deps( package_root: &Path, profile: &str, target: Option<&str>, consumer_deps: &mut crate::core::deps::common::ResolvedDeps, visited: &mut std::collections::HashSet, ) -> Result<(), String> { let root = package_root .canonicalize() .unwrap_or_else(|_| package_root.to_path_buf()); if !visited.insert(root.clone()) { return Ok(()); } let config = Config::open(&root.join("dcr.toml").to_string_lossy()).map_err(|err| err.to_string())?; let deps = resolve_deps(&config, profile, target, &root)?; consumer_deps.include_dirs.extend(deps.include_dirs); consumer_deps.lib_dirs.extend(deps.lib_dirs); consumer_deps.libs.extend(deps.libs); for child_root in deps.package_roots { extend_transitive_link_deps( Path::new(&child_root), profile, target, consumer_deps, visited, )?; } 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) { if rest.is_empty() { return flag.to_string(); } if rest.starts_with('/') || rest.starts_with('@') { return flag.to_string(); } if rest.len() >= 2 && rest.as_bytes()[1] == b':' { return flag.to_string(); } let abs = project_root.join(rest); let path = abs.to_string_lossy().replace('\\', "/"); return format!("{prefix}{path}"); } } 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() { if ancestor.file_name().and_then(|n| n.to_str()) == Some("target") { return ancestor.to_path_buf(); } } 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], project_root: &Path, ) -> Result<(), String> { let target_root = if let Some(dir) = ctx.target_dir { find_target_root(dir, &project_root.join("target")) } else { project_root.join("target") }; let include_dir = target_root.join("include"); let lib_dir = target_root.join("lib"); fs::create_dir_all(&include_dir).map_err(|e| e.to_string())?; fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?; for header in headers { if let Some(name) = header.file_name() { let dest = include_dir.join(name); fs::copy(header, dest) .map_err(|e| format!("Failed to copy header {:?}: {}", header, e))?; } } let mut outputs = Vec::new(); if ctx.kind == "staticlib" || ctx.kind == "any" { outputs.push(crate::platform::lib_path( ctx.profile, ctx.project_name, ctx.target_dir, )); } if ctx.kind == "sharedlib" || ctx.kind == "any" { outputs.push(crate::platform::shared_lib_path( ctx.profile, ctx.project_name, ctx.target_dir, )); } for output in outputs { let path = Path::new(&output); if path.exists() && let Some(name) = path.file_name() { let dest = lib_dir.join(name); fs::copy(path, dest).map_err(|e| format!("Failed to copy lib {:?}: {}", path, e))?; } } 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, rcc: &Option, pkg_configs: &[String], ) -> ToolchainExecs { let qt_bins = resolve_qt_host_bins(pkg_configs); ToolchainExecs { uic: resolve_qt_tool(uic, qt_bins.as_deref(), "uic"), moc: resolve_qt_tool(moc, qt_bins.as_deref(), "moc"), rcc: resolve_qt_tool(rcc, qt_bins.as_deref(), "rcc"), } } /// 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(); } if let Some(dir) = qt_bins { let candidate = dir.join(tool); if candidate.is_file() { return candidate.to_string_lossy().to_string(); } if cfg!(target_os = "windows") { let candidate = dir.join(format!("{tool}.exe")); if candidate.is_file() { return candidate.to_string_lossy().to_string(); } } } if let Some(candidate) = detect_qt6_tool_variant(tool) { return candidate; } 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() { return None; } let preferred = ["Qt6Core", "Qt6Widgets", "Qt6Gui"]; for name in preferred { if let Some(dir) = query_pkg_config_var(name, "host_bins") { return Some(dir); } if let Some(dir) = query_pkg_config_var(name, "libexecdir") && let Some(bin) = qt_bins_from_libexec(&dir) { return Some(bin); } if let Some(dir) = query_pkg_config_var(name, "bindir") { return Some(dir); } } for pkg in qt_pkgs { if let Some(dir) = query_pkg_config_var(pkg, "host_bins") { return Some(dir); } if let Some(dir) = query_pkg_config_var(pkg, "libexecdir") && let Some(bin) = qt_bins_from_libexec(&dir) { return Some(bin); } if let Some(dir) = query_pkg_config_var(pkg, "bindir") { return Some(dir); } } 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}")) .arg(pkg) .output() .ok()?; if !output.status.success() { return None; } let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); if value.is_empty() { return None; } let path = PathBuf::from(value); 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() { return Some(libexec.to_path_buf()); } if cfg!(target_os = "windows") && libexec.join(format!("{tool}.exe")).is_file() { return Some(libexec.to_path_buf()); } } let bin = libexec.join("bin"); 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; } std::env::var_os("PATH") .is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file())) } #[cfg(test)] mod tests { use super::absolutize_flag_path; use std::path::Path; #[test] fn absolutize_prefers_long_include_prefixes() { let root = Path::new("/pkg"); let prefs = ["-isystem", "-idirafter", "-I", "-i"]; assert_eq!( absolutize_flag_path("-isystemvendor/inc", root, &prefs), "-isystem/pkg/vendor/inc" ); assert_eq!( absolutize_flag_path("-idiraftervendor/old", root, &prefs), "-idirafter/pkg/vendor/old" ); assert_eq!( absolutize_flag_path("-I../inc", root, &prefs), "-I/pkg/../inc" ); assert_eq!( absolutize_flag_path("-i../inc", root, &prefs), "-i/pkg/../inc" ); assert_eq!( absolutize_flag_path("-I/usr/include", root, &prefs), "-I/usr/include" ); } }