mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 15:59:40 +00:00
432 lines
14 KiB
Rust
432 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::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::{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;
|
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
/// Retrieves the run command from the config, preferring target-specific,
|
|
/// then profile-specific, then the base `run.cmd`.
|
|
fn get_run_cmd(
|
|
config: &Config,
|
|
profile: &str,
|
|
target: Option<&str>,
|
|
version: &str,
|
|
) -> Option<String> {
|
|
let base = config.get("run.cmd").and_then(|v| v.as_str());
|
|
let target_cmd = if let Some(t) = target {
|
|
let normalized_t = normalize_target_os(t);
|
|
config
|
|
.get(&format!("run.{}.cmd", normalized_t))
|
|
.or_else(|| config.get(&format!("run.{}.cmd", t)))
|
|
.and_then(|v| v.as_str())
|
|
} else {
|
|
None
|
|
};
|
|
let profile_cmd = config
|
|
.get(&format!("run.{}.cmd", profile))
|
|
.and_then(|v| v.as_str());
|
|
let cmd = target_cmd.or(profile_cmd).or(base)?;
|
|
let trimmed = cmd.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(substitute_run_vars(trimmed, profile, version))
|
|
}
|
|
}
|
|
|
|
/// Handles the `dcr run` subcommand: parses flags, finds the project root,
|
|
/// builds if needed, and runs the resulting binary or configured command.
|
|
///
|
|
/// # Parameters
|
|
/// - `args`: Tokens after `dcr run` (build flags; after `--` → binary args).
|
|
///
|
|
/// # Returns
|
|
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
|
|
pub fn run(args: &[String]) -> i32 {
|
|
if args.first().is_some_and(|a| a == "--help") {
|
|
printc("USAGE:", BOLD_GREEN);
|
|
printc(
|
|
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
|
|
BOLD_CYAN,
|
|
);
|
|
println!();
|
|
printc("DESCRIPTION:", BOLD_GREEN);
|
|
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
|
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
|
|
println!();
|
|
printc("OPTIONS:", BOLD_GREEN);
|
|
println!(" --debug Run with debug profile (default)");
|
|
println!(" --release Run 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!(" -- <args>... Arguments forwarded to the binary");
|
|
return 0;
|
|
}
|
|
|
|
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)) => dir,
|
|
Ok(None) => {
|
|
error("dcr.toml file not found");
|
|
return 1;
|
|
}
|
|
Err(_) => {
|
|
error("Failed to find project root");
|
|
return 1;
|
|
}
|
|
};
|
|
let config = match with_dir(&root, || {
|
|
Config::open("./dcr.toml").map_err(|err| err.to_string())
|
|
}) {
|
|
Ok(cfg) => cfg,
|
|
Err(err) => {
|
|
error(&err);
|
|
return 1;
|
|
}
|
|
};
|
|
let flags = match parse_build_run_flags(args) {
|
|
Ok(v) => v,
|
|
Err(_) => return 1,
|
|
};
|
|
|
|
if config.is_workspace_only() {
|
|
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 {
|
|
let display = display_run_cmd(&cmd, &flags.bin_args);
|
|
println!(
|
|
" {} {}",
|
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
|
display
|
|
);
|
|
return run_shell_with_args(&cmd, &flags.bin_args);
|
|
}
|
|
return build_status;
|
|
}
|
|
|
|
let ws = match crate::core::workspace::parse_workspace(
|
|
&config,
|
|
&flags.profile,
|
|
flags.target.as_deref(),
|
|
&root,
|
|
) {
|
|
Ok(Some(ws)) => ws,
|
|
Ok(None) => {
|
|
error("Workspace root has no members defined");
|
|
return 1;
|
|
}
|
|
Err(e) => {
|
|
error(&e);
|
|
return 1;
|
|
}
|
|
};
|
|
let member = match &flags.workspace {
|
|
Some(name) => ws.members.iter().find(|m| m.name == *name),
|
|
None => ws.main_member(),
|
|
};
|
|
let member = match member {
|
|
Some(m) => m,
|
|
None => {
|
|
if let Some(name) = &flags.workspace {
|
|
error(&format!("Workspace member '{name}' not found"));
|
|
} else {
|
|
error("No workspace member to run (set `main = true` on one member)");
|
|
}
|
|
return 1;
|
|
}
|
|
};
|
|
// Build and run from the member's directory
|
|
return match with_dir(&member.path, || {
|
|
run_project(&member.path, &flags, Some(root.as_path()))
|
|
}) {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
error(&e);
|
|
1
|
|
}
|
|
};
|
|
}
|
|
|
|
match run_project(&root, &flags, None) {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
error(&e);
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builds and runs a single project at `root` (optionally as a workspace member).
|
|
fn run_project(
|
|
root: &Path,
|
|
flags: &crate::cli::flags::BuildRunFlags,
|
|
workspace_root: Option<&Path>,
|
|
) -> Result<i32, String> {
|
|
let config =
|
|
Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?;
|
|
|
|
let project_name: &str = config
|
|
.get("package.name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("");
|
|
|
|
let mut target = flags.target.clone();
|
|
if target.is_none() {
|
|
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
|
|
.get(&format!("build.{}.kind", flags.profile))
|
|
.and_then(|v| v.as_str())
|
|
.or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
|
|
.unwrap_or("");
|
|
|
|
let out_dir =
|
|
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
|
|
let build_target_str = target.clone().unwrap_or_default();
|
|
let has_explicit = target.as_ref().is_some_and(|t| !t.trim().is_empty());
|
|
|
|
let normalized_target_dir = Some(crate::utils::build::resolve_artifact_target_dir(
|
|
root,
|
|
workspace_root,
|
|
&flags.profile,
|
|
&build_target_str,
|
|
&out_dir,
|
|
has_explicit,
|
|
));
|
|
|
|
let version = config
|
|
.get("package.version")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("");
|
|
let run_cmd = get_run_cmd(&config, &flags.profile, target.as_deref(), version);
|
|
|
|
let kind = build_kind.trim();
|
|
if run_cmd.is_none()
|
|
&& (kind == "staticlib"
|
|
|| kind == "sharedlib"
|
|
|| kind == "efi"
|
|
|| kind == "elf"
|
|
|| kind == "flat-bin")
|
|
{
|
|
return Err("Cannot run library build".to_string());
|
|
}
|
|
|
|
let build_status = if let Some(wroot) = workspace_root {
|
|
with_dir(wroot, || {
|
|
Ok(build(&args_for_workspace_build(flags, project_name)))
|
|
})
|
|
.unwrap_or(1)
|
|
} else {
|
|
build(&args_for_build(flags))
|
|
};
|
|
let bin_path = crate::platform::bin_path(
|
|
&flags.profile,
|
|
project_name,
|
|
normalized_target_dir.as_deref(),
|
|
);
|
|
if build_status == 0 {
|
|
if let Some(cmd) = run_cmd {
|
|
let display = display_run_cmd(&cmd, &flags.bin_args);
|
|
println!(
|
|
" {} {}",
|
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
|
display
|
|
);
|
|
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
|
|
}
|
|
let display = display_bin_run(&bin_path, &flags.bin_args);
|
|
println!(
|
|
" {} {}",
|
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
|
display
|
|
);
|
|
return Ok(run_binary(
|
|
project_name,
|
|
&flags.profile,
|
|
normalized_target_dir.as_deref(),
|
|
&flags.bin_args,
|
|
));
|
|
}
|
|
|
|
let fallback_code = if let Some(cmd) = run_cmd {
|
|
run_shell_with_args(&cmd, &flags.bin_args)
|
|
} else {
|
|
run_binary(
|
|
project_name,
|
|
&flags.profile,
|
|
normalized_target_dir.as_deref(),
|
|
&flags.bin_args,
|
|
)
|
|
};
|
|
if fallback_code != 1 {
|
|
return Ok(fallback_code);
|
|
}
|
|
|
|
Err("Fix errors in the code to run the project".to_string())
|
|
}
|
|
|
|
/// Builds the list of arguments for the build command based on the provided flags.
|
|
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
|
let mut args = Vec::new();
|
|
args.push(format!("--{}", flags.profile));
|
|
if let Some(ref target) = flags.target {
|
|
args.push("--target".to_string());
|
|
args.push(target.clone());
|
|
}
|
|
if let Some(ref name) = flags.workspace {
|
|
args.push("--workspace".to_string());
|
|
args.push(name.clone());
|
|
}
|
|
if flags.force {
|
|
args.push("--force".to_string());
|
|
}
|
|
if flags.clean {
|
|
args.push("--clean".to_string());
|
|
}
|
|
if flags.verbose {
|
|
args.push("--verbose".to_string());
|
|
}
|
|
args
|
|
}
|
|
|
|
/// Constructs build arguments specifically for a workspace member.
|
|
fn args_for_workspace_build(
|
|
flags: &crate::cli::flags::BuildRunFlags,
|
|
member_name: &str,
|
|
) -> Vec<String> {
|
|
let mut args = Vec::new();
|
|
args.push(format!("--{}", flags.profile));
|
|
if let Some(ref target) = flags.target {
|
|
args.push("--target".to_string());
|
|
args.push(target.clone());
|
|
}
|
|
args.push("--workspace".to_string());
|
|
args.push(member_name.to_string());
|
|
if flags.force {
|
|
args.push("--force".to_string());
|
|
}
|
|
if flags.clean {
|
|
args.push("--clean".to_string());
|
|
}
|
|
if flags.verbose {
|
|
args.push("--verbose".to_string());
|
|
}
|
|
args
|
|
}
|
|
|
|
/// Executes the given shell command and returns its exit code.
|
|
fn run_shell(cmd: &str) -> i32 {
|
|
let status = if cfg!(target_os = "windows") {
|
|
Command::new("cmd").arg("/C").arg(cmd).status()
|
|
} else {
|
|
Command::new("sh").arg("-c").arg(cmd).status()
|
|
};
|
|
match status {
|
|
Ok(s) if s.success() => 0,
|
|
Ok(s) => s.code().unwrap_or(1),
|
|
Err(_) => 1,
|
|
}
|
|
}
|
|
|
|
/// Runs the command, appending escaped binary arguments if provided.
|
|
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
|
if bin_args.is_empty() {
|
|
return run_shell(cmd);
|
|
}
|
|
let mut full = cmd.to_string();
|
|
for arg in bin_args {
|
|
full.push(' ');
|
|
full.push_str(&shell_escape(arg));
|
|
}
|
|
run_shell(&full)
|
|
}
|
|
|
|
/// Formats the run command string for console output, appending arguments if any.
|
|
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
|
if bin_args.is_empty() {
|
|
return cmd.to_string();
|
|
}
|
|
let mut display = cmd.to_string();
|
|
for arg in bin_args {
|
|
display.push(' ');
|
|
display.push_str(arg);
|
|
}
|
|
display
|
|
}
|
|
|
|
/// Formats the binary path for console display, appending arguments if any.
|
|
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
|
if bin_args.is_empty() {
|
|
return bin_path.to_string();
|
|
}
|
|
let mut display = bin_path.to_string();
|
|
for arg in bin_args {
|
|
display.push(' ');
|
|
display.push_str(arg);
|
|
}
|
|
display
|
|
}
|
|
|
|
/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix.
|
|
fn shell_escape(arg: &str) -> String {
|
|
if cfg!(target_os = "windows") {
|
|
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
|
let escaped = arg.replace('"', "\\\"");
|
|
format!("\"{escaped}\"")
|
|
} else {
|
|
// POSIX sh: single-quote and escape embedded single quotes as '\''.
|
|
let mut out = String::from("'");
|
|
for ch in arg.chars() {
|
|
if ch == '\'' {
|
|
out.push_str("'\\''");
|
|
} else {
|
|
out.push(ch);
|
|
}
|
|
}
|
|
out.push('\'');
|
|
out
|
|
}
|
|
}
|
|
|
|
/// Replaces version info and profile in the command template.
|
|
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
|
let info = parse_version_info(version);
|
|
substitute_vars(cmd, &info, profile, "")
|
|
}
|