feat(build): archive feature, progress UI, and workspace path fixes

This commit is contained in:
2026-07-23 13:56:36 +08:00
parent a9dd8c1631
commit 588e4dd13f
27 changed files with 597 additions and 276 deletions

View File

@@ -17,18 +17,29 @@
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build::common;
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
use crate::core::build_config::Config;
use crate::utils::build::default_target_triple;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::io::IsTerminal;
use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile;
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
}
struct CliReporter;
impl CliReporter {
fn line(&self, verb: &str, style: &str, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
impl BuildReporter for CliReporter {
fn on_event(&mut self, event: BuildEvent<'_>) {
match event {
@@ -37,73 +48,57 @@ impl BuildReporter for CliReporter {
total,
target,
} => {
println!(" Building target {} of {}: {}", index, total, target);
common::finish_progress_line();
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
}
BuildEvent::ProjectStart {
name,
profile,
target,
} => {
println!(
" Building project `{}`\n Profile: {}\n Target: {}",
colored(name, BOLD_GREEN),
colored(profile, BOLD_GREEN),
colored(target, BOLD_GREEN)
common::finish_progress_line();
self.line(
"project",
BOLD_CYAN,
&format!("{name} ({profile}, {target})"),
);
}
BuildEvent::DepBuilding { name, version } => {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Building", BOLD_GREEN),
name,
version
);
std::io::Write::flush(&mut std::io::stdout()).unwrap();
common::finish_progress_line();
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
}
BuildEvent::DepReady {
name,
version,
rebuilt,
} => {
common::finish_progress_line();
if rebuilt {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Ready", BOLD_GREEN),
name,
version
);
println!();
} else {
println!(
" {} {} v{}",
colored("Ready", BOLD_GREEN),
name,
version
);
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
}
}
BuildEvent::Compiling { name, version } => {
println!(
" {} {} v{}",
colored("Compiling", BOLD_GREEN),
name,
version
);
common::finish_progress_line();
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
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 } => {
println!(
" {} Build completed successfully in {} seconds",
colored("✔", BOLD_GREEN),
colored(&secs.to_string(), BOLD_GREEN)
);
common::finish_progress_line();
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
}
BuildEvent::CompilerOutput { stream, text } => {
common::interrupt_progress_for_output();
if stream == "stderr" {
eprintln!("{}", text);
eprint!("{text}");
} else {
println!("{}", text);
print!("{text}");
}
}
}
@@ -166,15 +161,12 @@ pub fn build(args: &[String]) -> i32 {
if let Ok(config) = Config::open(config_path.to_str().unwrap())
&& !config.is_workspace_only()
{
let bt = get_build_string_with_profile(&config, "target", "debug");
let bt = get_build_string_with_profile(&config, "target", &flags.profile);
if !bt.is_empty() {
flags.target = Some(bt);
}
}
}
if flags.target.is_none() {
flags.target = Some(default_target_triple());
}
if flags.clean {
let mut clean_args = Vec::new();
clean_args.push(format!("--{}", flags.profile));
@@ -193,6 +185,7 @@ pub fn build(args: &[String]) -> i32 {
match run_build(&root, &req, &mut reporter) {
Ok(_) => 0,
Err(err) => {
common::finish_progress_line();
error(&err.message);
1
}

View File

@@ -146,7 +146,10 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
)?
{
for member in &workspace.members {
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
let member_target = member.path.join("target");
if member_target.is_dir() {
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
}
}
}
clean_project_at(root, flags.profile.as_deref(), target.as_deref())

View File

@@ -29,9 +29,12 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)");
println!(" add <name> ... Add a dependency to dcr.toml");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format");
println!(" lint Run clang-tidy on C/C++ sources");
println!(" setup Show configured package registries");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN);

View File

@@ -85,7 +85,7 @@ pub fn new(args: &[String]) -> i32 {
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
));
printc("Подсказка:", BOLD_CYAN);
printc("Hint:", BOLD_CYAN);
println!(
" Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN)

View File

@@ -19,9 +19,7 @@ use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build_config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{
default_target_triple, normalize_target_os, parse_version_info, substitute_vars,
};
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
@@ -114,8 +112,11 @@ pub fn run(args: &[String]) -> i32 {
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
let build_status = build(&args_for_build(&flags));
if build_status == 0 {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return run_shell(&cmd);
}
return build_status;
@@ -186,10 +187,13 @@ fn run_project(
.and_then(|v| v.as_str())
.unwrap_or("");
// If no target specified, use default host target for target-specific config
let mut target = flags.target.clone();
if target.is_none() {
target = Some(default_target_triple());
let bt =
crate::cli::build::get_build_string_with_profile(&config, "target", &flags.profile);
if !bt.is_empty() {
target = Some(bt);
}
}
let build_kind = config
@@ -246,12 +250,18 @@ fn run_project(
);
if build_status == 0 {
if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return Ok(run_shell(&cmd));
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------");
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
bin_path
);
return Ok(run_binary(
project_name,
&flags.profile,

View File

@@ -130,7 +130,14 @@ pub(crate) fn link_flat_binary(
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
format!("linker not found: {program} (check [toolchain].ld / PATH)")
} else {
format!("flat-bin link failed: {err}")
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
@@ -240,7 +247,15 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64,
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
let msg = err.to_string();
if err.kind() == std::io::ErrorKind::NotFound {
format!("linker not found: {program} (check [toolchain].ld / PATH)")
} else {
format!("Build failed: {msg}")
}
})?;
if output.status.success() {
Ok(common::elapsed_secs(start_time))
} else {

View File

@@ -16,17 +16,72 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
pub fn get_output_lock() -> &'static Mutex<()> {
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
}
fn progress_label_lock() -> &'static Mutex<Option<String>> {
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
}
pub fn set_progress_label(label: Option<String>) {
*progress_label_lock().lock().unwrap() = label;
}
pub fn finish_progress_line() {
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
*progress_label_lock().lock().unwrap() = None;
if !was_dirty {
return;
}
if io::stderr().is_terminal() {
let _guard = get_output_lock().lock().unwrap();
let _ = writeln!(io::stderr());
let _ = io::stderr().flush();
}
}
pub fn interrupt_progress_for_output() {
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
return;
}
if io::stderr().is_terminal() {
let _guard = get_output_lock().lock().unwrap();
let _ = writeln!(io::stderr());
let _ = io::stderr().flush();
}
}
fn report_compile_progress(done: usize, total: usize) {
let label = progress_label_lock().lock().unwrap().clone();
let Some(label) = label else {
return;
};
if !io::stderr().is_terminal() {
return;
}
let _guard = get_output_lock().lock().unwrap();
let msg = format!("{label} [{done}/{total}]");
if PROGRESS_DIRTY.load(Ordering::SeqCst) {
eprint!("\r{msg:<80}");
} else {
eprint!("{msg}");
}
let _ = io::stderr().flush();
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
}
pub fn collect_sources(
roots: &[PathBuf],
extensions: &[&str],
@@ -214,8 +269,13 @@ where
};
let counter = std::sync::atomic::AtomicUsize::new(0);
let completed = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None);
if total_tasks > 0 {
report_compile_progress(0, total_tasks);
}
std::thread::scope(|s| {
for _ in 0..num_threads {
s.spawn(|| {
@@ -236,6 +296,9 @@ where
}
break;
}
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
report_compile_progress(done, total_tasks);
}
});
}
@@ -249,10 +312,21 @@ where
}
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let _guard = OUTPUT_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap();
let program = cmd.get_program().to_string_lossy().into_owned();
let output = cmd.output().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
format!("{program} not found (check PATH / [toolchain])")
} else {
format!("Build failed: {err}")
}
})?;
let has_out = !output.stdout.is_empty() || !output.stderr.is_empty();
if has_out {
interrupt_progress_for_output();
}
let _guard = get_output_lock().lock().unwrap();
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));

