From cb8a3b200152dc716b0ac5b35bd3f9c5b645d1df Mon Sep 17 00:00:00 2001 From: Dexoron Date: Mon, 23 Feb 2026 13:51:53 +0800 Subject: [PATCH] Custom output directory via `build.target` --- README.md | 1 + docs/dcr-toml.md | 3 +++ docs/index.md | 1 + docs/ru/README_ru.md | 1 + src/cli/build.rs | 19 +++++++++++++++++-- src/cli/run.rs | 35 ++++++++++++++++++++++++++--------- src/core/builder/clang.rs | 7 +++++-- src/core/builder/gcc.rs | 7 +++++-- src/core/builder/mod.rs | 1 + src/core/builder/msvc.rs | 2 +- src/core/config.rs | 11 +++++++---- src/platform/linux.rs | 7 +++++-- src/platform/macos.rs | 7 +++++-- src/platform/mod.rs | 13 ++++++++----- src/platform/windows.rs | 7 +++++-- 15 files changed, 91 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index e2c1d6d..d70cfab 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Two profiles are supported: - `--release` - built-in flags per compiler Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`. +You can also set `build.target` to override the output directory (profile-independent). ## Configuration The main project file is `dcr.toml`. diff --git a/docs/dcr-toml.md b/docs/dcr-toml.md index 1a81d8b..47bd088 100644 --- a/docs/dcr-toml.md +++ b/docs/dcr-toml.md @@ -13,6 +13,8 @@ version = "0.1.0" language = "c" standard = "c11" compiler = "gcc" +# Output directory for binary (optional) +target = "./dist" # Custom compile/link flags (optional) cflags = ["-D_POSIX_C_SOURCE=200809L", "-Wall", "-Wextra", "-O2", "-Ilib/frecli"] ldflags = ["-Llib/frecli", "-lfrecli", "-lm"] @@ -31,6 +33,7 @@ frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli" - `language` (string, required): `c`, `c++`, `cpp`, or `cxx` - `standard` (string, required): e.g. `c99`, `c11`, `c++17` - `compiler` (string, required): `gcc`, `clang`, `cl`(msvc), or `clang-cl` +- `target` (string, optional): output directory for the binary (profile-independent) - `cflags` (string[], optional): additional compile flags - `ldflags` (string[], optional): additional link flags diff --git a/docs/index.md b/docs/index.md index 435cb3d..7cc66e8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ You can add custom compile and link flags in `[build]`: [build] compiler = "gcc" standard = "c99" +target = "./dist" cflags = ["-D_POSIX_C_SOURCE=200809L", "-Wall", "-Wextra", "-O2", "-Ilib/frecli"] ldflags = ["-Llib/frecli", "-lfrecli", "-lm"] ``` diff --git a/docs/ru/README_ru.md b/docs/ru/README_ru.md index 825a508..425b71e 100644 --- a/docs/ru/README_ru.md +++ b/docs/ru/README_ru.md @@ -118,6 +118,7 @@ hello/ - `--release` — встроенные флаги для каждого компилятора Кастомные флаги можно добавить через `build.cflags` и `build.ldflags` в `dcr.toml`. +Можно указать `build.target`, чтобы задать директорию выхода бинарника (без привязки к профилю). ## Конфигурация Основной файл проекта — `dcr.toml`. diff --git a/src/cli/build.rs b/src/cli/build.rs index 5b55297..11b43ce 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -30,6 +30,7 @@ pub fn build(args: &[String]) -> i32 { let project_compiler = get_config_str(&config, "build.compiler"); let build_language = get_config_str(&config, "build.language"); let build_standard = get_config_str(&config, "build.standard"); + let build_target = get_config_str(&config, "build.target"); let build_cflags = match get_config_list(&config, "build.cflags") { Ok(v) => v, Err(msg) => { @@ -52,7 +53,7 @@ pub fn build(args: &[String]) -> i32 { colored(&project_compiler, BOLD_GREEN) ); - ensure_target_dirs(&items, &active_profile); + ensure_target_dirs(&items, &active_profile, normalize_target(&build_target)); let project_root = match std::env::current_dir() { Ok(p) => p, @@ -74,6 +75,7 @@ pub fn build(args: &[String]) -> i32 { compiler: &project_compiler, language: &build_language, standard: &build_standard, + target_dir: normalize_target(&build_target), include_dirs: &resolved.include_dirs, lib_dirs: &resolved.lib_dirs, libs: &resolved.libs, @@ -138,7 +140,11 @@ fn get_config_list(config: &Config, key: &str) -> Result, String> { Ok(out) } -fn ensure_target_dirs(items: &[String], profile: &str) { +fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<&str>) { + if let Some(dir) = target_dir { + let _ = fs::create_dir_all(dir); + return; + } if !items.contains(&"target".to_string()) { let _ = fs::create_dir("./target"); } @@ -162,3 +168,12 @@ fn run_build(ctx: &BuildContext) -> Result { Err(_) => Err("Build failed".to_string()), } } + +fn normalize_target(target: &str) -> Option<&str> { + let trimmed = target.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } +} diff --git a/src/cli/run.rs b/src/cli/run.rs index ff14163..70f7a1d 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,9 +1,11 @@ use crate::cli::build::build; use crate::config::{PROFILE, flags}; use crate::core::config::Config; +use crate::platform; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_GREEN, colored}; +use std::path::Path; use std::process::Command; pub fn run(args: &[String]) -> i32 { @@ -26,6 +28,11 @@ pub fn run(args: &[String]) -> i32 { .get("package.name") .and_then(|v| v.as_str()) .unwrap_or(""); + let target_dir = config + .get("build.target") + .and_then(|v| v.as_str()) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()); if let Some(first_arg) = args.first() { if first_arg.starts_with("--") { @@ -43,24 +50,34 @@ pub fn run(args: &[String]) -> i32 { } let build_status = build(args); + let bin_path = platform::bin_path(&active_profile, project_name, target_dir); if build_status == 0 { - println!( - "\n {} target/{active_profile}/{project_name}", - colored("Running", BOLD_GREEN) - ); + println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("--------------------------------"); - return Command::new(format!("./target/{active_profile}/{project_name}")) + return Command::new(bin_path) .status() .map(|status| status.code().unwrap_or(1)) .unwrap_or(1); } - if check_dir(Some(&format!("target/{active_profile}"))) - .unwrap_or_default() - .contains(&active_profile) + if target_dir.is_none() + && check_dir(Some(&format!("target/{active_profile}"))) + .unwrap_or_default() + .contains(&active_profile) { warn("Launch of the latest release"); - return Command::new(format!("./target/{active_profile}/{project_name}")) + return Command::new(platform::bin_path( + &active_profile, + project_name, + target_dir, + )) + .status() + .map(|status| status.code().unwrap_or(1)) + .unwrap_or(1); + } + if target_dir.is_some() && Path::new(&bin_path).exists() { + warn("Launch of the latest release"); + return Command::new(bin_path) .status() .map(|status| status.code().unwrap_or(1)) .unwrap_or(1); diff --git a/src/core/builder/clang.rs b/src/core/builder/clang.rs index 4c690a6..e38dfb3 100644 --- a/src/core/builder/clang.rs +++ b/src/core/builder/clang.rs @@ -37,8 +37,11 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in ctx.ldflags { cmd.arg(flag); } - cmd.arg("-o") - .arg(platform::bin_path(ctx.profile, ctx.project_name)); + cmd.arg("-o").arg(platform::bin_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); match cmd.status() { Ok(status) if status.success() => { diff --git a/src/core/builder/gcc.rs b/src/core/builder/gcc.rs index 4c690a6..e38dfb3 100644 --- a/src/core/builder/gcc.rs +++ b/src/core/builder/gcc.rs @@ -37,8 +37,11 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in ctx.ldflags { cmd.arg(flag); } - cmd.arg("-o") - .arg(platform::bin_path(ctx.profile, ctx.project_name)); + cmd.arg("-o").arg(platform::bin_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); match cmd.status() { Ok(status) if status.success() => { diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index 660b12c..1b03dba 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -8,6 +8,7 @@ pub struct BuildContext<'a> { pub compiler: &'a str, pub language: &'a str, pub standard: &'a str, + pub target_dir: Option<&'a str>, pub include_dirs: &'a [String], pub lib_dirs: &'a [String], pub libs: &'a [String], diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 3c46269..f6c18ff 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -63,7 +63,7 @@ pub fn build(ctx: &BuildContext) -> Result { } cmd.arg(format!( "/Fe:{}", - platform::bin_path(ctx.profile, ctx.project_name) + platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir) )); match cmd.status() { diff --git a/src/core/config.rs b/src/core/config.rs index dda2c1c..53cf4a4 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -216,16 +216,19 @@ fn format_toml(value: &Value) -> Result { out.push_str("[build]\n"); out.push_str(&format!("language = \"{language}\"\n")); out.push_str(&format!("standard = \"{standard}\"\n")); - out.push_str(&format!("compiler = \"{compiler}\"\n\n")); + out.push_str(&format!("compiler = \"{compiler}\"\n")); + if let Some(target) = build.get("target").and_then(|v| v.as_str()) { + if !target.trim().is_empty() { + out.push_str(&format!("target = \"{target}\"\n")); + } + } if let Some(cflags) = build.get("cflags") { out.push_str(&format!("cflags = {}\n", format_string_array(cflags))); } if let Some(ldflags) = build.get("ldflags") { out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags))); } - if build.contains_key("cflags") || build.contains_key("ldflags") { - out.push('\n'); - } + out.push('\n'); out.push_str("[dependencies]\n"); if !deps.is_empty() { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3746b47..439a5ad 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,3 +1,6 @@ -pub fn bin_path(profile: &str, name: &str) -> String { - format!("./target/{profile}/{name}") +pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}"), + } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3746b47..439a5ad 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,3 +1,6 @@ -pub fn bin_path(profile: &str, name: &str) -> String { - format!("./target/{profile}/{name}") +pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}"), + } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 839b760..e4b929b 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -5,21 +5,24 @@ pub mod macos; #[cfg(target_os = "windows")] pub mod windows; -pub fn bin_path(profile: &str, name: &str) -> String { +pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { - linux::bin_path(profile, name) + linux::bin_path(profile, name, target_dir) } #[cfg(target_os = "macos")] { - return macos::bin_path(profile, name); + return macos::bin_path(profile, name, target_dir); } #[cfg(target_os = "windows")] { - return windows::bin_path(profile, name); + return windows::bin_path(profile, name, target_dir); } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] { - format!("./target/{profile}/{name}") + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}"), + } } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c6e8eb0..5baf780 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,3 +1,6 @@ -pub fn bin_path(profile: &str, name: &str) -> String { - format!("./target/{profile}/{name}.exe") +pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}.exe"), + } }