mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 12:58:34 +00:00
391 lines
14 KiB
Rust
391 lines
14 KiB
Rust
// DCR — Cargo-like C/C++ project manager.
|
|
//
|
|
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
|
//
|
|
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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 <triple>] [--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 <triple> 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 <name> 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<ProjectInfo> {
|
|
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<String> {
|
|
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(),
|
|
)
|
|
}
|