10 Commits

Author SHA1 Message Date
kie
9ab7ce15db Merge pull request #6 from dexoron/feat-tree
feat: add dcr tree command
2026-05-15 12:44:56 +03:00
peoplemiau1
954f2870ba style: final formatting and clippy fixes for tree.rs 2026-05-15 12:39:28 +03:00
peoplemiau1
9a24a80d59 fix: address clippy warnings in tree.rs 2026-05-15 12:22:05 +03:00
peoplemiau1
a123f647c8 style: fix formatting in tree.rs 2026-05-15 12:10:09 +03:00
peoplemiau1
8f04a37eb0 feat: add dcr tree command to visualize dependencies 2026-05-15 12:06:41 +03:00
60b9c4f983 Merge pull request #5 from peoplemiau1/master
refactor: optimize parallel build and fix architectural issues
2026-05-14 21:35:36 +08:00
peoplemiau1
f5fd273e29 refactor: optimize parallel build and fix architectural issues 2026-05-13 19:35:43 +03:00
d6840fc7a2 Merge pull request #4 from peoplemiau1/master
feat: parallel compilation for all builders
2026-05-14 00:02:02 +08:00
peoplemiau1
f507148849 style: format code according to project standards 2026-05-13 18:59:59 +03:00
peoplemiau1
cfc6be1e95 feat: parallel compilation for all builders 2026-05-13 18:50:42 +03:00
9 changed files with 439 additions and 183 deletions

View File

@@ -12,6 +12,7 @@ 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!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");

View File

@@ -10,3 +10,4 @@ pub mod new;
pub mod run;
pub mod setup;
pub mod test;
pub mod tree;

118
src/cli/tree.rs Normal file
View File

@@ -0,0 +1,118 @@
use crate::core::config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
pub fn tree(_args: &[String]) -> i32 {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let config = match Config::open("dcr.toml") {
Ok(c) => c,
Err(_) => {
error("dcr.toml not found in current directory");
return 1;
}
};
let name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
printc(&format!("{} v{}", name, version), BOLD_CYAN);
let mut seen = HashSet::new();
seen.insert(name.to_string());
if let Some(deps) = config.get("dependencies").and_then(|v| v.as_table()) {
print_deps(deps, &current_dir, "", &mut seen);
}
0
}
fn print_deps(
deps: &toml::value::Table,
base_path: &Path,
prefix: &str,
seen: &mut HashSet<String>,
) {
let mut dep_list: Vec<_> = deps.iter().collect();
dep_list.sort_by_key(|(name, _)| *name);
for (i, (name, value)) in dep_list.iter().enumerate() {
let is_last = i == dep_list.len() - 1;
let connector = if is_last { "└── " } else { "├── " };
let child_prefix = if is_last { " " } else { "│ " };
let dep_path = resolve_dep_path(value, base_path);
let mut version = String::new();
let mut sub_deps = None;
let mut resolved_path = None;
if let Some(path) = &dep_path {
let dcr_toml = path.join("dcr.toml");
if dcr_toml.exists()
&& let Ok(config) = Config::open(&dcr_toml.to_string_lossy())
{
if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) {
version = format!(" v{}", v);
}
sub_deps = config
.get("dependencies")
.and_then(|v| v.as_table())
.cloned();
resolved_path = Some(path.clone());
}
}
if version.is_empty() {
version = match value {
Value::String(s) if s.starts_with("git:") || s.starts_with("github:") => {
format!(" ({})", s)
}
Value::Table(t) => {
if let Some(v) = t.get("version").and_then(|v| v.as_str()) {
format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git)
} else {
"".to_string()
}
}
_ => "".to_string(),
};
}
println!("{}{}{}{}", prefix, connector, name, version);
if let Some(s_deps) = sub_deps
&& !seen.contains(*name)
{
seen.insert(name.to_string());
let new_prefix = format!("{}{}", prefix, child_prefix);
if let Some(path) = resolved_path {
print_deps(&s_deps, &path, &new_prefix, seen);
}
seen.remove(*name);
}
}
}
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
match value {
Value::String(s) => s
.strip_prefix("path:")
.map(|stripped| base_path.join(stripped)),
Value::Table(t) => t
.get("path")
.and_then(|v| v.as_str())
.map(|path| base_path.join(path)),
_ => None,
}
}

View File

@@ -153,6 +153,49 @@ pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F) -> Result<(), String>
where
F: Fn(usize) -> Result<(), String> + Sync + Send,
{
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let counter = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None);
std::thread::scope(|s| {
for _ in 0..num_threads {
s.spawn(|| {
loop {
if err_msg.lock().unwrap().is_some() {
break;
}
let i = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if i >= total_tasks {
break;
}
if let Err(e) = task_fn(i) {
let mut err = err_msg.lock().unwrap();
if err.is_none() {
*err = Some(e);
}
break;
}
}
});
}
});
if let Some(err) = err_msg.into_inner().unwrap() {
return Err(err);
}
Ok(())
}
pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();

View File

@@ -107,25 +107,42 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(&obj_path);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
Ok(status) if status.success() => Ok(()),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}

View File

@@ -185,6 +185,8 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
}
}
static OUTPUT_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn build_objects(
compiler: &str,
sources: &[String],
@@ -192,15 +194,35 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
match ctx.language.to_lowercase().as_str() {
"c" => {
cmd.arg("/TC");
@@ -208,40 +230,46 @@ fn build_objects(
"c++" | "cpp" | "cxx" => {
cmd.arg("/TP");
}
_ => {
return Err("Unsupported language".to_string());
}
_ => return Err("Unsupported language".to_string()),
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("/I{dir}"));
}
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
cmd.arg("/showIncludes");
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut headers = Vec::new();
let mut clean_stdout = String::new();
for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string());
@@ -254,34 +282,36 @@ fn build_objects(
}
}
let _lock = OUTPUT_MUTEX.lock().unwrap();
if !output.status.success() {
eprint!("{}", clean_stdout);
eprint!("{}", stderr);
return Err("Build failed".to_string());
} else {
}
let trimmed_out = clean_stdout.trim();
let trimmed_err = stderr.trim();
let src_filename = Path::new(source)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("");
if !trimmed_out.is_empty() && trimmed_out != src_filename {
print!("{}", clean_stdout);
}
if !trimmed_err.is_empty() {
eprintln!("{}", trimmed_err);
}
}
let d_path = Path::new(&obj_path).with_extension("d");
let d_path = Path::new(obj_path).with_extension("d");
let mut d_content = format!("{}: \\\n", obj_path.replace('\\', "/"));
for h in headers {
let escaped = h.replace('\\', "/").replace(" ", "\\ ");
d_content.push_str(&format!(" {} \\\n", escaped));
}
std::fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
}
objects.push(obj_path);
}
Ok(objects)
fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
Ok(())
}

View File

@@ -107,36 +107,56 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(&obj_path);
.arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
Ok(status) if status.success() => Ok(()),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {

View File

@@ -132,55 +132,80 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
cmd.arg("-c").arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
let d_path = Path::new(&obj_path).with_extension("d");
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
Ok(status) if status.success() => Ok(()),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;

View File

@@ -22,6 +22,7 @@ fn main() {
"add" => cli::add::add(rest),
"build" => cli::build::build(rest),
"run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),