View File

@@ -90,7 +90,9 @@ fn run_compile(
_rep: &mut dyn BuildReporter,
) -> Result<f64, String> {
let start_time = Instant::now();
match build_project(ctx) {
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
@@ -166,7 +168,17 @@ fn build_all(
)?;
}
if let Some(archive) = &config.typed().archive {
super::archive::pack_archive(root, archive, profile)?;
#[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);
}
@@ -777,10 +789,12 @@ fn build_project_at(
let resolved_cflags: Vec<String> = 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<String> = 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<String> = merged_include_dirs
.iter()
@@ -876,11 +890,11 @@ fn build_project_at(
if skip && !debug_enabled {
return Ok(());
}
rep.on_event(BuildEvent::Compiling {
name: &project_name,
version: &project_version,
});
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());
@@ -906,10 +920,42 @@ fn build_project_at(
}
verify_expectations(&build_expects, &step_vars)?;
if let Some(archive) = &config.typed().archive {
super::archive::pack_archive(project_root, archive, profile)?;
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(())
}
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);
return format!("{prefix}{}", abs.to_string_lossy());
}
}
flag.to_string()
}
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
let p = Path::new(dir);
for ancestor in p.ancestors() {
@@ -1094,3 +1140,35 @@ fn is_on_path(cmd: &str) -> bool {
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"
);
}
}

View File

@@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[cfg(feature = "archive")]
pub mod archive;
pub mod builder;
pub mod cache;

View File

@@ -69,6 +69,9 @@ pub enum BuildEvent<'a> {
name: &'a str,
version: &'a str,
},
Packing {
path: &'a str,
},
CompilerOutput {
stream: &'a str,
text: &'a str,

View File

@@ -120,6 +120,7 @@ pub struct ArchiveConfig {
pub layout: Vec<ArchiveLayout>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct ArchiveLayout {
pub from: String,

View File

@@ -57,7 +57,7 @@ fn main() {
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
0
1
}
};

View File

@@ -15,27 +15,14 @@
// 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::utils::text::{BOLD_CYAN, colored};
use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
#[allow(dead_code)]
pub fn error(msg: &str) {
println!("{}: {msg}", colored("error", BOLD_CYAN),);
eprintln!("{}: {msg}", colored("error", BOLD_RED));
}
#[allow(dead_code)]
pub fn warn(msg: &str) {
println!("{}: {msg}", colored("warn", BOLD_CYAN),);
eprintln!("{}: {msg}", colored("warn", BOLD_YELLOW));
}
// #[allow(dead_code)]
// pub fn info(msg: &str) {
// println!("{}: {msg}", colored("info", BOLD));
// }
//
// #[allow(dead_code)]
// pub fn ok(msg: &str) {
// println!(
// "{}: {msg}",
// colored("ok", &(BRIGHT_GREEN.to_owned() + BOLD))
// );
// }