mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 14:38:30 +00:00
docs: add and clean up AI-generated rustdoc comments (grok-4.5)
This commit is contained in:
@@ -23,16 +23,38 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
|||||||
use toml::Value;
|
use toml::Value;
|
||||||
use toml::map::Map;
|
use toml::map::Map;
|
||||||
|
|
||||||
|
/// Parsed arguments for the `dcr add` command.
|
||||||
|
///
|
||||||
|
/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set,
|
||||||
|
/// depending on how the dependency source was specified.
|
||||||
pub struct AddArgs {
|
pub struct AddArgs {
|
||||||
|
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Local filesystem path when the source uses the `path:` prefix.
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
|
/// Git repository URL when the source is a git/GitHub/GitLab URL or prefix.
|
||||||
pub git: Option<String>,
|
pub git: Option<String>,
|
||||||
|
/// Optional git branch to pin the dependency to.
|
||||||
pub branch: Option<String>,
|
pub branch: Option<String>,
|
||||||
|
/// Optional git tag to pin the dependency to.
|
||||||
pub tag: Option<String>,
|
pub tag: Option<String>,
|
||||||
|
/// Optional git commit hash (revision) to pin the dependency to.
|
||||||
pub rev: Option<String>,
|
pub rev: Option<String>,
|
||||||
|
/// Version string resolved from the package registry when no explicit source is given.
|
||||||
pub version_from_registry: Option<String>,
|
pub version_from_registry: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a dependency to the current project's `dcr.toml`.
|
||||||
|
///
|
||||||
|
/// Parses CLI arguments, locates the project root, builds a TOML dependency value
|
||||||
|
/// (path, git with optional ref, or registry version), and writes it under
|
||||||
|
/// `dependencies.<name>`.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: Tokens after `dcr add` (name, source, optional `--branch`/`--tag`/`--rev`).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Process exit code: `0` on success, non-zero on usage or I/O errors.
|
||||||
pub fn add(args: &[String]) -> i32 {
|
pub fn add(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -91,6 +113,7 @@ pub fn add(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build the TOML value written under dependencies.<name>
|
||||||
let dep_value = if let Some(git_url) = add_args.git {
|
let dep_value = if let Some(git_url) = add_args.git {
|
||||||
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
|
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
|
||||||
let mut table = Map::new();
|
let mut table = Map::new();
|
||||||
@@ -135,6 +158,11 @@ pub fn add(args: &[String]) -> i32 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses CLI arguments into an [`AddArgs`] value.
|
||||||
|
///
|
||||||
|
/// Expects `<name> [source] [--branch|--tag|--rev ...]`. If `source` is omitted,
|
||||||
|
/// the package registry is queried for a default version. Source strings may use
|
||||||
|
/// `path:`, `github:`, `gitlab:`, or `git:` prefixes, or a full git URL.
|
||||||
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
||||||
@@ -168,7 +196,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
|||||||
}
|
}
|
||||||
source_spec = s.clone();
|
source_spec = s.clone();
|
||||||
} else {
|
} else {
|
||||||
// No source provided — try registry auto-lookup
|
// No source provided — resolve version from the package registry
|
||||||
match register::resolve_package_from_registry(&name) {
|
match register::resolve_package_from_registry(&name) {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
let version = info
|
let version = info
|
||||||
@@ -235,6 +263,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
|||||||
let mut path = None;
|
let mut path = None;
|
||||||
let mut git = None;
|
let mut git = None;
|
||||||
|
|
||||||
|
// Map source prefixes / bare URLs to path or git fields
|
||||||
if let Some(p) = source_spec.strip_prefix("path:") {
|
if let Some(p) = source_spec.strip_prefix("path:") {
|
||||||
path = Some(p.to_string());
|
path = Some(p.to_string());
|
||||||
} else if let Some(g) = source_spec.strip_prefix("github:") {
|
} else if let Some(g) = source_spec.strip_prefix("github:") {
|
||||||
@@ -243,10 +272,10 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
|||||||
git = Some(format!("https://gitlab.com/{}", g));
|
git = Some(format!("https://gitlab.com/{}", g));
|
||||||
} else if let Some(g) = source_spec.strip_prefix("git:") {
|
} else if let Some(g) = source_spec.strip_prefix("git:") {
|
||||||
if g.contains('/') && !g.contains('.') {
|
if g.contains('/') && !g.contains('.') {
|
||||||
// git:user/repo -> default to github
|
// git:user/repo with no host — default to GitHub
|
||||||
git = Some(format!("https://github.com/{}", g));
|
git = Some(format!("https://github.com/{}", g));
|
||||||
} else {
|
} else {
|
||||||
// git:host.com/user/repo
|
// git:host.com/user/repo or an already-qualified URL
|
||||||
let url = if g.starts_with("http") || g.starts_with("git@") {
|
let url = if g.starts_with("http") || g.starts_with("git@") {
|
||||||
g.to_string()
|
g.to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,19 +35,31 @@ use std::sync::{Arc, atomic::AtomicBool};
|
|||||||
|
|
||||||
pub use crate::core::build::get_build_string_with_profile;
|
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: &str) -> String {
|
fn status(verb: &str, style: &str) -> String {
|
||||||
colored(&format!("{verb:<9}"), style)
|
colored(&format!("{verb:<9}"), style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CLI build reporter that prints human-readable progress to stderr/stdout.
|
||||||
struct CliReporter;
|
struct CliReporter;
|
||||||
|
|
||||||
impl 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: &str, rest: &str) {
|
fn line(&self, verb: &str, style: &str, rest: &str) {
|
||||||
eprintln!(" {} {}", status(verb, style), rest);
|
eprintln!(" {} {}", status(verb, style), rest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildReporter for CliReporter {
|
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<'_>) {
|
fn on_event(&mut self, event: BuildEvent<'_>) {
|
||||||
match event {
|
match event {
|
||||||
BuildEvent::TargetStart {
|
BuildEvent::TargetStart {
|
||||||
@@ -80,6 +92,7 @@ impl BuildReporter for CliReporter {
|
|||||||
rebuilt,
|
rebuilt,
|
||||||
} => {
|
} => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
|
// Only announce deps that were actually rebuilt this run.
|
||||||
if rebuilt {
|
if rebuilt {
|
||||||
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
|
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
|
||||||
}
|
}
|
||||||
@@ -87,6 +100,7 @@ impl BuildReporter for CliReporter {
|
|||||||
BuildEvent::Compiling { name, version } => {
|
BuildEvent::Compiling { name, version } => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
|
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
|
||||||
|
// Terminals use an in-place progress line; non-TTY gets a plain log line.
|
||||||
common::set_progress_label(Some(label.clone()));
|
common::set_progress_label(Some(label.clone()));
|
||||||
if !std::io::stderr().is_terminal() {
|
if !std::io::stderr().is_terminal() {
|
||||||
eprintln!("{label}");
|
eprintln!("{label}");
|
||||||
@@ -101,6 +115,7 @@ impl BuildReporter for CliReporter {
|
|||||||
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
|
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
|
||||||
}
|
}
|
||||||
BuildEvent::CompilerOutput { stream, text } => {
|
BuildEvent::CompilerOutput { stream, text } => {
|
||||||
|
// Pause the progress spinner so compiler text is not overwritten.
|
||||||
common::interrupt_progress_for_output();
|
common::interrupt_progress_for_output();
|
||||||
if stream == "stderr" {
|
if stream == "stderr" {
|
||||||
eprint!("{text}");
|
eprint!("{text}");
|
||||||
@@ -112,6 +127,17 @@ impl BuildReporter for CliReporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn build(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -137,6 +163,7 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared cancel flag so Ctrl-C can stop an in-progress build cooperatively.
|
||||||
let cancel = Arc::new(AtomicBool::new(false));
|
let cancel = Arc::new(AtomicBool::new(false));
|
||||||
let cancel_clone = cancel.clone();
|
let cancel_clone = cancel.clone();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
@@ -166,6 +193,7 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prefer CLI --target; otherwise fall back to profile target from dcr.toml.
|
||||||
if flags.target.is_none() {
|
if flags.target.is_none() {
|
||||||
let config_path = root.join("dcr.toml");
|
let config_path = root.join("dcr.toml");
|
||||||
if let Ok(config) = Config::open(config_path.to_str().unwrap())
|
if let Ok(config) = Config::open(config_path.to_str().unwrap())
|
||||||
@@ -194,6 +222,7 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
let mut reporter = CliReporter;
|
let mut reporter = CliReporter;
|
||||||
match run_build(&root, &req, &mut reporter) {
|
match run_build(&root, &req, &mut reporter) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
// Persist IDE/metadata and optionally echo the main artifact path.
|
||||||
if let Some(info) =
|
if let Some(info) =
|
||||||
resolve_post_build_info(&root, &flags.profile, flags.target.as_deref())
|
resolve_post_build_info(&root, &flags.profile, flags.target.as_deref())
|
||||||
{
|
{
|
||||||
@@ -219,6 +248,19 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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(
|
fn resolve_post_build_info(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -262,6 +304,7 @@ fn resolve_post_build_info(
|
|||||||
tc_as.as_deref(),
|
tc_as.as_deref(),
|
||||||
);
|
);
|
||||||
let out_dir = get_string_with_profile(&config, "out_dir", profile);
|
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 has_explicit = cli_target.is_some_and(|t| !t.is_empty()) || !build_target.trim().is_empty();
|
||||||
let target_dir =
|
let target_dir =
|
||||||
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
||||||
@@ -319,6 +362,10 @@ fn resolve_post_build_info(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
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 config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||||
let name = get_config_str(&config, "package.name");
|
let name = get_config_str(&config, "package.name");
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ use glob::glob;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Entry point for the `dcr clean` command.
|
||||||
|
///
|
||||||
|
/// Removes build artifacts from the project (and optionally workspace members).
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: Tokens after `dcr clean` (`--debug`/`--release`, `--target`, `--all`).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Process exit code: `0` on success, `1` on failure.
|
||||||
pub fn clean(args: &[String]) -> i32 {
|
pub fn clean(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -70,6 +79,7 @@ pub fn clean(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Run cleanup relative to the project root so paths resolve consistently.
|
||||||
match with_dir(&root, || clean_from_root(&root, &flags)) {
|
match with_dir(&root, || clean_from_root(&root, &flags)) {
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
@@ -79,12 +89,20 @@ pub fn clean(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parsed flags controlling which artifacts `clean` should remove.
|
||||||
struct CleanFlags {
|
struct CleanFlags {
|
||||||
|
/// Optional build profile name (e.g. `debug`, `release`).
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
|
/// Optional target triple to clean under `target/<triple>/...`.
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
|
/// When true, also clean each workspace member that has a `target` directory.
|
||||||
all: bool,
|
all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses CLI arguments into [`CleanFlags`].
|
||||||
|
///
|
||||||
|
/// Recognizes `--all`, `--target <triple>`, and profile flags such as `--debug`
|
||||||
|
/// / `--release` (validated via [`default_profile_flags`]).
|
||||||
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||||
let mut profile: Option<String> = None;
|
let mut profile: Option<String> = None;
|
||||||
let mut target: Option<String> = None;
|
let mut target: Option<String> = None;
|
||||||
@@ -104,6 +122,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if arg.starts_with("--") {
|
if arg.starts_with("--") {
|
||||||
|
// Accept only known profile names (e.g. debug/release), not arbitrary flags.
|
||||||
let candidate = arg.trim_start_matches("--").to_string();
|
let candidate = arg.trim_start_matches("--").to_string();
|
||||||
if !default_profile_flags(&candidate).is_empty() {
|
if !default_profile_flags(&candidate).is_empty() {
|
||||||
if profile.is_some() {
|
if profile.is_some() {
|
||||||
@@ -122,9 +141,14 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cleans the project at `root`, and optionally all workspace members.
|
||||||
|
///
|
||||||
|
/// Resolves the effective target triple from flags, `dcr.toml`, or host defaults,
|
||||||
|
/// then removes the appropriate `target` subdirectories and custom clean paths.
|
||||||
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
// Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD.
|
||||||
let target = flags.target.clone().or_else(|| {
|
let target = flags.target.clone().or_else(|| {
|
||||||
config
|
config
|
||||||
.get("build.target")
|
.get("build.target")
|
||||||
@@ -165,6 +189,12 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
|||||||
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
|
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes build artifacts for a single project directory.
|
||||||
|
///
|
||||||
|
/// When `profile` is set, only that profile's directory under `target` (optionally
|
||||||
|
/// scoped by `target` triple) is removed, plus custom paths for that profile.
|
||||||
|
/// When `profile` is `None`, the entire `target` tree is removed and custom paths
|
||||||
|
/// are cleaned for both `debug` and `release`.
|
||||||
fn clean_project_at(
|
fn clean_project_at(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
profile: Option<&str>,
|
profile: Option<&str>,
|
||||||
@@ -192,6 +222,7 @@ fn clean_project_at(
|
|||||||
};
|
};
|
||||||
let target_items = check_dir(Some("target")).unwrap_or_default();
|
let target_items = check_dir(Some("target")).unwrap_or_default();
|
||||||
let parent_dir = target.unwrap_or("");
|
let parent_dir = target.unwrap_or("");
|
||||||
|
// Existence check differs: with a triple, look for target/<triple>; otherwise target/<profile>.
|
||||||
let dir_exists = if target.is_some() {
|
let dir_exists = if target.is_some() {
|
||||||
target_items.contains(&parent_dir.to_string())
|
target_items.contains(&parent_dir.to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -230,6 +261,10 @@ fn clean_project_at(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes extra paths listed under `build.clean` in `dcr.toml` for a profile.
|
||||||
|
///
|
||||||
|
/// Each pattern may contain `{profile}` and version placeholders, which are
|
||||||
|
/// expanded before matching with `glob`. Matched files and directories are removed.
|
||||||
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
||||||
let patterns = match config.get("build.clean") {
|
let patterns = match config.get("build.clean") {
|
||||||
Some(v) => v
|
Some(v) => v
|
||||||
@@ -260,6 +295,10 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands version-related placeholders in a clean path pattern.
|
||||||
|
///
|
||||||
|
/// Reads `package.version` from `config`, parses it, and applies
|
||||||
|
/// [`crate::utils::build::substitute_version_vars`].
|
||||||
fn substitute_clean_vars(value: &str, config: &Config) -> String {
|
fn substitute_clean_vars(value: &str, config: &Config) -> String {
|
||||||
let version = config
|
let version = config
|
||||||
.get("package.version")
|
.get("package.version")
|
||||||
|
|||||||
@@ -27,18 +27,32 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
|
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
|
||||||
|
|
||||||
|
/// Metadata for the latest GitHub release of DCR.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Release {
|
struct Release {
|
||||||
|
/// Release tag, typically including a leading `v` (e.g. `v1.2.3`).
|
||||||
tag_name: String,
|
tag_name: String,
|
||||||
|
/// Downloadable assets attached to the release.
|
||||||
assets: Vec<ReleaseAsset>,
|
assets: Vec<ReleaseAsset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single downloadable asset from a GitHub release.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ReleaseAsset {
|
struct ReleaseAsset {
|
||||||
|
/// Asset file name as published on GitHub.
|
||||||
name: String,
|
name: String,
|
||||||
|
/// Direct URL used to download the asset.
|
||||||
browser_download_url: String,
|
browser_download_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles the `dcr --update` flag: checks GitHub for a newer release and
|
||||||
|
/// replaces the running binary when an update is available.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: Remaining tokens after `--update` (only `--help` is accepted).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
|
||||||
pub fn flag_update(args: &[String]) -> i32 {
|
pub fn flag_update(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -62,6 +76,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Refuse self-update when the binary is owned by pacman/AUR.
|
||||||
if let Some(package_name) = pacman_owned_package(¤t_exe) {
|
if let Some(package_name) = pacman_owned_package(¤t_exe) {
|
||||||
warn("This dcr binary is managed by pacman/AUR");
|
warn("This dcr binary is managed by pacman/AUR");
|
||||||
println!(
|
println!(
|
||||||
@@ -81,6 +96,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize the tag by stripping a leading `v` before comparison.
|
||||||
let latest_version = release.tag_name.trim_start_matches('v');
|
let latest_version = release.tag_name.trim_start_matches('v');
|
||||||
if latest_version == current_version {
|
if latest_version == current_version {
|
||||||
println!("Latest version is already installed: {current_version}");
|
println!("Latest version is already installed: {current_version}");
|
||||||
@@ -113,6 +129,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
set_executable_permissions(&temp_path);
|
set_executable_permissions(&temp_path);
|
||||||
|
|
||||||
|
// Replace the running executable in place, then clean up the temp file.
|
||||||
if self_replace::self_replace(&temp_path).is_err() {
|
if self_replace::self_replace(&temp_path).is_err() {
|
||||||
let _ = fs::remove_file(&temp_path);
|
let _ = fs::remove_file(&temp_path);
|
||||||
error("Failed to replace current binary");
|
error("Failed to replace current binary");
|
||||||
@@ -124,6 +141,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the latest DCR release metadata from the GitHub Releases API.
|
||||||
fn fetch_latest_release() -> Result<Release, String> {
|
fn fetch_latest_release() -> Result<Release, String> {
|
||||||
let response = ureq::get(LATEST_RELEASE_URL)
|
let response = ureq::get(LATEST_RELEASE_URL)
|
||||||
.set("User-Agent", "dcr-updater")
|
.set("User-Agent", "dcr-updater")
|
||||||
@@ -138,6 +156,7 @@ fn fetch_latest_release() -> Result<Release, String> {
|
|||||||
.map_err(|_| "GitHub API response has an unexpected format".to_string())
|
.map_err(|_| "GitHub API response has an unexpected format".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Downloads a release asset from the given URL into memory.
|
||||||
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||||
let response = ureq::get(url)
|
let response = ureq::get(url)
|
||||||
.set("User-Agent", "dcr-updater")
|
.set("User-Agent", "dcr-updater")
|
||||||
@@ -155,9 +174,11 @@ fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the list of expected asset file names for the given build target.
|
||||||
fn asset_candidates(target: &str) -> Vec<String> {
|
fn asset_candidates(target: &str) -> Vec<String> {
|
||||||
let mut names = vec![format!("dcr-{target}")];
|
let mut names = vec![format!("dcr-{target}")];
|
||||||
|
|
||||||
|
// Windows releases may publish the binary with an `.exe` suffix.
|
||||||
if target.contains("-windows-") || target.ends_with("-windows") {
|
if target.contains("-windows-") || target.ends_with("-windows") {
|
||||||
names.push(format!("dcr-{target}.exe"));
|
names.push(format!("dcr-{target}.exe"));
|
||||||
}
|
}
|
||||||
@@ -165,6 +186,7 @@ fn asset_candidates(target: &str) -> Vec<String> {
|
|||||||
names
|
names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a unique temporary path next to the current executable for the download.
|
||||||
fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
||||||
let stamp = SystemTime::now()
|
let stamp = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -177,6 +199,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
|||||||
current_exe.with_extension(extension)
|
current_exe.with_extension(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets executable permissions on Unix; no-op on other platforms.
|
||||||
fn set_executable_permissions(_path: &Path) {
|
fn set_executable_permissions(_path: &Path) {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -189,6 +212,7 @@ fn set_executable_permissions(_path: &Path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// On Linux, returns the pacman package that owns `path`, if any.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn pacman_owned_package(path: &Path) -> Option<String> {
|
fn pacman_owned_package(path: &Path) -> Option<String> {
|
||||||
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
|
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
|
||||||
@@ -204,6 +228,7 @@ fn pacman_owned_package(path: &Path) -> Option<String> {
|
|||||||
Some(package_name)
|
Some(package_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Non-Linux stub: package-manager ownership is not checked.
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn pacman_owned_package(_path: &Path) -> Option<String> {
|
fn pacman_owned_package(_path: &Path) -> Option<String> {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -18,17 +18,36 @@
|
|||||||
use crate::config::{PROFILE, flags};
|
use crate::config::{PROFILE, flags};
|
||||||
use crate::utils::log::warn;
|
use crate::utils::log::warn;
|
||||||
|
|
||||||
|
/// Flags shared by the `build` and `run` CLI commands.
|
||||||
pub struct BuildRunFlags {
|
pub struct BuildRunFlags {
|
||||||
|
/// Build profile name (for example `debug` or `release`).
|
||||||
pub profile: String,
|
pub profile: String,
|
||||||
|
/// Optional target triple or platform name.
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
|
/// Optional workspace member name (`--workspace <name>`).
|
||||||
pub workspace: Option<String>,
|
pub workspace: Option<String>,
|
||||||
|
/// Force a rebuild even when artifacts appear up to date.
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
/// Clean build outputs before building.
|
||||||
pub clean: bool,
|
pub clean: bool,
|
||||||
|
/// Enable verbose logging during the build or run.
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
/// Print the final artifact path to the user.
|
||||||
pub print_artifact_path: bool,
|
pub print_artifact_path: bool,
|
||||||
|
/// Arguments forwarded to the binary after a `--` separator.
|
||||||
pub bin_args: Vec<String>,
|
pub bin_args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Splits CLI arguments at the first `--` separator.
|
||||||
|
///
|
||||||
|
/// Everything before `--` is treated as DCR flags; everything after is
|
||||||
|
/// forwarded to the built binary as `bin_args`.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: Full argument list for the command (without the program name).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `(dcr_args, bin_args)`. If `--` is absent, `bin_args` is empty.
|
||||||
pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
|
pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
|
||||||
match args.iter().position(|a| a == "--") {
|
match args.iter().position(|a| a == "--") {
|
||||||
Some(i) => (&args[..i], &args[i + 1..]),
|
Some(i) => (&args[..i], &args[i + 1..]),
|
||||||
@@ -36,6 +55,19 @@ pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses build/run flags from a list of CLI arguments.
|
||||||
|
///
|
||||||
|
/// Recognizes boolean flags (`--force`, `--clean`, `--verbose`,
|
||||||
|
/// `--print-artifact-path`), value flags (`--workspace`, `--target`), and
|
||||||
|
/// profile names registered via [`flags`]. Arguments after `--` become
|
||||||
|
/// [`BuildRunFlags::bin_args`].
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: CLI arguments for build/run (may include `--`).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(BuildRunFlags)` on success.
|
||||||
|
/// - `Err(1)` for unknown flags, missing values, or a duplicate profile.
|
||||||
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||||
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
||||||
let mut profile = PROFILE.to_string();
|
let mut profile = PROFILE.to_string();
|
||||||
@@ -48,6 +80,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
let mut iter = dcr_args.iter();
|
let mut iter = dcr_args.iter();
|
||||||
|
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
|
// DCR build/run options are long flags only (`--name`).
|
||||||
if !arg.starts_with("--") {
|
if !arg.starts_with("--") {
|
||||||
warn("Unknown argument");
|
warn("Unknown argument");
|
||||||
return Err(1);
|
return Err(1);
|
||||||
@@ -87,6 +120,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Profile names come from config::flags; only one profile may be set.
|
||||||
if flags(candidate).is_some() {
|
if flags(candidate).is_some() {
|
||||||
if profile != PROFILE {
|
if profile != PROFILE {
|
||||||
warn("Duplicate profile flag");
|
warn("Duplicate profile flag");
|
||||||
@@ -115,10 +149,12 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Converts string literals into owned `String` CLI arguments for tests.
|
||||||
fn s(args: &[&str]) -> Vec<String> {
|
fn s(args: &[&str]) -> Vec<String> {
|
||||||
args.iter().map(|a| a.to_string()).collect()
|
args.iter().map(|a| a.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks that without `--`, the full list stays on the DCR side.
|
||||||
#[test]
|
#[test]
|
||||||
fn split_double_dash_empty_tail() {
|
fn split_double_dash_empty_tail() {
|
||||||
let args = s(&["--release", "--force"]);
|
let args = s(&["--release", "--force"]);
|
||||||
@@ -127,6 +163,7 @@ mod tests {
|
|||||||
assert!(right.is_empty());
|
assert!(right.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks that arguments after `--` are split into the bin-args slice.
|
||||||
#[test]
|
#[test]
|
||||||
fn split_double_dash_with_bin_args() {
|
fn split_double_dash_with_bin_args() {
|
||||||
let args = s(&["--release", "--", "--test_help", "x"]);
|
let args = s(&["--release", "--", "--test_help", "x"]);
|
||||||
@@ -135,6 +172,7 @@ mod tests {
|
|||||||
assert_eq!(right, &s(&["--test_help", "x"]));
|
assert_eq!(right, &s(&["--test_help", "x"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks profile parsing and that bin args after `--` are forwarded.
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_run_flags_forwards_after_double_dash() {
|
fn parse_run_flags_forwards_after_double_dash() {
|
||||||
let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap();
|
let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap();
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
|||||||
use glob::glob;
|
use glob::glob;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Formats all C/C++ source files using clang-format.
|
||||||
|
///
|
||||||
|
/// Scans the `src/` and `tests/` directories recursively for files with common
|
||||||
|
/// C/C++ extensions and formats them in place with clang-format.
|
||||||
|
///
|
||||||
|
/// If the first argument is `--help`, prints usage information and returns 0.
|
||||||
pub fn fmt(args: &[String]) -> i32 {
|
pub fn fmt(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -31,6 +37,7 @@ pub fn fmt(args: &[String]) -> i32 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect all matching files from src/ and tests/ using glob patterns
|
||||||
let patterns = [
|
let patterns = [
|
||||||
"src/**/*.c",
|
"src/**/*.c",
|
||||||
"src/**/*.cpp",
|
"src/**/*.cpp",
|
||||||
@@ -66,6 +73,7 @@ pub fn fmt(args: &[String]) -> i32 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format the collected files
|
||||||
println!(
|
println!(
|
||||||
" {} {} files",
|
" {} {} files",
|
||||||
colored("Formatting", BOLD_GREEN),
|
colored("Formatting", BOLD_GREEN),
|
||||||
|
|||||||
@@ -34,14 +34,20 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
|
/// Path-dep include/lib/libs metadata for `dcr gen` (project may not be built yet).
|
||||||
/// Used by `gen` commands where the project may not have been built yet.
|
///
|
||||||
|
/// Explicit include/lib lists may point at missing paths; default `include`/`lib`
|
||||||
|
/// candidates are added only when those directories exist.
|
||||||
struct GenDeps {
|
struct GenDeps {
|
||||||
include_dirs: Vec<String>,
|
include_dirs: Vec<String>,
|
||||||
lib_dirs: Vec<String>,
|
lib_dirs: Vec<String>,
|
||||||
libs: Vec<String>,
|
libs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation.
|
||||||
|
///
|
||||||
|
/// Skips `system` deps and non-path entries. Falls back to common include/lib
|
||||||
|
/// directory names only when those directories exist.
|
||||||
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
|
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
|
||||||
let deps_val = match config.get("dependencies") {
|
let deps_val = match config.get("dependencies") {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
@@ -168,6 +174,7 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
|
|||||||
|
|
||||||
// ── public API ───────────────────────────────────────────────────────────────
|
// ── public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Per-project (or workspace member) metadata collected for `dcr gen`.
|
||||||
pub struct ProjectInfo {
|
pub struct ProjectInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
@@ -201,8 +208,13 @@ pub struct ProjectInfo {
|
|||||||
pub workspace_root: Option<PathBuf>,
|
pub workspace_root: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── entry-point for `dcr gen` ────────────────────────────────────────────────
|
/// Entry point for `dcr gen`: dispatches to project-info / compile-commands / vscode / clion.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `args`: First token is the subcommand; the rest are subcommand flags.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Process exit code from the chosen subcommand (`0` for help).
|
||||||
pub fn r#gen(args: &[String]) -> i32 {
|
pub fn r#gen(args: &[String]) -> i32 {
|
||||||
let subcommand = match args.first() {
|
let subcommand = match args.first() {
|
||||||
Some(s) if s == "--help" => {
|
Some(s) if s == "--help" => {
|
||||||
@@ -253,6 +265,7 @@ pub fn r#gen(args: &[String]) -> i32 {
|
|||||||
|
|
||||||
// ── shared: collect per-project data ─────────────────────────────────────────
|
// ── shared: collect per-project data ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Collects [`ProjectInfo`] with cwd set to the project root so relative paths resolve.
|
||||||
fn collect_project_info(
|
fn collect_project_info(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -265,6 +278,9 @@ fn collect_project_info(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads `dcr.toml` and resolves compiler, deps, sources, and artifact paths for one project.
|
||||||
|
///
|
||||||
|
/// Called once per project/member from [`collect_all`] (not recursive).
|
||||||
fn collect_project_info_inner(
|
fn collect_project_info_inner(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -576,6 +592,10 @@ fn find_in_path(name: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a tool name (e.g. moc, uic, rcc) for generated metadata.
|
||||||
|
///
|
||||||
|
/// Prefers a PATH hit via [`find_in_path`]; otherwise returns the trimmed name
|
||||||
|
/// as given (absolute or relative). Empty input yields `None`.
|
||||||
fn resolve_tool_path(name: &str) -> Option<String> {
|
fn resolve_tool_path(name: &str) -> Option<String> {
|
||||||
let t = name.trim();
|
let t = name.trim();
|
||||||
if t.is_empty() {
|
if t.is_empty() {
|
||||||
@@ -584,6 +604,8 @@ fn resolve_tool_path(name: &str) -> Option<String> {
|
|||||||
find_in_path(t).or_else(|| Some(t.to_string()))
|
find_in_path(t).or_else(|| Some(t.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Picks a debugger name: configured value if set, else first of lldb/gdb on PATH,
|
||||||
|
/// else `cppvsdbg` on Windows or `lldb` elsewhere (even if not on PATH).
|
||||||
fn resolve_debugger(configured: Option<&str>) -> String {
|
fn resolve_debugger(configured: Option<&str>) -> String {
|
||||||
if let Some(d) = configured {
|
if let Some(d) = configured {
|
||||||
let t = d.trim();
|
let t = d.trim();
|
||||||
@@ -617,6 +639,7 @@ fn utc_now_iso() -> String {
|
|||||||
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
|
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts days since the Unix epoch to `(year, month, day)` (proleptic Gregorian).
|
||||||
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||||
let z = days + 719468;
|
let z = days + 719468;
|
||||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||||
@@ -631,6 +654,14 @@ fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
|||||||
(y as i32, m, d)
|
(y as i32, m, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes `.dcr/build-info.json` and `.dcr/toolchain.json` from project metadata.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `root`: Project root (creates `.dcr/` if needed).
|
||||||
|
/// - `info`: Collected project metadata (compiler, artifact, toolchain tools).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `Ok(())` on success, or an error string if directory/file writes fail.
|
||||||
pub fn write_dcr_metadata(root: &Path, info: &ProjectInfo) -> Result<(), String> {
|
pub fn write_dcr_metadata(root: &Path, info: &ProjectInfo) -> Result<(), String> {
|
||||||
let dcr = ensure_dcr_dir(root).map_err(|e| format!("Failed to create .dcr/: {e}"))?;
|
let dcr = ensure_dcr_dir(root).map_err(|e| format!("Failed to create .dcr/: {e}"))?;
|
||||||
|
|
||||||
@@ -723,6 +754,7 @@ fn ensure_clangd(root: &Path, quiet: bool) {
|
|||||||
let _ = std::fs::write(&path, desired);
|
let _ = std::fs::write(&path, desired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects [`ProjectInfo`] for the root package and workspace members (skips failed members).
|
||||||
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
||||||
let root = canonicalize_path(root);
|
let root = canonicalize_path(root);
|
||||||
let config = {
|
let config = {
|
||||||
@@ -776,6 +808,7 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
|||||||
|
|
||||||
// ── dcr gen project-info ─────────────────────────────────────────────────────
|
// ── dcr gen project-info ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `dcr gen project-info`: print project metadata JSON; writes `.dcr` meta from the last entry.
|
||||||
fn gen_project_info(args: &[String]) -> i32 {
|
fn gen_project_info(args: &[String]) -> i32 {
|
||||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -811,6 +844,7 @@ fn gen_project_info(args: &[String]) -> i32 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pretty multi-line JSON object for one [`ProjectInfo`] (used inside a JSON array).
|
||||||
fn project_info_to_json(info: &ProjectInfo) -> String {
|
fn project_info_to_json(info: &ProjectInfo) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(" {\n");
|
out.push_str(" {\n");
|
||||||
@@ -896,6 +930,7 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
|
|||||||
|
|
||||||
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `dcr gen compile-commands`: write `.dcr/compile_commands.json`.
|
||||||
fn gen_compile_commands(args: &[String]) -> i32 {
|
fn gen_compile_commands(args: &[String]) -> i32 {
|
||||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -913,6 +948,9 @@ fn gen_compile_commands(args: &[String]) -> i32 {
|
|||||||
gen_compile_commands_inner(&root, &profile, &all, quiet)
|
gen_compile_commands_inner(&root, &profile, &all, quiet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes compile_commands.json, build metadata, and ensures `.clangd`.
|
||||||
|
///
|
||||||
|
/// Used by `compile-commands`, `vscode`, and `clion`.
|
||||||
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 {
|
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 {
|
||||||
if let Err(e) = ensure_dcr_dir(root) {
|
if let Err(e) = ensure_dcr_dir(root) {
|
||||||
error(&format!("Failed to create .dcr/: {e}"));
|
error(&format!("Failed to create .dcr/: {e}"));
|
||||||
@@ -942,6 +980,7 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a clangd/cpptools `compile_commands.json` array (one entry per source file).
|
||||||
fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String {
|
fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String {
|
||||||
let mut out = String::from("[\n");
|
let mut out = String::from("[\n");
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
@@ -972,6 +1011,8 @@ fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assembles one compiler argv: compiler, `-c`, optional `-x` asm, source/obj paths,
|
||||||
|
/// `-fPIC`, `-std=`, profile defaults, cflags, and include paths.
|
||||||
fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec<String> {
|
fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec<String> {
|
||||||
let mut cmd: Vec<String> = Vec::new();
|
let mut cmd: Vec<String> = Vec::new();
|
||||||
let compiler = if info.compiler.is_empty() {
|
let compiler = if info.compiler.is_empty() {
|
||||||
@@ -1027,7 +1068,7 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default profile flags (mirrors unix_cc.rs defaults)
|
// Default profile flags (same idea as default_profile_flags)
|
||||||
match profile {
|
match profile {
|
||||||
"release" => {
|
"release" => {
|
||||||
cmd.push("-O3".to_string());
|
cmd.push("-O3".to_string());
|
||||||
@@ -1076,6 +1117,7 @@ fn strip_src_prefix(p: &Path) -> PathBuf {
|
|||||||
|
|
||||||
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `dcr gen vscode`: compile_commands plus `.vscode/` tasks, launch, settings, extensions.
|
||||||
fn gen_vscode(args: &[String]) -> i32 {
|
fn gen_vscode(args: &[String]) -> i32 {
|
||||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -1216,6 +1258,8 @@ fn gen_tasks_json() -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Absolute program path for a launch config; reuses `artifact_path` when profiles match,
|
||||||
|
/// otherwise recomputes from kind/target/out_dir/filename.
|
||||||
fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String {
|
fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String {
|
||||||
if info.profile == profile
|
if info.profile == profile
|
||||||
&& let Some(ref p) = info.artifact_path
|
&& let Some(ref p) = info.artifact_path
|
||||||
@@ -1257,6 +1301,7 @@ fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// VS Code debug type from debugger name: `cppdbg`, `cppvsdbg`, or `lldb`.
|
||||||
fn debug_type_for(info: &ProjectInfo) -> &'static str {
|
fn debug_type_for(info: &ProjectInfo) -> &'static str {
|
||||||
let d = info.debugger.to_lowercase();
|
let d = info.debugger.to_lowercase();
|
||||||
if d.contains("gdb") || d == "cppdbg" {
|
if d.contains("gdb") || d == "cppdbg" {
|
||||||
@@ -1268,6 +1313,7 @@ fn debug_type_for(info: &ProjectInfo) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `launch.json` content: one debug config per `bin` target for debug and release.
|
||||||
fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
||||||
let mut configs = Vec::new();
|
let mut configs = Vec::new();
|
||||||
|
|
||||||
@@ -1361,6 +1407,7 @@ fn gen_settings_json(root: &Path) -> String {
|
|||||||
|
|
||||||
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `dcr gen clion`: compile_commands plus `.idea/` tools, targets, misc, run configs.
|
||||||
fn gen_clion(args: &[String]) -> i32 {
|
fn gen_clion(args: &[String]) -> i32 {
|
||||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -1571,6 +1618,7 @@ fn gen_clion_misc_xml(root: &Path) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// XML for one CLion run configuration under `.idea/runConfigurations/`.
|
||||||
fn gen_clion_run_config(info: &ProjectInfo, _root: &Path, profile: &str) -> String {
|
fn gen_clion_run_config(info: &ProjectInfo, _root: &Path, profile: &str) -> String {
|
||||||
let bin_path = resolve_launch_program(info, profile);
|
let bin_path = resolve_launch_program(info, profile);
|
||||||
let bin = xml_escape(&bin_path);
|
let bin = xml_escape(&bin_path);
|
||||||
@@ -1623,6 +1671,7 @@ fn xml_escape(s: &str) -> String {
|
|||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Parses `--debug` / `--release` / `--quiet` and resolves the project root.
|
||||||
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
|
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
|
||||||
let mut profile = "debug".to_string();
|
let mut profile = "debug".to_string();
|
||||||
let mut quiet = false;
|
let mut quiet = false;
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
|
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
|
|
||||||
|
/// Displays the help information for the DCR CLI tool.
|
||||||
|
///
|
||||||
|
/// Prints usage, commands, flags, options, examples, and a tip.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Always `0`.
|
||||||
pub fn help() -> i32 {
|
pub fn help() -> i32 {
|
||||||
println!("DCR (Dexoron Cargo Realization)");
|
println!("DCR (Dexoron Cargo Realization)");
|
||||||
println!("C project manager inspired by Cargo.");
|
println!("C project manager inspired by Cargo.");
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ use std::fs;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
|
/// Initializes the current working directory as a new DCR project.
|
||||||
|
///
|
||||||
|
/// Verifies that the current directory is empty, sets up the project structure by creating
|
||||||
|
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
|
||||||
pub fn init(args: &[String]) -> i32 {
|
pub fn init(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -36,6 +40,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse options (--vcs) and collect positional arguments.
|
||||||
let mut vcs_str = None;
|
let mut vcs_str = None;
|
||||||
let mut clean_args = Vec::new();
|
let mut clean_args = Vec::new();
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
@@ -59,6 +64,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve target project name from current directory name and enforce empty directory check.
|
||||||
let items = check_dir(None).unwrap_or_default();
|
let items = check_dir(None).unwrap_or_default();
|
||||||
let project_name = std::env::current_dir()
|
let project_name = std::env::current_dir()
|
||||||
.ok()
|
.ok()
|
||||||
@@ -84,6 +90,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
.unwrap_or_else(|_| ".".to_string());
|
.unwrap_or_else(|_| ".".to_string());
|
||||||
println!("Initializing the project in {cwd}");
|
println!("Initializing the project in {cwd}");
|
||||||
|
|
||||||
|
// Create and initialize default manifest (dcr.toml).
|
||||||
let mut config = match Config::new("./dcr.toml") {
|
let mut config = match Config::new("./dcr.toml") {
|
||||||
Ok(cfg) => cfg,
|
Ok(cfg) => cfg,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -104,6 +111,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
colored("dcr.toml", BOLD_CYAN)
|
colored("dcr.toml", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create src directory and write template main.c file.
|
||||||
if fs::create_dir("src").is_err() {
|
if fs::create_dir("src").is_err() {
|
||||||
error("Failed to create src/");
|
error("Failed to create src/");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -125,7 +133,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|
||||||
// settings VCS
|
// Resolve target VCS provider or detect existing repositories.
|
||||||
let mut vcs_kind = VcsKind::Git;
|
let mut vcs_kind = VcsKind::Git;
|
||||||
if let Some(ref vcs_val) = vcs_str {
|
if let Some(ref vcs_val) = vcs_str {
|
||||||
match VcsKind::parse(vcs_val) {
|
match VcsKind::parse(vcs_val) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Runs clang-tidy on all C/C++ source files.
|
||||||
|
///
|
||||||
|
/// Scans source roots (configurable via build.roots) and tests/ directory.
|
||||||
pub fn lint(args: &[String]) -> i32 {
|
pub fn lint(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -72,6 +75,7 @@ pub fn lint(args: &[String]) -> i32 {
|
|||||||
let build_roots = get_list_with_profile(&config, "roots", profile);
|
let build_roots = get_list_with_profile(&config, "roots", profile);
|
||||||
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
||||||
|
|
||||||
|
// Construct source roots from config
|
||||||
let mut source_roots: Vec<String> = Vec::new();
|
let mut source_roots: Vec<String> = Vec::new();
|
||||||
for raw in &build_roots {
|
for raw in &build_roots {
|
||||||
let p = Path::new(raw);
|
let p = Path::new(raw);
|
||||||
@@ -126,6 +130,7 @@ pub fn lint(args: &[String]) -> i32 {
|
|||||||
let mut total_issues = 0u32;
|
let mut total_issues = 0u32;
|
||||||
let mut failed = 0u32;
|
let mut failed = 0u32;
|
||||||
|
|
||||||
|
// Execute clang-tidy for each file and count issues
|
||||||
for file in &files {
|
for file in &files {
|
||||||
let mut cmd = Command::new("clang-tidy");
|
let mut cmd = Command::new("clang-tidy");
|
||||||
if do_fix {
|
if do_fix {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// DCR CLI module.
|
||||||
|
///
|
||||||
|
/// This module declares submodules for all DCR commands.
|
||||||
pub mod add;
|
pub mod add;
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod clean;
|
pub mod clean;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ use std::fs;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
|
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
|
||||||
|
///
|
||||||
|
/// Handles command-line argument parsing, directory creation, default configuration generation (`dcr.toml`),
|
||||||
|
/// template source file generation (`src/main.c`), and initial VCS setup.
|
||||||
pub fn new(args: &[String]) -> i32 {
|
pub fn new(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -38,6 +42,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
|
|
||||||
let items = check_dir(None).unwrap_or_default();
|
let items = check_dir(None).unwrap_or_default();
|
||||||
|
|
||||||
|
// Parse CLI options (--vcs) while extracting positional arguments for the project name.
|
||||||
let mut vcs_str = None;
|
let mut vcs_str = None;
|
||||||
let mut clean_args = Vec::new();
|
let mut clean_args = Vec::new();
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
@@ -103,6 +108,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
project_name
|
project_name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialize and write project configuration manifest (dcr.toml).
|
||||||
let toml_path = format!("./{project_name}/dcr.toml");
|
let toml_path = format!("./{project_name}/dcr.toml");
|
||||||
let mut config = match Config::new(&toml_path) {
|
let mut config = match Config::new(&toml_path) {
|
||||||
Ok(cfg) => cfg,
|
Ok(cfg) => cfg,
|
||||||
@@ -124,6 +130,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
colored("dcr.toml", BOLD_CYAN)
|
colored("dcr.toml", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Generate src directory and populate initial main.c file.
|
||||||
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
|
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
|
||||||
error("Failed to create src/");
|
error("Failed to create src/");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -146,7 +153,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|
||||||
// settings VCS
|
// Resolve target VCS mode based on CLI flags or parent directory state.
|
||||||
let mut vcs_kind = VcsKind::Git;
|
let mut vcs_kind = VcsKind::Git;
|
||||||
if let Some(ref vcs_val) = vcs_str {
|
if let Some(ref vcs_val) = vcs_str {
|
||||||
match VcsKind::parse(vcs_val) {
|
match VcsKind::parse(vcs_val) {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
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(
|
fn get_run_cmd(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -55,6 +57,14 @@ fn get_run_cmd(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn run(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -177,6 +187,7 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds and runs a single project at `root` (optionally as a workspace member).
|
||||||
fn run_project(
|
fn run_project(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
flags: &crate::cli::flags::BuildRunFlags,
|
flags: &crate::cli::flags::BuildRunFlags,
|
||||||
@@ -290,6 +301,7 @@ fn run_project(
|
|||||||
Err("Fix errors in the code to run the project".to_string())
|
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> {
|
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
args.push(format!("--{}", flags.profile));
|
args.push(format!("--{}", flags.profile));
|
||||||
@@ -313,6 +325,7 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs build arguments specifically for a workspace member.
|
||||||
fn args_for_workspace_build(
|
fn args_for_workspace_build(
|
||||||
flags: &crate::cli::flags::BuildRunFlags,
|
flags: &crate::cli::flags::BuildRunFlags,
|
||||||
member_name: &str,
|
member_name: &str,
|
||||||
@@ -337,6 +350,7 @@ fn args_for_workspace_build(
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the given shell command and returns its exit code.
|
||||||
fn run_shell(cmd: &str) -> i32 {
|
fn run_shell(cmd: &str) -> i32 {
|
||||||
let status = if cfg!(target_os = "windows") {
|
let status = if cfg!(target_os = "windows") {
|
||||||
Command::new("cmd").arg("/C").arg(cmd).status()
|
Command::new("cmd").arg("/C").arg(cmd).status()
|
||||||
@@ -350,6 +364,7 @@ fn run_shell(cmd: &str) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the command, appending escaped binary arguments if provided.
|
||||||
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
||||||
if bin_args.is_empty() {
|
if bin_args.is_empty() {
|
||||||
return run_shell(cmd);
|
return run_shell(cmd);
|
||||||
@@ -362,6 +377,7 @@ fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
|||||||
run_shell(&full)
|
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 {
|
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
||||||
if bin_args.is_empty() {
|
if bin_args.is_empty() {
|
||||||
return cmd.to_string();
|
return cmd.to_string();
|
||||||
@@ -374,6 +390,7 @@ fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
|||||||
display
|
display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats the binary path for console display, appending arguments if any.
|
||||||
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
||||||
if bin_args.is_empty() {
|
if bin_args.is_empty() {
|
||||||
return bin_path.to_string();
|
return bin_path.to_string();
|
||||||
@@ -386,6 +403,7 @@ fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
|||||||
display
|
display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix.
|
||||||
fn shell_escape(arg: &str) -> String {
|
fn shell_escape(arg: &str) -> String {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
||||||
@@ -406,6 +424,7 @@ fn shell_escape(arg: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replaces version info and profile in the command template.
|
||||||
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
||||||
let info = parse_version_info(version);
|
let info = parse_version_info(version);
|
||||||
substitute_vars(cmd, &info, profile, "")
|
substitute_vars(cmd, &info, profile, "")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
|
|
||||||
|
/// Sets up DCR registries by loading the registry manager and printing the list of registries.
|
||||||
pub fn setup(args: &[String]) -> i32 {
|
pub fn setup(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -29,6 +30,7 @@ pub fn setup(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!("Setting up DCR registries...");
|
println!("Setting up DCR registries...");
|
||||||
|
// Load and process the registries, returning 0 on success or 1 on error
|
||||||
match crate::core::registry::RegistryManager::load() {
|
match crate::core::registry::RegistryManager::load() {
|
||||||
Ok(manager) => {
|
Ok(manager) => {
|
||||||
println!("Loaded {} registries.", manager.config.registry.len());
|
println!("Loaded {} registries.", manager.config.registry.len());
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use std::process::Command;
|
|||||||
|
|
||||||
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||||
|
|
||||||
|
/// Runs project tests and prints a unified testsuite report.
|
||||||
pub fn test(args: &[String]) -> i32 {
|
pub fn test(args: &[String]) -> i32 {
|
||||||
let mut init_header = false;
|
let mut init_header = false;
|
||||||
let mut profile = "debug";
|
let mut profile = "debug";
|
||||||
@@ -101,6 +102,7 @@ pub fn test(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the testsuite for the given build profile.
|
||||||
fn run_testsuite(profile: &str) -> Result<i32, String> {
|
fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||||
if build::build(&[format!("--{profile}")]) != 0 {
|
if build::build(&[format!("--{profile}")]) != 0 {
|
||||||
return Ok(1);
|
return Ok(1);
|
||||||
@@ -260,6 +262,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all .c source files from the tests/ directory.
|
||||||
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
||||||
let tests_dir = Path::new("./tests");
|
let tests_dir = Path::new("./tests");
|
||||||
if !tests_dir.is_dir() {
|
if !tests_dir.is_dir() {
|
||||||
@@ -278,6 +281,7 @@ fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
|||||||
Ok(sources)
|
Ok(sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a list of include directories for test compilation.
|
||||||
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
||||||
let mut dirs = vec![
|
let mut dirs = vec![
|
||||||
"./tests".to_string(),
|
"./tests".to_string(),
|
||||||
@@ -288,6 +292,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
|||||||
dirs
|
dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles a single test source file to an object file.
|
||||||
fn compile_test_source(
|
fn compile_test_source(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
standard: &str,
|
standard: &str,
|
||||||
@@ -317,6 +322,7 @@ fn compile_test_source(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Links the test object file into an executable binary.
|
||||||
fn link_test_binary(
|
fn link_test_binary(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
ldflags: &[String],
|
ldflags: &[String],
|
||||||
@@ -343,6 +349,7 @@ fn link_test_binary(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints the stdout and stderr from a command execution.
|
||||||
fn print_command_output(output: &std::process::Output) {
|
fn print_command_output(output: &std::process::Output) {
|
||||||
if !output.stdout.is_empty() {
|
if !output.stdout.is_empty() {
|
||||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
@@ -352,6 +359,7 @@ fn print_command_output(output: &std::process::Output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates the default test files (dcr_test.h and test.c) in the tests/ directory.
|
||||||
fn ensure_test_header() -> Result<(), String> {
|
fn ensure_test_header() -> Result<(), String> {
|
||||||
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||||
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||||
@@ -363,6 +371,7 @@ fn ensure_test_header() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts test case names from a C source file by parsing TEST_CASE macros.
|
||||||
fn extract_test_names_path(path: &Path) -> Vec<String> {
|
fn extract_test_names_path(path: &Path) -> Vec<String> {
|
||||||
let Ok(content) = fs::read_to_string(path) else {
|
let Ok(content) = fs::read_to_string(path) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -379,6 +388,7 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
|
|||||||
names
|
names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints a test result field with color coding.
|
||||||
fn print_field(label: &str, value: i32, color: &str) {
|
fn print_field(label: &str, value: i32, color: &str) {
|
||||||
if value == 0 {
|
if value == 0 {
|
||||||
println!("{}: {}", label, value);
|
println!("{}: {}", label, value);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ use std::collections::HashSet;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
|
/// Displays the dependency tree of the current project.
|
||||||
|
///
|
||||||
|
/// This function checks for the --help flag and prints usage information if present.
|
||||||
|
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
|
||||||
pub fn tree(args: &[String]) -> i32 {
|
pub fn tree(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
@@ -62,6 +66,9 @@ pub fn tree(args: &[String]) -> i32 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively prints the dependency tree starting from the given dependencies table.
|
||||||
|
///
|
||||||
|
/// Sorts dependencies for consistent ordering and handles path resolution for sub-dependencies.
|
||||||
fn print_deps(
|
fn print_deps(
|
||||||
deps: &toml::value::Table,
|
deps: &toml::value::Table,
|
||||||
base_path: &Path,
|
base_path: &Path,
|
||||||
@@ -81,6 +88,7 @@ fn print_deps(
|
|||||||
let mut sub_deps = None;
|
let mut sub_deps = None;
|
||||||
let mut resolved_path = None;
|
let mut resolved_path = None;
|
||||||
|
|
||||||
|
// Resolve dependency path if it's a path-based dependency
|
||||||
if let Some(path) = &dep_path {
|
if let Some(path) = &dep_path {
|
||||||
let dcr_toml = path.join("dcr.toml");
|
let dcr_toml = path.join("dcr.toml");
|
||||||
if dcr_toml.exists()
|
if dcr_toml.exists()
|
||||||
@@ -117,6 +125,7 @@ fn print_deps(
|
|||||||
|
|
||||||
println!("{}{}{}{}", prefix, connector, name, version);
|
println!("{}{}{}{}", prefix, connector, name, version);
|
||||||
|
|
||||||
|
// Recurse only if sub-dependencies exist and this dep hasn't been seen
|
||||||
if let Some(s_deps) = sub_deps
|
if let Some(s_deps) = sub_deps
|
||||||
&& !seen.contains(*name)
|
&& !seen.contains(*name)
|
||||||
{
|
{
|
||||||
@@ -130,6 +139,7 @@ fn print_deps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the filesystem path for a dependency entry if it specifies a "path:" prefix or table with "path" key.
|
||||||
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
|
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
|
||||||
match value {
|
match value {
|
||||||
Value::String(s) => s
|
Value::String(s) => s
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
/// Returns build flags for the given profile as a static slice, or None if empty.
|
||||||
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
||||||
let f = crate::utils::build::default_profile_flags(profile);
|
let f = crate::utils::build::default_profile_flags(profile);
|
||||||
if f.is_empty() { None } else { Some(f) }
|
if f.is_empty() { None } else { Some(f) }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use std::fs::{self, File, OpenOptions};
|
|||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// File wrapper that shifts all seek offsets (used to skip the boot sector on a FAT image).
|
||||||
struct OffsetFile<T> {
|
struct OffsetFile<T> {
|
||||||
inner: T,
|
inner: T,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
@@ -73,6 +74,7 @@ impl<T: Seek> Seek for OffsetFile<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a size string like `1.5m` or `500k` into bytes (1024-based; empty → 0).
|
||||||
fn parse_size(s: &str) -> Result<u64, String> {
|
fn parse_size(s: &str) -> Result<u64, String> {
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
@@ -101,6 +103,7 @@ fn parse_size(s: &str) -> Result<u64, String> {
|
|||||||
Ok((val * multiplier) as u64)
|
Ok((val * multiplier) as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a FAT image from layout config, substituting `{profile}` and optional boot sector.
|
||||||
pub fn pack_archive(
|
pub fn pack_archive(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
config: &ArchiveConfig,
|
config: &ArchiveConfig,
|
||||||
@@ -142,6 +145,7 @@ pub fn pack_archive(
|
|||||||
if offset == 0
|
if offset == 0
|
||||||
&& let Some(boot_rel) = &config.bootsector
|
&& let Some(boot_rel) = &config.bootsector
|
||||||
{
|
{
|
||||||
|
// Load and write bootsector if present
|
||||||
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
|
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
|
||||||
let boot_path = project_root.join(&boot_rel_substituted);
|
let boot_path = project_root.join(&boot_rel_substituted);
|
||||||
if boot_path.is_file() {
|
if boot_path.is_file() {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Returns the artifact directory path based on the build context.
|
||||||
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
||||||
match ctx.target_dir {
|
match ctx.target_dir {
|
||||||
Some(dir) => Path::new(dir).to_path_buf(),
|
Some(dir) => Path::new(dir).to_path_buf(),
|
||||||
@@ -30,6 +31,17 @@ pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the artifact path for a given kind/profile/name.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `kind`: Artifact kind (`bin`, `staticlib`, `sharedlib`, `flat-bin`, `efi`, `elf`, …).
|
||||||
|
/// - `profile`: Build profile (`debug` / `release` / custom).
|
||||||
|
/// - `project_name`: Default basename when `output_filename` is unset.
|
||||||
|
/// - `target_dir`: Optional artifact directory override.
|
||||||
|
/// - `output_filename` / `output_extension`: Optional name/ext overrides.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Relative/absolute path string, or `None` for `none` / `custom` kinds.
|
||||||
pub fn resolve_artifact_path(
|
pub fn resolve_artifact_path(
|
||||||
kind: &str,
|
kind: &str,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -75,6 +87,7 @@ pub fn resolve_artifact_path(
|
|||||||
Some(path)
|
Some(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a relative or absolute path to an absolute path by joining with project root if needed.
|
||||||
pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf {
|
pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf {
|
||||||
let p = Path::new(relative_or_abs);
|
let p = Path::new(relative_or_abs);
|
||||||
let joined = if p.is_absolute() {
|
let joined = if p.is_absolute() {
|
||||||
@@ -88,6 +101,7 @@ pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> Pat
|
|||||||
crate::utils::fs::canonicalize_path(&joined)
|
crate::utils::fs::canonicalize_path(&joined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the output path for a flat binary artifact.
|
||||||
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
||||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||||
let ext = ctx.output_extension.unwrap_or("bin");
|
let ext = ctx.output_extension.unwrap_or("bin");
|
||||||
@@ -99,6 +113,7 @@ pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
|||||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the output path for a flat binary artifact from a source file.
|
||||||
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
|
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
|
||||||
let stem = Path::new(source)
|
let stem = Path::new(source)
|
||||||
.file_stem()
|
.file_stem()
|
||||||
@@ -113,6 +128,7 @@ pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> Strin
|
|||||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to find a suitable objcopy tool in PATH
|
||||||
fn resolve_objcopy() -> Result<String, String> {
|
fn resolve_objcopy() -> Result<String, String> {
|
||||||
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
|
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
|
||||||
if Command::new(candidate)
|
if Command::new(candidate)
|
||||||
@@ -131,6 +147,7 @@ fn resolve_objcopy() -> Result<String, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an object/ELF to a raw binary via `objcopy -O binary` (skips if up to date).
|
||||||
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
|
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
|
||||||
if let Some(parent) = Path::new(output).parent() {
|
if let Some(parent) = Path::new(output).parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
|
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
|
||||||
@@ -147,6 +164,7 @@ pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> R
|
|||||||
common::run_command_sync_output(&mut cmd)
|
common::run_command_sync_output(&mut cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Links objects into an intermediate ELF, then converts it to a flat binary via objcopy.
|
||||||
pub(crate) fn link_flat_binary(
|
pub(crate) fn link_flat_binary(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
objects: &[String],
|
objects: &[String],
|
||||||
@@ -208,6 +226,7 @@ pub(crate) fn link_flat_binary(
|
|||||||
Ok(common::elapsed_secs(start_time))
|
Ok(common::elapsed_secs(start_time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Archives objects into a static library.
|
||||||
pub(crate) fn archive_static(
|
pub(crate) fn archive_static(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
objects: &[String],
|
objects: &[String],
|
||||||
@@ -234,6 +253,7 @@ pub(crate) fn archive_static(
|
|||||||
run(ctx, cmd, start_time)
|
run(ctx, cmd, start_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Links objects into a binary or library based on artifact kind.
|
||||||
pub(crate) fn link_binary(
|
pub(crate) fn link_binary(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
objects: &[String],
|
objects: &[String],
|
||||||
@@ -299,6 +319,7 @@ pub(crate) fn link_binary(
|
|||||||
run(ctx, cmd, start_time)
|
run(ctx, cmd, start_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the default archiver name based on compiler and platform.
|
||||||
fn default_archiver(compiler: &str) -> &'static str {
|
fn default_archiver(compiler: &str) -> &'static str {
|
||||||
let c = compiler.to_lowercase();
|
let c = compiler.to_lowercase();
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
@@ -314,11 +335,13 @@ fn default_archiver(compiler: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the archiver is an MSVC lib tool.
|
||||||
fn is_msvc_lib(archiver: &str) -> bool {
|
fn is_msvc_lib(archiver: &str) -> bool {
|
||||||
let a = archiver.to_lowercase();
|
let a = archiver.to_lowercase();
|
||||||
a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe")
|
a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a command and handles success/failure with timing.
|
||||||
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
|
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
|
||||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||||
eprintln!("[dcr] {:?}", cmd);
|
eprintln!("[dcr] {:?}", cmd);
|
||||||
@@ -345,12 +368,14 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64,
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Default `bin` kind resolves via platform bin path.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_bin_default() {
|
fn resolve_bin_default() {
|
||||||
let p = resolve_artifact_path("bin", "debug", "hello", None, None, None).unwrap();
|
let p = resolve_artifact_path("bin", "debug", "hello", None, None, None).unwrap();
|
||||||
assert_eq!(p, platform::bin_path("debug", "hello", None));
|
assert_eq!(p, platform::bin_path("debug", "hello", None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Custom `target_dir` places the binary under that directory.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_bin_custom_target_dir() {
|
fn resolve_bin_custom_target_dir() {
|
||||||
let p = resolve_artifact_path("bin", "debug", "hello", Some("/out"), None, None).unwrap();
|
let p = resolve_artifact_path("bin", "debug", "hello", Some("/out"), None, None).unwrap();
|
||||||
@@ -359,6 +384,7 @@ mod tests {
|
|||||||
assert_eq!(p, expected.to_string_lossy());
|
assert_eq!(p, expected.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `staticlib` kind resolves to a platform static library path.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_staticlib() {
|
fn resolve_staticlib() {
|
||||||
let p = resolve_artifact_path("staticlib", "release", "mylib", Some("dist"), None, None)
|
let p = resolve_artifact_path("staticlib", "release", "mylib", Some("dist"), None, None)
|
||||||
@@ -372,6 +398,7 @@ mod tests {
|
|||||||
assert_eq!(p, expected.to_string_lossy());
|
assert_eq!(p, expected.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Custom output filename and extension override the default name.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_with_filename_ext() {
|
fn resolve_with_filename_ext() {
|
||||||
let p = resolve_artifact_path(
|
let p = resolve_artifact_path(
|
||||||
@@ -391,6 +418,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `none` kind has no artifact path.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_none_kind() {
|
fn resolve_none_kind() {
|
||||||
assert!(resolve_artifact_path("none", "debug", "x", None, None, None).is_none());
|
assert!(resolve_artifact_path("none", "debug", "x", None, None, None).is_none());
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Compiles C/C++ sources and links/archives by kind (`bin`, libs, `flat-bin`, or compile-only).
|
||||||
|
///
|
||||||
|
/// Handles Qt preprocessing when enabled. Returns elapsed seconds.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
let compiler = if ctx.compiler.is_empty() {
|
let compiler = if ctx.compiler.is_empty() {
|
||||||
"cc"
|
"cc"
|
||||||
} else {
|
} else {
|
||||||
ctx.compiler
|
ctx.compiler
|
||||||
};
|
};
|
||||||
|
// Reject unsupported flat-bin with Qt
|
||||||
if is_flat_bin(ctx.kind) && ctx.qt {
|
if is_flat_bin(ctx.kind) && ctx.qt {
|
||||||
return Err("flat-bin is not supported with build.qt = true".to_string());
|
return Err("flat-bin is not supported with build.qt = true".to_string());
|
||||||
}
|
}
|
||||||
@@ -69,6 +73,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
artifact::link_binary(ctx, &objects, compiler, start_time)
|
artifact::link_binary(ctx, &objects, compiler, start_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects source files from the provided source roots for the build context.
|
||||||
|
///
|
||||||
|
/// Delegates to common source collection after resolving language-specific extensions.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
let extensions = source_extensions(ctx.language);
|
let extensions = source_extensions(ctx.language);
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
@@ -79,14 +86,19 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Source file extensions for `language` (owned `Vec` of static slices).
|
||||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||||
crate::core::build::common::source_extensions(language)
|
crate::core::build::common::source_extensions(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves default compiler flags for the specified build profile.
|
||||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||||
crate::utils::build::default_profile_flags(profile)
|
crate::utils::build::default_profile_flags(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles all provided sources into object files in parallel.
|
||||||
|
///
|
||||||
|
/// Prepares object file paths and spawns parallel tasks for each source compilation.
|
||||||
fn build_objects(
|
fn build_objects(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
sources: &[String],
|
sources: &[String],
|
||||||
@@ -100,6 +112,7 @@ fn build_objects(
|
|||||||
.map(|s| common::object_path(obj_dir, s, obj_ext))
|
.map(|s| common::object_path(obj_dir, s, obj_ext))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Compile objects in parallel
|
||||||
common::parallel_build(
|
common::parallel_build(
|
||||||
sources.len(),
|
sources.len(),
|
||||||
|i| {
|
|i| {
|
||||||
@@ -117,6 +130,9 @@ fn build_objects(
|
|||||||
Ok(objects)
|
Ok(objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles a single source file to an object file.
|
||||||
|
///
|
||||||
|
/// Creates object directory if needed, checks for rebuild necessity, constructs and runs the compiler command with appropriate flags.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -128,6 +144,7 @@ fn build_object(
|
|||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if object is already up to date
|
||||||
if !common::needs_rebuild(source, obj_path) {
|
if !common::needs_rebuild(source, obj_path) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -183,6 +200,8 @@ fn build_object(
|
|||||||
|
|
||||||
let use_dcr_defaults = ctx.cflags.is_empty()
|
let use_dcr_defaults = ctx.cflags.is_empty()
|
||||||
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
|
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
|
||||||
|
|
||||||
|
// Use default flags only if no custom flags and not special target
|
||||||
if use_dcr_defaults {
|
if use_dcr_defaults {
|
||||||
for flag in default_flags(ctx.profile) {
|
for flag in default_flags(ctx.profile) {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
@@ -211,6 +230,7 @@ fn build_object(
|
|||||||
common::run_command_sync_output(&mut cmd)
|
common::run_command_sync_output(&mut cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the assembly language flag for the compiler based on source extension.
|
||||||
fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||||
crate::core::build::common::asm_lang_flag(source)
|
crate::core::build::common::asm_lang_flag(source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub mod msvc;
|
|||||||
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
|
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
|
||||||
use crate::core::build::language::llvm_ir;
|
use crate::core::build::language::llvm_ir;
|
||||||
|
|
||||||
|
/// Configuration context for the build process, including compiler, target, and output settings.
|
||||||
pub struct BuildContext<'a> {
|
pub struct BuildContext<'a> {
|
||||||
pub profile: &'a str,
|
pub profile: &'a str,
|
||||||
pub project_name: &'a str,
|
pub project_name: &'a str,
|
||||||
@@ -56,6 +57,7 @@ pub struct BuildContext<'a> {
|
|||||||
pub qt: bool,
|
pub qt: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait defining the build interface for different compilers and languages.
|
||||||
pub trait Builder {
|
pub trait Builder {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn id(&self) -> &'static str;
|
fn id(&self) -> &'static str;
|
||||||
@@ -63,12 +65,19 @@ pub trait Builder {
|
|||||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
|
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concrete builder for C/C++ languages using gcc/clang.
|
||||||
struct Cc;
|
struct Cc;
|
||||||
|
/// Concrete builder for MSVC and clang-cl.
|
||||||
struct Msvc;
|
struct Msvc;
|
||||||
|
/// Concrete builder for NASM assembly.
|
||||||
struct Nasm;
|
struct Nasm;
|
||||||
|
/// Concrete builder for GNU Assembler (gas).
|
||||||
struct Gas;
|
struct Gas;
|
||||||
|
/// Concrete builder for MASM assembler.
|
||||||
struct Masm;
|
struct Masm;
|
||||||
|
/// Concrete builder for FASM assembler.
|
||||||
struct Fasm;
|
struct Fasm;
|
||||||
|
/// Concrete builder for LLVM IR.
|
||||||
struct Llc;
|
struct Llc;
|
||||||
|
|
||||||
impl Builder for Cc {
|
impl Builder for Cc {
|
||||||
@@ -156,6 +165,7 @@ impl Builder for Llc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
||||||
|
// Normalize compiler name to lowercase for matching.
|
||||||
let c = compiler.to_lowercase();
|
let c = compiler.to_lowercase();
|
||||||
if c.contains("clang-cl") {
|
if c.contains("clang-cl") {
|
||||||
return &Msvc;
|
return &Msvc;
|
||||||
@@ -181,6 +191,7 @@ pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
|||||||
&Cc
|
&Cc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Main build entrypoint that validates the compiler and delegates to the selected builder.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
if !check_compiler_exists(ctx.compiler) {
|
if !check_compiler_exists(ctx.compiler) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -188,9 +199,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
ctx.compiler
|
ctx.compiler
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Delegate to the concrete builder's build method.
|
||||||
builder_for(ctx.compiler).build(ctx)
|
builder_for(ctx.compiler).build(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `compiler --version` can be spawned (exit status is ignored).
|
||||||
fn check_compiler_exists(compiler: &str) -> bool {
|
fn check_compiler_exists(compiler: &str) -> bool {
|
||||||
let name = if compiler.is_empty() {
|
let name = if compiler.is_empty() {
|
||||||
"cc"
|
"cc"
|
||||||
@@ -209,10 +222,12 @@ fn check_compiler_exists(compiler: &str) -> bool {
|
|||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects source code files by delegating to the builder selected for the compiler.
|
||||||
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
builder_for(ctx.compiler).collect_sources(ctx)
|
builder_for(ctx.compiler).collect_sources(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unit test verifying that builder_for correctly identifies the builder for various compiler names.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// MSVC build backend for DCR.
|
||||||
|
///
|
||||||
|
/// This module provides the MSVC-specific build implementation for C and C++
|
||||||
|
/// projects, including object compilation and linking for executables, static
|
||||||
|
/// libraries, shared libraries, and flat binaries.
|
||||||
use crate::core::build::builder::BuildContext;
|
use crate::core::build::builder::BuildContext;
|
||||||
use crate::core::build::builder::artifact;
|
use crate::core::build::builder::artifact;
|
||||||
use crate::core::build::common;
|
use crate::core::build::common;
|
||||||
@@ -25,6 +30,7 @@ use std::path::Path;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Builds the project target using the MSVC compiler.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
let compiler = if ctx.compiler.is_empty() {
|
let compiler = if ctx.compiler.is_empty() {
|
||||||
"cl"
|
"cl"
|
||||||
@@ -159,6 +165,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Links the flat binary output for MSVC build targets.
|
||||||
fn link_flat_msvc(
|
fn link_flat_msvc(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
@@ -214,6 +221,7 @@ fn link_flat_msvc(
|
|||||||
Ok(common::elapsed_secs(start_time))
|
Ok(common::elapsed_secs(start_time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all source files for the build based on language extensions.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
let extensions = source_extensions(ctx.language);
|
let extensions = source_extensions(ctx.language);
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
@@ -224,10 +232,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Source file extensions for `language` (owned `Vec` of static slices).
|
||||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||||
crate::core::build::common::source_extensions(language)
|
crate::core::build::common::source_extensions(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps the language and standard version to the corresponding MSVC flag string.
|
||||||
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
||||||
let lang = language.to_lowercase();
|
let lang = language.to_lowercase();
|
||||||
let std = standard.to_lowercase();
|
let std = standard.to_lowercase();
|
||||||
@@ -251,6 +261,7 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String>
|
|||||||
Err("Unsupported language".to_string())
|
Err("Unsupported language".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the MSVC architecture flag based on the target platform.
|
||||||
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
|
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
|
||||||
let raw = platform?.trim();
|
let raw = platform?.trim();
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
@@ -268,6 +279,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns default compiler flags for the specified build profile.
|
||||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||||
match profile {
|
match profile {
|
||||||
"release" => &["/O2", "/DNDEBUG"],
|
"release" => &["/O2", "/DNDEBUG"],
|
||||||
@@ -276,6 +288,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles all sources into object files using parallel processing.
|
||||||
fn build_objects(
|
fn build_objects(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
sources: &[String],
|
sources: &[String],
|
||||||
@@ -297,6 +310,7 @@ fn build_objects(
|
|||||||
Ok(objects)
|
Ok(objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles a single source file to an object file.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -369,6 +383,7 @@ fn build_object(
|
|||||||
let mut headers = Vec::new();
|
let mut headers = Vec::new();
|
||||||
let mut clean_stdout = String::new();
|
let mut clean_stdout = String::new();
|
||||||
|
|
||||||
|
// Parse compiler output to extract include file notes for dependency generation
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
if let Some(stripped) = line.strip_prefix("Note: including file:") {
|
if let Some(stripped) = line.strip_prefix("Note: including file:") {
|
||||||
headers.push(stripped.trim().to_string());
|
headers.push(stripped.trim().to_string());
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Computes a build fingerprint by hashing context fields and file metadata (SHA-256).
|
||||||
|
///
|
||||||
|
/// Includes `dcr.toml` (required) and `dcr.lock` when present, plus path/size/mtime
|
||||||
|
/// for each source, header, and library file.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctx`: Build context (profile, tools, flags, dirs, …).
|
||||||
|
/// - `sources` / `headers` / `lib_files`: Inputs whose metadata enters the hash.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Hex-encoded fingerprint, or an error if `dcr.toml` or a listed file cannot be read.
|
||||||
pub(crate) fn compute_build_fingerprint(
|
pub(crate) fn compute_build_fingerprint(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
sources: &[String],
|
sources: &[String],
|
||||||
@@ -61,12 +72,14 @@ pub(crate) fn compute_build_fingerprint(
|
|||||||
for value in ctx.ldflags {
|
for value in ctx.ldflags {
|
||||||
hasher.update(value.as_bytes());
|
hasher.update(value.as_bytes());
|
||||||
}
|
}
|
||||||
|
// Include project metadata from dcr.toml in the fingerprint
|
||||||
let toml =
|
let toml =
|
||||||
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
|
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
|
||||||
hasher.update(toml.as_bytes());
|
hasher.update(toml.as_bytes());
|
||||||
if let Ok(lock) = fs::read_to_string("dcr.lock") {
|
if let Ok(lock) = fs::read_to_string("dcr.lock") {
|
||||||
hasher.update(lock.as_bytes());
|
hasher.update(lock.as_bytes());
|
||||||
}
|
}
|
||||||
|
// Include source, header, and library files in the fingerprint
|
||||||
for source in sources {
|
for source in sources {
|
||||||
let path = Path::new(source);
|
let path = Path::new(source);
|
||||||
update_hasher_with_file(&mut hasher, path)?;
|
update_hasher_with_file(&mut hasher, path)?;
|
||||||
@@ -80,6 +93,14 @@ pub(crate) fn compute_build_fingerprint(
|
|||||||
Ok(to_hex(&hasher.finalize()))
|
Ok(to_hex(&hasher.finalize()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the build can be skipped: main artifact exists and cache hash matches.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctx`: Used to locate the output artifact and `.dcr-build.hash`.
|
||||||
|
/// - `fingerprint`: Fresh hash from [`compute_build_fingerprint`].
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` only when both the artifact and matching cache file are present.
|
||||||
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
||||||
let output = build_output_path(ctx);
|
let output = build_output_path(ctx);
|
||||||
if !Path::new(&output).is_file() {
|
if !Path::new(&output).is_file() {
|
||||||
@@ -90,6 +111,7 @@ pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
|||||||
cached.trim() == fingerprint
|
cached.trim() == fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persists the build fingerprint to the cache file on disk.
|
||||||
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
|
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
|
||||||
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
|
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
|
||||||
if let Some(parent) = cache_path.parent() {
|
if let Some(parent) = cache_path.parent() {
|
||||||
@@ -99,6 +121,7 @@ pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) ->
|
|||||||
.map_err(|err| format!("Failed to write cache: {err}"))
|
.map_err(|err| format!("Failed to write cache: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the build cache hash file based on profile and target directory.
|
||||||
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
|
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
|
||||||
@@ -106,6 +129,7 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the output path for the build artifact using the provided context.
|
||||||
pub fn build_output_path(ctx: &BuildContext) -> String {
|
pub fn build_output_path(ctx: &BuildContext) -> String {
|
||||||
crate::core::build::builder::artifact::resolve_artifact_path(
|
crate::core::build::builder::artifact::resolve_artifact_path(
|
||||||
ctx.kind,
|
ctx.kind,
|
||||||
@@ -124,6 +148,7 @@ pub fn build_output_path(ctx: &BuildContext) -> String {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects header files from source roots and include directories, filtering out excluded paths.
|
||||||
pub(crate) fn collect_header_files(
|
pub(crate) fn collect_header_files(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
@@ -165,6 +190,7 @@ pub(crate) fn collect_header_files(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively scans a directory to collect header files, respecting exclusion rules.
|
||||||
fn collect_header_files_rec(
|
fn collect_header_files_rec(
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
out: &mut Vec<PathBuf>,
|
out: &mut Vec<PathBuf>,
|
||||||
@@ -197,11 +223,13 @@ fn collect_header_files_rec(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the given path has a header file extension.
|
||||||
fn is_header_file(path: &Path) -> bool {
|
fn is_header_file(path: &Path) -> bool {
|
||||||
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
|
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
|
||||||
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
|
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects library files from lib directories using platform-specific candidates.
|
||||||
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for dir in ctx.lib_dirs {
|
for dir in ctx.lib_dirs {
|
||||||
@@ -218,6 +246,7 @@ pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Candidate library filenames for the **host** OS (`cfg!(target_os = …)`).
|
||||||
fn lib_candidates(name: &str) -> Vec<String> {
|
fn lib_candidates(name: &str) -> Vec<String> {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
return vec![format!("{name}.lib")];
|
return vec![format!("{name}.lib")];
|
||||||
@@ -236,6 +265,7 @@ fn lib_candidates(name: &str) -> Vec<String> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the hasher with the file's path, size, and last modification time.
|
||||||
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
|
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
|
||||||
hasher.update(path.to_string_lossy().as_bytes());
|
hasher.update(path.to_string_lossy().as_bytes());
|
||||||
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
|
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
|
||||||
@@ -248,6 +278,7 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a byte slice containing the SHA256 hash to a hexadecimal string.
|
||||||
fn to_hex(bytes: &[u8]) -> String {
|
fn to_hex(bytes: &[u8]) -> String {
|
||||||
crate::utils::fs::to_hex(bytes)
|
crate::utils::fs::to_hex(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,25 @@ static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
|||||||
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||||
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
|
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Returns a static mutex used to synchronize access to stderr during progress reporting.
|
||||||
|
/// This ensures thread-safe output when multiple build threads write to the terminal.
|
||||||
pub fn get_output_lock() -> &'static Mutex<()> {
|
pub fn get_output_lock() -> &'static Mutex<()> {
|
||||||
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
|
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes the shared progress label storage.
|
||||||
fn progress_label_lock() -> &'static Mutex<Option<String>> {
|
fn progress_label_lock() -> &'static Mutex<Option<String>> {
|
||||||
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
|
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the label text that will be shown next to the compile progress counter.
|
||||||
|
/// The label is cleared after the progress line is finished.
|
||||||
pub fn set_progress_label(label: Option<String>) {
|
pub fn set_progress_label(label: Option<String>) {
|
||||||
*progress_label_lock().lock().unwrap() = label;
|
*progress_label_lock().lock().unwrap() = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finishes the current progress line by clearing the label and flushing the line if it was dirty.
|
||||||
|
/// This is called when a build step completes normally.
|
||||||
pub fn finish_progress_line() {
|
pub fn finish_progress_line() {
|
||||||
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
|
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
|
||||||
*progress_label_lock().lock().unwrap() = None;
|
*progress_label_lock().lock().unwrap() = None;
|
||||||
@@ -52,6 +59,8 @@ pub fn finish_progress_line() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Interrupts the current progress line (if any) and flushes it to stderr.
|
||||||
|
/// Used when a build step produces output that should interrupt the progress display.
|
||||||
pub fn interrupt_progress_for_output() {
|
pub fn interrupt_progress_for_output() {
|
||||||
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
|
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
|
||||||
return;
|
return;
|
||||||
@@ -63,6 +72,9 @@ pub fn interrupt_progress_for_output() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reports the current compile progress to stderr in the format "label [done/total]".
|
||||||
|
/// Only prints if the terminal is attached and the label is set.
|
||||||
|
/// Updates the dirty flag so the line can be overwritten on the next call.
|
||||||
fn report_compile_progress(done: usize, total: usize) {
|
fn report_compile_progress(done: usize, total: usize) {
|
||||||
let label = progress_label_lock().lock().unwrap().clone();
|
let label = progress_label_lock().lock().unwrap().clone();
|
||||||
let Some(label) = label else {
|
let Some(label) = label else {
|
||||||
@@ -82,6 +94,9 @@ fn report_compile_progress(done: usize, total: usize) {
|
|||||||
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
|
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively collects all source files under the given roots that match the provided extensions.
|
||||||
|
/// Directories listed in exclude_dirs are skipped unless overridden by include_paths.
|
||||||
|
/// Returns a sorted list of normalized source paths or an error string.
|
||||||
pub fn collect_sources(
|
pub fn collect_sources(
|
||||||
roots: &[PathBuf],
|
roots: &[PathBuf],
|
||||||
extensions: &[&str],
|
extensions: &[&str],
|
||||||
@@ -104,6 +119,8 @@ pub fn collect_sources(
|
|||||||
Ok(sources)
|
Ok(sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively walks a directory tree to collect source files.
|
||||||
|
/// Skips excluded directories unless they are explicitly allowed by include_paths.
|
||||||
fn collect_sources_rec(
|
fn collect_sources_rec(
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
extensions: &[&str],
|
extensions: &[&str],
|
||||||
@@ -151,6 +168,8 @@ fn collect_sources_rec(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines whether a path should be excluded from source collection.
|
||||||
|
/// Returns false if the path is explicitly allowed by include_paths (even if it matches an exclude).
|
||||||
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
|
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
|
||||||
if matches_patterns(path, include_paths, true) {
|
if matches_patterns(path, include_paths, true) {
|
||||||
return false;
|
return false;
|
||||||
@@ -161,6 +180,8 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
|
|||||||
exclude_dirs.iter().any(|dir| path_is_under(path, dir))
|
exclude_dirs.iter().any(|dir| path_is_under(path, dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a path is under a given prefix, handling both absolute and relative paths
|
||||||
|
/// and normalizing Windows-style separators for cross-platform comparison.
|
||||||
fn path_is_under(path: &Path, prefix: &Path) -> bool {
|
fn path_is_under(path: &Path, prefix: &Path) -> bool {
|
||||||
if path.starts_with(prefix) {
|
if path.starts_with(prefix) {
|
||||||
return true;
|
return true;
|
||||||
@@ -180,6 +201,9 @@ fn path_is_under(path: &Path, prefix: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Matches a path against include/exclude glob and path patterns.
|
||||||
|
/// Positive patterns allow files; negative patterns (starting with !) exclude them.
|
||||||
|
/// Returns true if the path matches any rule.
|
||||||
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
||||||
if patterns.is_empty() {
|
if patterns.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
@@ -244,10 +268,20 @@ fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the string contains any glob magic characters (*, ?, or [).
|
||||||
pub fn has_glob_magic(value: &str) -> bool {
|
pub fn has_glob_magic(value: &str) -> bool {
|
||||||
value.chars().any(|c| matches!(c, '*' | '?' | '['))
|
value.chars().any(|c| matches!(c, '*' | '?' | '['))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a GCC/Clang `-x` language for assembly sources.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `source`: Source file path (extension decides the dialect).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Some("assembler-with-cpp")` for `.S`
|
||||||
|
/// - `Some("assembler")` for `.s` / `.asm`
|
||||||
|
/// - `None` for other extensions
|
||||||
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||||
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
|
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
|
||||||
match ext {
|
match ext {
|
||||||
@@ -257,6 +291,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of file extensions associated with the given language token(s).
|
||||||
|
/// Splits comma-separated tokens and falls back to .c if no language is recognized.
|
||||||
pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for part in language.split(',').map(|s| s.trim()) {
|
for part in language.split(',').map(|s| s.trim()) {
|
||||||
@@ -270,10 +306,13 @@ pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns elapsed time in seconds with two decimal places for display purposes.
|
||||||
pub fn elapsed_secs(start: Instant) -> f64 {
|
pub fn elapsed_secs(start: Instant) -> f64 {
|
||||||
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
|
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns a fixed number of worker threads to parallelize a build task.
|
||||||
|
/// Reports progress after each task completes.
|
||||||
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
|
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
|
||||||
where
|
where
|
||||||
F: Fn(usize) -> Result<(), String> + Sync + Send,
|
F: Fn(usize) -> Result<(), String> + Sync + Send,
|
||||||
@@ -330,6 +369,8 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a command, prints stdout/stderr to the terminal (interrupting progress),
|
||||||
|
/// and returns `Ok(())` or `Err` based on exit status.
|
||||||
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
||||||
let program = cmd.get_program().to_string_lossy().into_owned();
|
let program = cmd.get_program().to_string_lossy().into_owned();
|
||||||
let output = cmd.output().map_err(|err| {
|
let output = cmd.output().map_err(|err| {
|
||||||
@@ -357,6 +398,8 @@ pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes a source path to be relative to the current working directory when possible.
|
||||||
|
/// Returns the original path unchanged if it cannot be made relative.
|
||||||
pub fn normalize_source_path(path: &Path) -> String {
|
pub fn normalize_source_path(path: &Path) -> String {
|
||||||
if !path.is_absolute() {
|
if !path.is_absolute() {
|
||||||
return path.to_string_lossy().to_string();
|
return path.to_string_lossy().to_string();
|
||||||
@@ -369,6 +412,8 @@ pub fn normalize_source_path(path: &Path) -> String {
|
|||||||
path.to_string_lossy().to_string()
|
path.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the output object file path by stripping the leading ./ and adjusting the extension.
|
||||||
|
/// Places the object in the specified obj_dir while preserving directory structure.
|
||||||
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
||||||
let src_path = Path::new(source);
|
let src_path = Path::new(source);
|
||||||
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
|
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
|
||||||
@@ -401,6 +446,16 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
|||||||
out.to_string_lossy().replace('\\', "/")
|
out.to_string_lossy().replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether a source file needs to be rebuilt.
|
||||||
|
///
|
||||||
|
/// Compares mtimes of the source, object, and deps listed in the sibling `.d` file.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `source`: Source path.
|
||||||
|
/// - `object`: Object path (`.d` is derived by changing the extension).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` if the object is missing, older than source/deps, or a dep is missing.
|
||||||
pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
||||||
let src_time = fs::metadata(source).and_then(|m| m.modified());
|
let src_time = fs::metadata(source).and_then(|m| m.modified());
|
||||||
let obj_time = fs::metadata(object).and_then(|m| m.modified());
|
let obj_time = fs::metadata(object).and_then(|m| m.modified());
|
||||||
@@ -436,6 +491,14 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the output executable needs to be relinked.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `objects`: Object file paths that feed the link step.
|
||||||
|
/// - `output_path`: Linked artifact path.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` if the output is missing or any object is newer than it.
|
||||||
pub fn needs_link(objects: &[String], output_path: &str) -> bool {
|
pub fn needs_link(objects: &[String], output_path: &str) -> bool {
|
||||||
let out_meta = match fs::metadata(output_path) {
|
let out_meta = match fs::metadata(output_path) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
@@ -461,6 +524,8 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a dependency file (.d) produced by GCC or MSVC compilers.
|
||||||
|
/// Extracts all dependency paths, handling escaped newlines and Windows-style paths.
|
||||||
fn parse_d_file(content: &str) -> Vec<String> {
|
fn parse_d_file(content: &str) -> Vec<String> {
|
||||||
let mut deps = Vec::new();
|
let mut deps = Vec::new();
|
||||||
let mut target_end = 0;
|
let mut target_end = 0;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Core build engine module for DCR.
|
||||||
|
/// This module provides the main build logic for projects and workspaces.
|
||||||
use crate::core::build::builder::{BuildContext, build as build_project, collect_sources};
|
use crate::core::build::builder::{BuildContext, build as build_project, collect_sources};
|
||||||
use crate::core::build::cache::{
|
use crate::core::build::cache::{
|
||||||
collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build,
|
collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build,
|
||||||
@@ -49,6 +51,7 @@ use std::sync::Arc;
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Ensures that target directories exist for the build output.
|
||||||
fn ensure_target_dirs(
|
fn ensure_target_dirs(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
items: &[String],
|
items: &[String],
|
||||||
@@ -68,6 +71,7 @@ fn ensure_target_dirs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the project build and returns the elapsed time in seconds or an error.
|
||||||
fn run_compile(
|
fn run_compile(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
_cancel: &Arc<AtomicBool>,
|
_cancel: &Arc<AtomicBool>,
|
||||||
@@ -89,8 +93,16 @@ fn run_compile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Library entry point: run a build described by `req`, streaming progress to
|
/// Library entry point: run a build described by `req`, streaming progress to
|
||||||
// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
|
/// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `root`: Project or workspace root containing `dcr.toml`.
|
||||||
|
/// - `req`: Profile, target, force/verbose, optional member, cancel flag.
|
||||||
|
/// - `rep`: Progress sink for build events.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `BuildOutcome` with elapsed seconds, or `BuildError` on failure/cancel.
|
||||||
pub fn run_build(
|
pub fn run_build(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
req: &BuildRequest,
|
req: &BuildRequest,
|
||||||
@@ -110,6 +122,7 @@ pub fn run_build(
|
|||||||
Ok(BuildOutcome { secs })
|
Ok(BuildOutcome { secs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Orchestrates the build for the specified profile and target, handling workspaces and projects.
|
||||||
#[allow(unused_variables, clippy::too_many_arguments)]
|
#[allow(unused_variables, clippy::too_many_arguments)]
|
||||||
fn build_all(
|
fn build_all(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
@@ -279,6 +292,7 @@ fn build_all(
|
|||||||
Ok(elapsed)
|
Ok(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds each workspace member in order (flat loop, not recursive into nested workspaces).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_workspace(
|
fn build_workspace(
|
||||||
workspace: &crate::core::workspace::Workspace,
|
workspace: &crate::core::workspace::Workspace,
|
||||||
@@ -308,6 +322,7 @@ fn build_workspace(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a single project at the given root, handling configuration and steps.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_project_at(
|
fn build_project_at(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
@@ -904,6 +919,7 @@ fn build_project_at(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts relative paths in flags to absolute paths based on project root for include and link flags.
|
||||||
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
|
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
|
||||||
for prefix in prefixes {
|
for prefix in prefixes {
|
||||||
if let Some(rest) = flag.strip_prefix(prefix) {
|
if let Some(rest) = flag.strip_prefix(prefix) {
|
||||||
@@ -924,6 +940,7 @@ fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> S
|
|||||||
flag.to_string()
|
flag.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the nearest ancestor directory named "target" starting from the given dir.
|
||||||
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
|
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
|
||||||
let p = Path::new(dir);
|
let p = Path::new(dir);
|
||||||
for ancestor in p.ancestors() {
|
for ancestor in p.ancestors() {
|
||||||
@@ -934,6 +951,7 @@ fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
|
|||||||
fallback.to_path_buf()
|
fallback.to_path_buf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Packages the built library by copying headers and libs to the target include and lib directories.
|
||||||
fn package_library(
|
fn package_library(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
headers: &[PathBuf],
|
headers: &[PathBuf],
|
||||||
@@ -988,12 +1006,14 @@ fn package_library(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds the resolved executable paths for Qt tools.
|
||||||
pub(crate) struct ToolchainExecs {
|
pub(crate) struct ToolchainExecs {
|
||||||
pub(crate) uic: String,
|
pub(crate) uic: String,
|
||||||
pub(crate) moc: String,
|
pub(crate) moc: String,
|
||||||
pub(crate) rcc: String,
|
pub(crate) rcc: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the paths for uic, moc, and rcc tools, preferring configured values or pkg-config/Qt6 variants.
|
||||||
fn resolve_toolchain_execs(
|
fn resolve_toolchain_execs(
|
||||||
uic: &Option<String>,
|
uic: &Option<String>,
|
||||||
moc: &Option<String>,
|
moc: &Option<String>,
|
||||||
@@ -1008,6 +1028,7 @@ fn resolve_toolchain_execs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a Qt tool path, falling back to Qt6 variants if not found in standard locations.
|
||||||
fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &str) -> String {
|
fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &str) -> String {
|
||||||
if let Some(value) = configured {
|
if let Some(value) = configured {
|
||||||
return value.clone();
|
return value.clone();
|
||||||
@@ -1030,6 +1051,7 @@ fn resolve_qt_tool(configured: &Option<String>, qt_bins: Option<&Path>, tool: &s
|
|||||||
tool.to_string()
|
tool.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queries pkg-config for Qt6 host bin directories for the given packages.
|
||||||
fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
|
fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
|
||||||
let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect();
|
let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect();
|
||||||
if qt_pkgs.is_empty() {
|
if qt_pkgs.is_empty() {
|
||||||
@@ -1065,6 +1087,7 @@ fn resolve_qt_host_bins(pkgs: &[String]) -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes pkg-config to get a variable value for a package and returns the path if valid.
|
||||||
fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
|
fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
|
||||||
let output = std::process::Command::new("pkg-config")
|
let output = std::process::Command::new("pkg-config")
|
||||||
.arg(format!("--variable={var}"))
|
.arg(format!("--variable={var}"))
|
||||||
@@ -1082,6 +1105,7 @@ fn query_pkg_config_var(pkg: &str, var: &str) -> Option<PathBuf> {
|
|||||||
if path.is_dir() { Some(path) } else { None }
|
if path.is_dir() { Some(path) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts tool binaries from a libexec directory or its bin subdirectory.
|
||||||
fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
|
fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
|
||||||
for tool in ["moc", "uic", "rcc"] {
|
for tool in ["moc", "uic", "rcc"] {
|
||||||
if libexec.join(tool).is_file() {
|
if libexec.join(tool).is_file() {
|
||||||
@@ -1095,12 +1119,14 @@ fn qt_bins_from_libexec(libexec: &Path) -> Option<PathBuf> {
|
|||||||
if bin.is_dir() { Some(bin) } else { None }
|
if bin.is_dir() { Some(bin) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detects if a Qt6 tool variant (like uic6 or uic-qt6) is available on the PATH.
|
||||||
fn detect_qt6_tool_variant(tool: &str) -> Option<String> {
|
fn detect_qt6_tool_variant(tool: &str) -> Option<String> {
|
||||||
[format!("{tool}6"), format!("{tool}-qt6")]
|
[format!("{tool}6"), format!("{tool}-qt6")]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|candidate| is_on_path(candidate))
|
.find(|candidate| is_on_path(candidate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a command is available either as a file or in the PATH environment variable.
|
||||||
fn is_on_path(cmd: &str) -> bool {
|
fn is_on_path(cmd: &str) -> bool {
|
||||||
if Path::new(cmd).is_file() {
|
if Path::new(cmd).is_file() {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ use crate::utils::build::{is_compile_only, is_flat_bin};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Keeps only assembler-relevant flags from a cflags list.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `flags`: Full compiler flag list (often shared with C/C++).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Cloned flags that start with `-I`/`-i` or equal `-g`.
|
||||||
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
||||||
flags
|
flags
|
||||||
.iter()
|
.iter()
|
||||||
@@ -16,6 +23,19 @@ pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds assembly sources with direct flat emission when `kind` is `flat-bin`.
|
||||||
|
///
|
||||||
|
/// For non-flat kinds, assembles to objects and then links/archives as usual.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctx`: Build context (kind, sources, flags, …).
|
||||||
|
/// - `backend_name`: Label used in error messages (e.g. `"NASM"`).
|
||||||
|
/// - `default_assembler`: Assembler binary when `ctx.compiler` is empty.
|
||||||
|
/// - `extensions`: Source extensions to collect.
|
||||||
|
/// - `build_object`: Per-source assemble callback `(assembler, src, out, ctx)`.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Elapsed seconds, or an error string.
|
||||||
pub fn build_assembly<F>(
|
pub fn build_assembly<F>(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
backend_name: &str,
|
backend_name: &str,
|
||||||
@@ -36,6 +56,15 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds assembly sources; for `flat-bin`, assembles to objects then runs objcopy.
|
||||||
|
///
|
||||||
|
/// For non-flat kinds, assembles to objects and then links/archives as usual.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - Same as [`build_assembly`]; only the flat emission strategy differs.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Elapsed seconds, or an error string.
|
||||||
pub fn build_assembly_via_objcopy<F>(
|
pub fn build_assembly_via_objcopy<F>(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
backend_name: &str,
|
backend_name: &str,
|
||||||
@@ -56,12 +85,14 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flat-bin emission strategy: direct assembler output, or object then objcopy.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum FlatEmit {
|
enum FlatEmit {
|
||||||
Direct,
|
Direct,
|
||||||
ViaObjcopy,
|
ViaObjcopy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Core assembly build with a chosen flat emission mode; `build_object` runs per source in parallel.
|
||||||
fn build_assembly_with_mode<F>(
|
fn build_assembly_with_mode<F>(
|
||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
backend_name: &str,
|
backend_name: &str,
|
||||||
@@ -74,6 +105,7 @@ where
|
|||||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||||
{
|
{
|
||||||
let lang = ctx.language.to_lowercase();
|
let lang = ctx.language.to_lowercase();
|
||||||
|
// Validate language
|
||||||
if lang.split(',').any(|p| {
|
if lang.split(',').any(|p| {
|
||||||
let t = p.trim();
|
let t = p.trim();
|
||||||
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
|
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
|
||||||
@@ -104,6 +136,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let objects = match (flat, flat_mode) {
|
let objects = match (flat, flat_mode) {
|
||||||
|
// Dispatch based on flat binary kind and emission mode
|
||||||
(true, FlatEmit::Direct) => {
|
(true, FlatEmit::Direct) => {
|
||||||
let outs: Vec<String> = sources
|
let outs: Vec<String> = sources
|
||||||
.iter()
|
.iter()
|
||||||
@@ -148,6 +181,7 @@ where
|
|||||||
artifact::link_binary(ctx, &objects, "cc", start_time)
|
artifact::link_binary(ctx, &objects, "cc", start_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compiles all sources to objects in parallel (non-flat path) and returns object paths.
|
||||||
fn build_objects_common<F>(
|
fn build_objects_common<F>(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
sources: &[String],
|
sources: &[String],
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Builds an assembly file using the FASM assembler.
|
||||||
|
///
|
||||||
|
/// Delegates to the shared assembly build logic with FASM configuration.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
|
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all source files for assembly files using FASM.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
ctx.source_roots,
|
ctx.source_roots,
|
||||||
@@ -18,6 +22,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assembles one FASM source into an object file.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -28,6 +33,7 @@ fn build_object(
|
|||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip rebuilding if the object is up to date
|
||||||
if !common::needs_rebuild(source, obj_path) {
|
if !common::needs_rebuild(source, obj_path) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Builds GAS (`.s`) sources via the GNU Assembler.
|
||||||
|
///
|
||||||
|
/// Resolves `as` on PATH and delegates to the shared assembly builder. For
|
||||||
|
/// `flat-bin`, intermediates are converted with objcopy after assembly.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
// Use full path to as
|
// Use full path to as
|
||||||
let as_path = std::process::Command::new("which")
|
let as_path = std::process::Command::new("which")
|
||||||
@@ -35,6 +39,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
|
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects assembly source files with .s extension from the build context's source roots.
|
||||||
|
///
|
||||||
|
/// Excludes specified directories and respects include paths for filtering.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
ctx.source_roots,
|
ctx.source_roots,
|
||||||
@@ -44,6 +51,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assembles a single .s source file into an object file using the provided assembler.
|
||||||
|
///
|
||||||
|
/// Creates parent directories for the output object, skips if up-to-date,
|
||||||
|
/// runs the assembler with flags from the build context, and handles verbose output.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -54,10 +65,12 @@ fn build_object(
|
|||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the object file is already up to date to avoid unnecessary rebuilds.
|
||||||
if !common::needs_rebuild(source, obj_path) {
|
if !common::needs_rebuild(source, obj_path) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare the assembler command with source file and output path.
|
||||||
let mut cmd = Command::new(assembler);
|
let mut cmd = Command::new(assembler);
|
||||||
cmd.arg(source).arg("-o").arg(obj_path);
|
cmd.arg(source).arg("-o").arg(obj_path);
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Builds MASM (`.asm`) sources via `ml`.
|
||||||
|
///
|
||||||
|
/// For `flat-bin`, intermediates are converted with objcopy after assembly.
|
||||||
|
/// Returns elapsed seconds or an error.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
|
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects MASM assembly source files from the given source roots.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
ctx.source_roots,
|
ctx.source_roots,
|
||||||
@@ -18,6 +23,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a single MASM object file from the given source.
|
||||||
|
///
|
||||||
|
/// Creates the parent directory for the output object if it doesn't exist,
|
||||||
|
/// skips the build if the object is already up to date, constructs the ml
|
||||||
|
/// command line with flags, prints the command in debug mode if enabled,
|
||||||
|
/// and executes it.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -25,10 +36,12 @@ fn build_object(
|
|||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(parent) = Path::new(obj_path).parent() {
|
if let Some(parent) = Path::new(obj_path).parent() {
|
||||||
|
// Ensure the parent directory exists for the object file
|
||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
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) {
|
||||||
|
// Skip rebuild if the object file is already current
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +53,7 @@ fn build_object(
|
|||||||
.arg(source);
|
.arg(source);
|
||||||
|
|
||||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||||
|
// Add filtered compiler flags to the command
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Assembly language module for the DCR build system.
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod fasm;
|
pub mod fasm;
|
||||||
pub mod gas;
|
pub mod gas;
|
||||||
@@ -23,15 +24,21 @@ pub mod nasm;
|
|||||||
|
|
||||||
use crate::core::build::language::Language;
|
use crate::core::build::language::Language;
|
||||||
|
|
||||||
|
/// Represents the assembly language support in the build system.
|
||||||
pub struct Asm;
|
pub struct Asm;
|
||||||
|
|
||||||
impl Language for Asm {
|
impl Language for Asm {
|
||||||
|
/// Returns the identifier string for this language.
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"asm"
|
"asm"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of supported file extensions for assembly files.
|
||||||
fn extensions(&self) -> &'static [&'static str] {
|
fn extensions(&self) -> &'static [&'static str] {
|
||||||
&["s", "S", "asm"]
|
&["s", "S", "asm"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the given token matches the assembly language, using case-insensitive comparison.
|
||||||
fn matches_token(&self, token: &str) -> bool {
|
fn matches_token(&self, token: &str) -> bool {
|
||||||
token.eq_ignore_ascii_case("asm")
|
token.eq_ignore_ascii_case("asm")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Support for NASM assembly language in the DCR build system.
|
||||||
use crate::core::build::builder::BuildContext;
|
use crate::core::build::builder::BuildContext;
|
||||||
use crate::core::build::common;
|
use crate::core::build::common;
|
||||||
use crate::core::build::language::asm::common as asm;
|
use crate::core::build::language::asm::common as asm;
|
||||||
@@ -22,10 +23,16 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Builds assembly sources using the NASM assembler.
|
||||||
|
///
|
||||||
|
/// Delegates to the shared assembly builder with NASM-specific configuration.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
|
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects source files for NASM assembly.
|
||||||
|
///
|
||||||
|
/// Filters for .asm files only, as NASM uses that extension (distinct from GAS .s).
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
|
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
@@ -36,6 +43,9 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a single object file from an assembly source using NASM.
|
||||||
|
///
|
||||||
|
/// Handles directory creation, rebuild checks, format selection, and command execution.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -43,13 +53,16 @@ fn build_object(
|
|||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(parent) = Path::new(obj_path).parent() {
|
if let Some(parent) = Path::new(obj_path).parent() {
|
||||||
|
// Ensure output directory exists before building.
|
||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip building if the object is already up to date.
|
||||||
if !common::needs_rebuild(source, obj_path) {
|
if !common::needs_rebuild(source, obj_path) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select appropriate object format for the target platform and binary type.
|
||||||
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
|
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||||
"bin"
|
"bin"
|
||||||
} else {
|
} else {
|
||||||
@@ -63,6 +76,7 @@ fn build_object(
|
|||||||
.arg("-o")
|
.arg("-o")
|
||||||
.arg(obj_path);
|
.arg(obj_path);
|
||||||
|
|
||||||
|
// Append any additional assembly flags from build context.
|
||||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
}
|
}
|
||||||
@@ -74,6 +88,9 @@ fn build_object(
|
|||||||
common::run_command_sync_output(&mut cmd)
|
common::run_command_sync_output(&mut cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects the NASM object file format string based on the target platform.
|
||||||
|
///
|
||||||
|
/// Supports different formats for x86 on various OSes and architectures.
|
||||||
fn nasm_format(platform: Option<&str>) -> &'static str {
|
fn nasm_format(platform: Option<&str>) -> &'static str {
|
||||||
if let Some(p) = platform {
|
if let Some(p) = platform {
|
||||||
let p = p.to_lowercase().replace('-', "_");
|
let p = p.to_lowercase().replace('-', "_");
|
||||||
|
|||||||
@@ -17,15 +17,21 @@
|
|||||||
|
|
||||||
use crate::core::build::language::Language;
|
use crate::core::build::language::Language;
|
||||||
|
|
||||||
|
/// Represents the C programming language support within the DCR build system.
|
||||||
pub struct C;
|
pub struct C;
|
||||||
|
|
||||||
impl Language for C {
|
impl Language for C {
|
||||||
|
/// Returns the unique identifier for the C language.
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"c"
|
"c"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an array containing the file extension associated with C source code.
|
||||||
fn extensions(&self) -> &'static [&'static str] {
|
fn extensions(&self) -> &'static [&'static str] {
|
||||||
&["c"]
|
&["c"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the given token matches the C language identifier in a case-insensitive manner.
|
||||||
fn matches_token(&self, token: &str) -> bool {
|
fn matches_token(&self, token: &str) -> bool {
|
||||||
token.eq_ignore_ascii_case("c")
|
token.eq_ignore_ascii_case("c")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,26 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// C++ language support for the DCR build system.
|
||||||
pub mod qt;
|
pub mod qt;
|
||||||
|
|
||||||
use crate::core::build::language::Language;
|
use crate::core::build::language::Language;
|
||||||
|
|
||||||
pub struct Cxx;
|
pub struct Cxx;
|
||||||
|
|
||||||
|
/// C++ language implementation.
|
||||||
impl Language for Cxx {
|
impl Language for Cxx {
|
||||||
|
/// Returns the language ID "cxx".
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"cxx"
|
"cxx"
|
||||||
}
|
}
|
||||||
|
/// Returns the extensions for C++ files.
|
||||||
fn extensions(&self) -> &'static [&'static str] {
|
fn extensions(&self) -> &'static [&'static str] {
|
||||||
&["cpp", "cxx", "cc"]
|
&["cpp", "cxx", "cc"]
|
||||||
}
|
}
|
||||||
|
/// Checks if the token matches C++ language names, ignoring case.
|
||||||
fn matches_token(&self, token: &str) -> bool {
|
fn matches_token(&self, token: &str) -> bool {
|
||||||
|
// Case-insensitive comparison using to_lowercase
|
||||||
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
|
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Runs uic/rcc/moc on Qt UI, resource, and header sources under `build_dir/qt`.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctx`: Sources, excludes, and toolchain tool paths.
|
||||||
|
/// - `build_dir`: Directory that will contain the generated `qt/` tree.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(Some(qt_dir))` if any generated files were produced.
|
||||||
|
/// - `Ok(None)` if there was nothing to process.
|
||||||
|
/// - `Err` if a Qt tool fails.
|
||||||
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
|
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
|
||||||
let qt_dir = build_dir.join("qt");
|
let qt_dir = build_dir.join("qt");
|
||||||
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
|
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
|
||||||
@@ -37,6 +47,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
|||||||
let mut qt_moc_args = Vec::new();
|
let mut qt_moc_args = Vec::new();
|
||||||
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
|
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
|
||||||
|
|
||||||
|
// Collect -I and -D flags from pkg-config for Qt modules to use in moc invocations
|
||||||
for module in qt_modules {
|
for module in qt_modules {
|
||||||
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
|
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
|
||||||
for flag in cflags.split_whitespace() {
|
for flag in cflags.split_whitespace() {
|
||||||
@@ -123,6 +134,7 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the specified command with the given arguments and returns success or an error message.
|
||||||
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
|
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
|
||||||
let mut cmd = Command::new(cmd_path);
|
let mut cmd = Command::new(cmd_path);
|
||||||
cmd.args(args);
|
cmd.args(args);
|
||||||
|
|||||||
@@ -6,24 +6,32 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Represents the LLVM IR language implementation for the build system.
|
||||||
pub struct LlvmIr;
|
pub struct LlvmIr;
|
||||||
|
|
||||||
impl Language for LlvmIr {
|
impl Language for LlvmIr {
|
||||||
|
/// Returns the unique identifier for the LLVM IR language.
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"llvm_ir"
|
"llvm_ir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of file extensions recognized for this language.
|
||||||
fn extensions(&self) -> &'static [&'static str] {
|
fn extensions(&self) -> &'static [&'static str] {
|
||||||
&["ll"]
|
&["ll"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines whether the provided token matches the LLVM IR language.
|
||||||
fn matches_token(&self, token: &str) -> bool {
|
fn matches_token(&self, token: &str) -> bool {
|
||||||
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
|
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds LLVM IR source files into object files using llc.
|
||||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||||
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
|
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects source files for LLVM IR compilation from the given context.
|
||||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||||
common::collect_sources(
|
common::collect_sources(
|
||||||
ctx.source_roots,
|
ctx.source_roots,
|
||||||
@@ -33,6 +41,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a single LLVM IR source file into an object file.
|
||||||
fn build_object(
|
fn build_object(
|
||||||
assembler: &str,
|
assembler: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
@@ -40,6 +49,7 @@ fn build_object(
|
|||||||
ctx: &BuildContext,
|
ctx: &BuildContext,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(parent) = Path::new(obj_path).parent() {
|
if let Some(parent) = Path::new(obj_path).parent() {
|
||||||
|
// Create parent directory if it does not exist
|
||||||
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,21 +15,29 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Core module for language support in the DCR build system.
|
||||||
|
/// Defines the Language trait and helper functions to register and lookup languages.
|
||||||
pub mod asm;
|
pub mod asm;
|
||||||
pub mod c;
|
pub mod c;
|
||||||
pub mod cxx;
|
pub mod cxx;
|
||||||
pub mod llvm_ir;
|
pub mod llvm_ir;
|
||||||
|
|
||||||
|
/// Trait representing a supported programming language.
|
||||||
|
/// Each implementation must specify its unique identifier, list of supported file extensions,
|
||||||
|
/// and a method to determine if a given token matches the language.
|
||||||
pub trait Language {
|
pub trait Language {
|
||||||
fn id(&self) -> &'static str;
|
fn id(&self) -> &'static str;
|
||||||
fn extensions(&self) -> &'static [&'static str];
|
fn extensions(&self) -> &'static [&'static str];
|
||||||
fn matches_token(&self, token: &str) -> bool;
|
fn matches_token(&self, token: &str) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a fixed-size array containing static references to all supported language types.
|
||||||
pub fn languages() -> [&'static dyn Language; 4] {
|
pub fn languages() -> [&'static dyn Language; 4] {
|
||||||
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
|
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up the language implementation that matches the given token by checking each language's
|
||||||
|
/// matches_token method. Returns the first match or None if no language matches.
|
||||||
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
||||||
languages().into_iter().find(|l| l.matches_token(token))
|
languages().into_iter().find(|l| l.matches_token(token))
|
||||||
}
|
}
|
||||||
@@ -38,6 +46,7 @@ pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Ensures that input tokens are resolved to the correct language IDs.
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_resolve_to_languages() {
|
fn tokens_resolve_to_languages() {
|
||||||
assert_eq!(language_for_token("c").unwrap().id(), "c");
|
assert_eq!(language_for_token("c").unwrap().id(), "c");
|
||||||
@@ -48,6 +57,7 @@ mod tests {
|
|||||||
assert!(language_for_token("rust").is_none());
|
assert!(language_for_token("rust").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures that each language reports the expected set of file extensions.
|
||||||
#[test]
|
#[test]
|
||||||
fn languages_expose_expected_extensions() {
|
fn languages_expose_expected_extensions() {
|
||||||
assert_eq!(c::C.extensions(), &["c"]);
|
assert_eq!(c::C.extensions(), &["c"]);
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Core build module for DCR — a Cargo-like C/C++ project manager.
|
||||||
|
/// This module defines the build engine and re-exports key components for
|
||||||
|
/// building C/C++ projects.
|
||||||
#[cfg(feature = "archive")]
|
#[cfg(feature = "archive")]
|
||||||
pub mod archive;
|
pub mod archive;
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
use std::sync::{Arc, atomic::AtomicBool};
|
use std::sync::{Arc, atomic::AtomicBool};
|
||||||
|
|
||||||
|
/// Represents a build request with options for the build process, including profile selection, target specification, force flag, verbosity, workspace, and a cancellation signal.
|
||||||
pub struct BuildRequest {
|
pub struct BuildRequest {
|
||||||
pub profile: String,
|
pub profile: String,
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
@@ -30,21 +31,25 @@ pub struct BuildRequest {
|
|||||||
pub cancel: Arc<AtomicBool>,
|
pub cancel: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Contains the result of a build operation, specifically the duration in seconds.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct BuildOutcome {
|
pub struct BuildOutcome {
|
||||||
pub secs: f64,
|
pub secs: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps a string message as a build error.
|
||||||
pub struct BuildError {
|
pub struct BuildError {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a BuildError from a String message.
|
||||||
impl From<String> for BuildError {
|
impl From<String> for BuildError {
|
||||||
fn from(message: String) -> Self {
|
fn from(message: String) -> Self {
|
||||||
BuildError { message }
|
BuildError { message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents events that the build engine can emit during compilation and packaging.
|
||||||
pub enum BuildEvent<'a> {
|
pub enum BuildEvent<'a> {
|
||||||
TargetStart {
|
TargetStart {
|
||||||
index: usize,
|
index: usize,
|
||||||
@@ -81,6 +86,8 @@ pub enum BuildEvent<'a> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for handling build events in different reporting backends.
|
||||||
pub trait BuildReporter {
|
pub trait BuildReporter {
|
||||||
|
/// Callback invoked when a build event is generated.
|
||||||
fn on_event(&mut self, event: BuildEvent<'_>);
|
fn on_event(&mut self, event: BuildEvent<'_>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
|
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
|
||||||
|
|
||||||
|
/// Retrieves a raw TOML value from the config using a prioritized lookup order based on target, profile, and base sections.
|
||||||
|
///
|
||||||
|
/// The keys are checked in this order:
|
||||||
|
/// {section}.{target}.{profile}.{field}, {section}.{profile}.{target}.{field}, {section}.{target}.{field}, {section}.{profile}.{field}, {section}.{field}
|
||||||
|
///
|
||||||
|
/// Returns the value if found, otherwise None.
|
||||||
pub(crate) fn get_config_value_raw(
|
pub(crate) fn get_config_value_raw(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
section: &str,
|
section: &str,
|
||||||
@@ -57,6 +63,9 @@ pub(crate) fn get_config_value_raw(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the 'inherit' flag is enabled for the given section, profile, and target.
|
||||||
|
///
|
||||||
|
/// Returns true by default if the flag is not set.
|
||||||
pub(crate) fn get_inherit(
|
pub(crate) fn get_inherit(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
section: &str,
|
section: &str,
|
||||||
@@ -68,6 +77,9 @@ pub(crate) fn get_inherit(
|
|||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts a trimmed non-empty string value from the config.
|
||||||
|
///
|
||||||
|
/// Returns Some(value) if found, None otherwise.
|
||||||
pub(crate) fn get_config_value(
|
pub(crate) fn get_config_value(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
section: &str,
|
section: &str,
|
||||||
@@ -80,6 +92,9 @@ pub(crate) fn get_config_value(
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches a string value from the build section, inheriting from base if enabled.
|
||||||
|
///
|
||||||
|
/// Returns the value or default empty string if not inheriting.
|
||||||
pub(crate) fn get_string_with_profile_and_target(
|
pub(crate) fn get_string_with_profile_and_target(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -94,10 +109,12 @@ pub(crate) fn get_string_with_profile_and_target(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a build string value for the given profile without a target.
|
||||||
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||||
get_string_with_profile_and_target(config, field, profile, None)
|
get_string_with_profile_and_target(config, field, profile, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a string value from the language-specific build.{lang} section.
|
||||||
pub(crate) fn get_lang_string(
|
pub(crate) fn get_lang_string(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
lang: &str,
|
lang: &str,
|
||||||
@@ -108,6 +125,7 @@ pub(crate) fn get_lang_string(
|
|||||||
get_config_value(config, &format!("build.{lang}"), field, profile, target)
|
get_config_value(config, &format!("build.{lang}"), field, profile, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a list of strings from the language-specific build.{lang} section.
|
||||||
pub(crate) fn get_lang_list(
|
pub(crate) fn get_lang_list(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
lang: &str,
|
lang: &str,
|
||||||
@@ -121,6 +139,7 @@ pub(crate) fn get_lang_list(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a TOML array into a Vec<String>, erroring if not an array of strings.
|
||||||
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
||||||
let arr = value
|
let arr = value
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -135,6 +154,9 @@ pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<S
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects a list of strings from config, supporting inheritance from base and profile/target overrides.
|
||||||
|
///
|
||||||
|
/// If inherit is true, starts with the base list and appends custom entries if defined.
|
||||||
pub(crate) fn get_list_with_profile_and_target(
|
pub(crate) fn get_list_with_profile_and_target(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -154,6 +176,7 @@ pub(crate) fn get_list_with_profile_and_target(
|
|||||||
.get(&format!("build.{field}"))
|
.get(&format!("build.{field}"))
|
||||||
.map(|v| v == &val)
|
.map(|v| v == &val)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
// Skip re-adding the base list when this is the base definition to prevent duplication
|
||||||
if !is_base {
|
if !is_base {
|
||||||
let custom = parse_string_array(&val, &format!("build.{field}"))?;
|
let custom = parse_string_array(&val, &format!("build.{field}"))?;
|
||||||
if inherit {
|
if inherit {
|
||||||
@@ -184,6 +207,7 @@ pub(crate) fn get_list_with_profile_and_target(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the list of strings for the given profile without a target.
|
||||||
pub(crate) fn get_list_with_profile(
|
pub(crate) fn get_list_with_profile(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -192,10 +216,21 @@ pub(crate) fn get_list_with_profile(
|
|||||||
get_list_with_profile_and_target(config, field, profile, None)
|
get_list_with_profile_and_target(config, field, profile, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of targets for the given profile.
|
||||||
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
|
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
|
||||||
get_list_with_profile(config, "targets", profile)
|
get_list_with_profile(config, "targets", profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the config value as a list of strings.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `config`: Project config.
|
||||||
|
/// - `key`: Dot-path key (e.g. `build.sources`).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok([])` if the key is missing.
|
||||||
|
/// - `Ok(vec)` for an array of strings.
|
||||||
|
/// - `Err` if present but not an array of strings.
|
||||||
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
|
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
|
||||||
let value = match config.get(key) {
|
let value = match config.get(key) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub(crate) struct BuildStep {
|
|||||||
cmd: String,
|
cmd: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds interpolated version and profile strings for build step substitution.
|
||||||
pub(crate) struct StepVars<'a> {
|
pub(crate) struct StepVars<'a> {
|
||||||
pub(crate) profile: &'a str,
|
pub(crate) profile: &'a str,
|
||||||
pub(crate) version: &'a str,
|
pub(crate) version: &'a str,
|
||||||
@@ -40,6 +41,7 @@ pub(crate) struct StepVars<'a> {
|
|||||||
pub(crate) version_suffix_dash: &'a str,
|
pub(crate) version_suffix_dash: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses TOML array of build step tables into Vec<BuildStep>.
|
||||||
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
|
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||||
let arr = value
|
let arr = value
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -86,6 +88,7 @@ fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<Buil
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves build steps for a specific profile from config, falling back to default if not present.
|
||||||
pub(crate) fn get_build_steps_with_profile(
|
pub(crate) fn get_build_steps_with_profile(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -99,6 +102,7 @@ pub(crate) fn get_build_steps_with_profile(
|
|||||||
get_build_steps(config, &format!("build.{field}"))
|
get_build_steps(config, &format!("build.{field}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets build steps from config for the given key.
|
||||||
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
|
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||||
let value = match config.get(key) {
|
let value = match config.get(key) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
@@ -110,6 +114,7 @@ fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String>
|
|||||||
use crate::core::build::report::{BuildEvent, BuildReporter};
|
use crate::core::build::report::{BuildEvent, BuildReporter};
|
||||||
use std::sync::{Arc, atomic::AtomicBool};
|
use std::sync::{Arc, atomic::AtomicBool};
|
||||||
|
|
||||||
|
/// Runs all build steps in sequence, checking for cancellation and reporting progress.
|
||||||
pub(crate) fn run_build_steps(
|
pub(crate) fn run_build_steps(
|
||||||
steps: &[BuildStep],
|
steps: &[BuildStep],
|
||||||
tools: &ToolchainExecs,
|
tools: &ToolchainExecs,
|
||||||
@@ -127,6 +132,7 @@ pub(crate) fn run_build_steps(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes a single build step, handling glob expansion, output paths, and command substitution.
|
||||||
fn run_build_step(
|
fn run_build_step(
|
||||||
step: &BuildStep,
|
step: &BuildStep,
|
||||||
tools: &ToolchainExecs,
|
tools: &ToolchainExecs,
|
||||||
@@ -137,6 +143,7 @@ fn run_build_step(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||||
let inputs = expand_glob(&input_pattern)?;
|
let inputs = expand_glob(&input_pattern)?;
|
||||||
|
// Skip step if no matching input files found
|
||||||
if inputs.is_empty() {
|
if inputs.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -170,6 +177,7 @@ fn run_build_step(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes files (not directories) matching the given glob patterns.
|
||||||
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
||||||
for pattern in patterns {
|
for pattern in patterns {
|
||||||
for path in expand_glob(pattern)? {
|
for path in expand_glob(pattern)? {
|
||||||
@@ -181,6 +189,7 @@ pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands glob patterns to find matching files.
|
||||||
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
|
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
|
||||||
@@ -191,6 +200,7 @@ pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies that expected artifacts are present by expanding patterns.
|
||||||
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
|
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
|
||||||
for pattern in patterns {
|
for pattern in patterns {
|
||||||
let expanded = expand_step_value(pattern, "", vars);
|
let expanded = expand_step_value(pattern, "", vars);
|
||||||
@@ -202,6 +212,7 @@ pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Resul
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if the step should run: missing/unreadable output, unreadable input, or input newer than output.
|
||||||
fn should_run_step(input: &Path, output: &Path) -> bool {
|
fn should_run_step(input: &Path, output: &Path) -> bool {
|
||||||
let in_time = fs::metadata(input).and_then(|m| m.modified());
|
let in_time = fs::metadata(input).and_then(|m| m.modified());
|
||||||
let out_time = fs::metadata(output).and_then(|m| m.modified());
|
let out_time = fs::metadata(output).and_then(|m| m.modified());
|
||||||
@@ -212,6 +223,7 @@ fn should_run_step(input: &Path, output: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Substitutes variables into step command template and replaces placeholders.
|
||||||
fn substitute_step_cmd(
|
fn substitute_step_cmd(
|
||||||
template: &str,
|
template: &str,
|
||||||
input: &Path,
|
input: &Path,
|
||||||
@@ -232,6 +244,7 @@ fn substitute_step_cmd(
|
|||||||
.replace("{stem}", stem)
|
.replace("{stem}", stem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a list of compiler flags from cflags, include dirs, and compiler type.
|
||||||
pub(crate) fn build_step_flags(
|
pub(crate) fn build_step_flags(
|
||||||
cflags: &[String],
|
cflags: &[String],
|
||||||
include_dirs: &[String],
|
include_dirs: &[String],
|
||||||
@@ -264,6 +277,7 @@ pub(crate) fn build_step_flags(
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Quotes argument if it contains whitespace or double quotes.
|
||||||
fn quote_step_arg(arg: String) -> String {
|
fn quote_step_arg(arg: String) -> String {
|
||||||
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
|
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
|
||||||
return arg;
|
return arg;
|
||||||
@@ -272,6 +286,7 @@ fn quote_step_arg(arg: String) -> String {
|
|||||||
format!("\"{escaped}\"")
|
format!("\"{escaped}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if compiler is MSVC based on name.
|
||||||
fn is_msvc_compiler(compiler: &str) -> bool {
|
fn is_msvc_compiler(compiler: &str) -> bool {
|
||||||
let lower = compiler.to_lowercase();
|
let lower = compiler.to_lowercase();
|
||||||
lower.contains("cl.exe")
|
lower.contains("cl.exe")
|
||||||
@@ -280,6 +295,7 @@ fn is_msvc_compiler(compiler: &str) -> bool {
|
|||||||
|| lower.contains("msvc")
|
|| lower.contains("msvc")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs shell command with output capture and cancellation support.
|
||||||
fn run_shell_command(
|
fn run_shell_command(
|
||||||
cmd: &str,
|
cmd: &str,
|
||||||
cancel: &Arc<AtomicBool>,
|
cancel: &Arc<AtomicBool>,
|
||||||
@@ -352,6 +368,7 @@ fn run_shell_command(
|
|||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if any build step needs to be run by comparing timestamps.
|
||||||
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
|
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
|
||||||
for step in steps {
|
for step in steps {
|
||||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||||
@@ -380,6 +397,7 @@ pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Resu
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates VersionInfo from StepVars.
|
||||||
fn make_version_info(vars: &StepVars) -> VersionInfo {
|
fn make_version_info(vars: &StepVars) -> VersionInfo {
|
||||||
VersionInfo {
|
VersionInfo {
|
||||||
full: vars.version.to_string(),
|
full: vars.version.to_string(),
|
||||||
@@ -391,6 +409,7 @@ fn make_version_info(vars: &StepVars) -> VersionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands step value by substituting vars and stem.
|
||||||
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
|
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
|
||||||
let info = make_version_info(vars);
|
let info = make_version_info(vars);
|
||||||
let s = substitute_vars(template, &info, vars.profile, "");
|
let s = substitute_vars(template, &info, vars.profile, "");
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const VALID_KINDS: &[&str] = &[
|
|||||||
"flat-bin",
|
"flat-bin",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Represents errors that can occur during configuration loading, parsing, or validation.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
@@ -80,6 +81,7 @@ impl From<toml::ser::Error> for ConfigError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Core configuration structure that manages TOML parsing, typed views, and validation for DCR projects.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
data: Value,
|
data: Value,
|
||||||
@@ -91,6 +93,7 @@ pub struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Top-level typed configuration parsed from TOML, containing optional sections for package, build, dependencies, etc.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct DcrConfig {
|
pub struct DcrConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -108,6 +111,7 @@ pub struct DcrConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Configuration for archive output formats (e.g. EFI, bootable images).
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ArchiveConfig {
|
pub struct ArchiveConfig {
|
||||||
pub output: String,
|
pub output: String,
|
||||||
@@ -121,12 +125,14 @@ pub struct ArchiveConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Layout definition for archive sections (from/to paths).
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ArchiveLayout {
|
pub struct ArchiveLayout {
|
||||||
pub from: String,
|
pub from: String,
|
||||||
pub to: String,
|
pub to: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Package metadata section (name, version, type).
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct PackageConfig {
|
pub struct PackageConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -144,6 +150,7 @@ fn default_compiler() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Build configuration section, including language, standards, compiler, and output settings.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct BuildConfig {
|
pub struct BuildConfig {
|
||||||
#[serde(default = "default_language")]
|
#[serde(default = "default_language")]
|
||||||
@@ -182,6 +189,7 @@ pub struct BuildConfig {
|
|||||||
pub qt: Option<bool>,
|
pub qt: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Language configuration can be a single string or array of strings (e.g. for multi-language builds).
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum LanguageConfig {
|
pub enum LanguageConfig {
|
||||||
@@ -198,6 +206,7 @@ impl LanguageConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dependency configuration can be a version string or a full table of dependency details.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@@ -206,6 +215,7 @@ pub enum DependencyConfig {
|
|||||||
Table(BTreeMap<String, Value>),
|
Table(BTreeMap<String, Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toolchain configuration for custom compilers, assemblers, and linkers.
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
pub struct ToolchainConfig {
|
pub struct ToolchainConfig {
|
||||||
pub cc: Option<String>,
|
pub cc: Option<String>,
|
||||||
@@ -216,6 +226,7 @@ pub struct ToolchainConfig {
|
|||||||
pub ld: Option<String>,
|
pub ld: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Workspace member configuration for multi-project workspaces.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct WorkspaceMemberConfig {
|
pub struct WorkspaceMemberConfig {
|
||||||
@@ -227,6 +238,7 @@ pub struct WorkspaceMemberConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one.
|
||||||
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
let (data, doc) = if path.exists() {
|
let (data, doc) = if path.exists() {
|
||||||
@@ -249,6 +261,7 @@ impl Config {
|
|||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens an existing Config from a TOML file path, failing if the file does not exist.
|
||||||
pub fn open(path: &str) -> Result<Self, ConfigError> {
|
pub fn open(path: &str) -> Result<Self, ConfigError> {
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@@ -267,16 +280,19 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Returns the fully typed configuration struct.
|
||||||
pub fn typed(&self) -> &DcrConfig {
|
pub fn typed(&self) -> &DcrConfig {
|
||||||
&self.typed
|
&self.typed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Returns the package configuration if present.
|
||||||
pub fn package(&self) -> Option<&PackageConfig> {
|
pub fn package(&self) -> Option<&PackageConfig> {
|
||||||
self.typed.package.as_ref()
|
self.typed.package.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Returns the build configuration if present.
|
||||||
pub fn build_config(&self) -> Option<&BuildConfig> {
|
pub fn build_config(&self) -> Option<&BuildConfig> {
|
||||||
self.typed.build.as_ref()
|
self.typed.build.as_ref()
|
||||||
}
|
}
|
||||||
@@ -290,7 +306,7 @@ impl Config {
|
|||||||
&& !self.typed.workspace.is_empty()
|
&& !self.typed.workspace.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge build fields from parent config into self where self has empty/default values.
|
/// Merges build fields from a parent config into self where self has empty/default values.
|
||||||
/// Used for workspace member inheritance (build.inherit = true).
|
/// Used for workspace member inheritance (build.inherit = true).
|
||||||
pub fn merge_parent(&mut self, parent: &Config) {
|
pub fn merge_parent(&mut self, parent: &Config) {
|
||||||
let fields: &[&str] = &[
|
let fields: &[&str] = &[
|
||||||
@@ -368,24 +384,29 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves a nested value from the config data using dot-separated path.
|
||||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||||
let parts: Vec<&str> = key.split('.').collect();
|
let parts: Vec<&str> = key.split('.').collect();
|
||||||
get_path(&self.data, &parts)
|
get_path(&self.data, &parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Adds a new key-value pair to the configuration (alias for set).
|
||||||
pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||||
self.set(key, value)
|
self.set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates a configuration value at the given key path.
|
||||||
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||||
self.set(key, value)
|
self.set(key, value)
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// Checks if the configuration is valid without returning the error.
|
||||||
pub fn check(&self) -> bool {
|
pub fn check(&self) -> bool {
|
||||||
self.validate().is_ok()
|
self.validate().is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs comprehensive validation of the configuration, including package, build, toolchain, and workspace rules.
|
||||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||||
if self.is_workspace_only() {
|
if self.is_workspace_only() {
|
||||||
return self.validate_workspace();
|
return self.validate_workspace();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// Represents resolved dependencies for a build target, including include directories, library directories, and libraries.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ResolvedDeps {
|
pub struct ResolvedDeps {
|
||||||
pub include_dirs: Vec<String>,
|
pub include_dirs: Vec<String>,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Represents a locked dependency entry containing its name, version, source, and checksum.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DepLock {
|
pub struct DepLock {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -26,6 +27,7 @@ pub struct DepLock {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the dcr.lock file in a Cargo-like format. The project entry comes first, followed by dependency entries.
|
||||||
pub fn write_lock(
|
pub fn write_lock(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
project_name: &str,
|
project_name: &str,
|
||||||
@@ -47,6 +49,7 @@ pub fn write_lock(
|
|||||||
}
|
}
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
|
|
||||||
|
// Write metadata for each package in the dependencies list
|
||||||
for pkg in packages {
|
for pkg in packages {
|
||||||
out.push_str("[[package]]\n");
|
out.push_str("[[package]]\n");
|
||||||
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
|
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
|
||||||
@@ -60,6 +63,7 @@ pub fn write_lock(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a list of strings into a comma-separated TOML array by quoting each item.
|
||||||
fn quote_list(items: &[String]) -> String {
|
fn quote_list(items: &[String]) -> String {
|
||||||
items
|
items
|
||||||
.iter()
|
.iter()
|
||||||
@@ -68,6 +72,7 @@ fn quote_list(items: &[String]) -> String {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escapes backslashes and double quotes in a string to make it safe for TOML.
|
||||||
fn escape_value(input: &str) -> String {
|
fn escape_value(input: &str) -> String {
|
||||||
input.replace('\\', "\\\\").replace('"', "\\\"")
|
input.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Core dependency resolver for DCR projects.
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod lock;
|
pub mod lock;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
@@ -24,6 +25,7 @@ use crate::core::deps::common::ResolvedDeps;
|
|||||||
use crate::core::deps::lock::{DepLock, write_lock};
|
use crate::core::deps::lock::{DepLock, write_lock};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
|
||||||
fn dep_version(path: &Path) -> String {
|
fn dep_version(path: &Path) -> String {
|
||||||
Config::open(&path.join("dcr.toml").to_string_lossy())
|
Config::open(&path.join("dcr.toml").to_string_lossy())
|
||||||
.ok()
|
.ok()
|
||||||
@@ -35,6 +37,18 @@ fn dep_version(path: &Path) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
|
||||||
|
///
|
||||||
|
/// Registry and path deps contribute to `ResolvedDeps`. Git deps are recorded in the lock only
|
||||||
|
/// (no fetch or include/lib resolution).
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `config`: Loaded `dcr.toml`.
|
||||||
|
/// - `_profile` / `_target`: Reserved for future profile/target-specific deps.
|
||||||
|
/// - `project_root`: Root used to resolve path deps and write `dcr.lock`.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Aggregated include/lib/libs, or an error string on resolve/lock failure.
|
||||||
pub fn resolve_deps(
|
pub fn resolve_deps(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
_profile: &str,
|
_profile: &str,
|
||||||
@@ -57,6 +71,7 @@ pub fn resolve_deps(
|
|||||||
let mut lock_packages: Vec<DepLock> = Vec::new();
|
let mut lock_packages: Vec<DepLock> = Vec::new();
|
||||||
|
|
||||||
if let Some(deps) = deps_table {
|
if let Some(deps) = deps_table {
|
||||||
|
// Process each dependency declaration in the TOML table
|
||||||
for (name, value) in deps {
|
for (name, value) in deps {
|
||||||
if register::is_registry_dep(value) {
|
if register::is_registry_dep(value) {
|
||||||
let pkg_info = register::resolve_package_from_registry(name)?;
|
let pkg_info = register::resolve_package_from_registry(name)?;
|
||||||
@@ -157,6 +172,7 @@ pub fn resolve_deps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write the lock file with all resolved dependencies
|
||||||
write_lock(
|
write_lock(
|
||||||
project_root,
|
project_root,
|
||||||
&project_name,
|
&project_name,
|
||||||
@@ -167,11 +183,13 @@ pub fn resolve_deps(
|
|||||||
Ok(resolved)
|
Ok(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Git dependency specification (url + optional version/ref for the lockfile).
|
||||||
struct GitDep<'a> {
|
struct GitDep<'a> {
|
||||||
url: &'a str,
|
url: &'a str,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a git dependency from a TOML value table.
|
||||||
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
||||||
if let Some(table) = value.as_table()
|
if let Some(table) = value.as_table()
|
||||||
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
|
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
|
||||||
@@ -185,6 +203,7 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the path for a path-based dependency, supporting both table format and legacy string format.
|
||||||
fn path_dep_path(value: &toml::Value) -> Option<&str> {
|
fn path_dep_path(value: &toml::Value) -> Option<&str> {
|
||||||
if let Some(table) = value.as_table() {
|
if let Some(table) = value.as_table() {
|
||||||
return table.get("path").and_then(|v| v.as_str());
|
return table.get("path").and_then(|v| v.as_str());
|
||||||
@@ -192,12 +211,14 @@ fn path_dep_path(value: &toml::Value) -> Option<&str> {
|
|||||||
register::path_from_string_dep(value)
|
register::path_from_string_dep(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a directory path to the list if it exists on the filesystem.
|
||||||
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
|
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
paths.push(path.to_string_lossy().to_string());
|
paths.push(path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds common library directory candidates to the list if they exist.
|
||||||
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
||||||
for dir in ["lib", "lib64"] {
|
for dir in ["lib", "lib64"] {
|
||||||
push_if_exists(paths, &dep_root.join(dir));
|
push_if_exists(paths, &dep_root.join(dir));
|
||||||
@@ -210,6 +231,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
|
/// Tests that path_dep_path correctly handles both TOML table and legacy string inputs for path dependencies.
|
||||||
#[test]
|
#[test]
|
||||||
fn path_dep_path_supports_table_and_legacy_strings() {
|
fn path_dep_path_supports_table_and_legacy_strings() {
|
||||||
let table = Value::Table(
|
let table = Value::Table(
|
||||||
@@ -232,6 +254,7 @@ mod tests {
|
|||||||
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
|
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
|
||||||
#[test]
|
#[test]
|
||||||
fn default_lib_dirs_include_packaged_library_output() {
|
fn default_lib_dirs_include_packaged_library_output() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
|||||||
@@ -21,25 +21,30 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
|
/// Represents a registry configuration entry with its URL and priority.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Registry {
|
pub struct Registry {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration struct for DCR registries, mapping registry names to Registry details.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct DcrConfig {
|
pub struct DcrConfig {
|
||||||
pub registry: std::collections::HashMap<String, Registry>,
|
pub registry: std::collections::HashMap<String, Registry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to get the user's home directory path.
|
||||||
fn home_dir() -> Option<PathBuf> {
|
fn home_dir() -> Option<PathBuf> {
|
||||||
crate::utils::fs::home_dir()
|
crate::utils::fs::home_dir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to get the DCR config directory path.
|
||||||
fn dcr_config_dir() -> Option<PathBuf> {
|
fn dcr_config_dir() -> Option<PathBuf> {
|
||||||
home_dir().map(|h| h.join(".dcr"))
|
home_dir().map(|h| h.join(".dcr"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads DCR registry configuration from the TOML file in the user's home directory.
|
||||||
pub fn get_registry_config() -> Option<DcrConfig> {
|
pub fn get_registry_config() -> Option<DcrConfig> {
|
||||||
let home = home_dir()?;
|
let home = home_dir()?;
|
||||||
let config_path = home.join(".dcr/config.toml");
|
let config_path = home.join(".dcr/config.toml");
|
||||||
@@ -51,6 +56,7 @@ pub fn get_registry_config() -> Option<DcrConfig> {
|
|||||||
toml::from_str(&content).ok()
|
toml::from_str(&content).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the registry index file, preferring the DCR_INDEX_PATH env var or falling back to ~/.dcr/index.json.
|
||||||
pub fn get_index_path() -> PathBuf {
|
pub fn get_index_path() -> PathBuf {
|
||||||
std::env::var("DCR_INDEX_PATH")
|
std::env::var("DCR_INDEX_PATH")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
@@ -61,6 +67,7 @@ pub fn get_index_path() -> PathBuf {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the root directory for registry cache, based on the index path's parent, creating it if necessary.
|
||||||
pub fn get_registry_cache_root() -> PathBuf {
|
pub fn get_registry_cache_root() -> PathBuf {
|
||||||
let index_path = get_index_path();
|
let index_path = get_index_path();
|
||||||
if let Some(parent) = index_path.parent() {
|
if let Some(parent) = index_path.parent() {
|
||||||
@@ -76,6 +83,7 @@ pub fn get_registry_cache_root() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the package root path from registry info. If the path points to a dcr.toml manifest, returns its parent directory.
|
||||||
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
|
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
|
||||||
let raw_path = pkg_info
|
let raw_path = pkg_info
|
||||||
.get("path")
|
.get("path")
|
||||||
@@ -97,6 +105,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
|
|||||||
Ok(full_path)
|
Ok(full_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a raw registry path string to a PathBuf, handling file:// prefixes, Windows extended paths, and relative paths by joining to cache root.
|
||||||
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
||||||
let s = raw.trim();
|
let s = raw.trim();
|
||||||
if let Some(rest) = s.strip_prefix("file://") {
|
if let Some(rest) = s.strip_prefix("file://") {
|
||||||
@@ -130,14 +139,26 @@ pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the include directory path for a dependency root, typically under target/include.
|
||||||
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
|
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
|
||||||
dep_root.join("target").join("include")
|
dep_root.join("target").join("include")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the library directory path for a dependency root, typically under target/lib.
|
||||||
pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
|
pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
|
||||||
dep_root.join("target").join("lib")
|
dep_root.join("target").join("lib")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`).
|
||||||
|
///
|
||||||
|
/// Configured registries are sorted by priority, but the lookup currently always reads the
|
||||||
|
/// same local index path (per-registry URLs are unused).
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `name`: Package name as in the index.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Package JSON object, or an error if config/index is missing or the name is absent.
|
||||||
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
|
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
|
||||||
let config = get_registry_config().ok_or("No registry config found")?;
|
let config = get_registry_config().ok_or("No registry config found")?;
|
||||||
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
|
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
|
||||||
@@ -165,6 +186,10 @@ pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if the TOML value is a registry-style dependency.
|
||||||
|
///
|
||||||
|
/// Strings: not path-like and not git-like. Tables: have version/features/optional/registry
|
||||||
|
/// and no `git`/`path`/`url` keys.
|
||||||
pub fn is_registry_dep(value: &TomlValue) -> bool {
|
pub fn is_registry_dep(value: &TomlValue) -> bool {
|
||||||
if let Some(raw) = value.as_str() {
|
if let Some(raw) = value.as_str() {
|
||||||
let raw = raw.trim();
|
let raw = raw.trim();
|
||||||
@@ -183,6 +208,7 @@ pub fn is_registry_dep(value: &TomlValue) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the path string from a dependency value if it's a path-like dependency.
|
||||||
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
|
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
|
||||||
let raw = value.as_str()?.trim();
|
let raw = value.as_str()?.trim();
|
||||||
if let Some(path) = raw.strip_prefix("path:") {
|
if let Some(path) = raw.strip_prefix("path:") {
|
||||||
@@ -194,6 +220,7 @@ pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to check if a string represents a local file path.
|
||||||
fn is_path_like_string(raw: &str) -> bool {
|
fn is_path_like_string(raw: &str) -> bool {
|
||||||
raw.starts_with("path:")
|
raw.starts_with("path:")
|
||||||
|| raw.starts_with("./")
|
|| raw.starts_with("./")
|
||||||
@@ -203,6 +230,7 @@ fn is_path_like_string(raw: &str) -> bool {
|
|||||||
|| raw.contains('\\')
|
|| raw.contains('\\')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to check if a string represents a git or remote dependency.
|
||||||
fn is_git_like_string(raw: &str) -> bool {
|
fn is_git_like_string(raw: &str) -> bool {
|
||||||
raw.starts_with("git:")
|
raw.starts_with("git:")
|
||||||
|| raw.starts_with("github:")
|
|| raw.starts_with("github:")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Core module for DCR, a Cargo-like C/C++ project manager.
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod build_config;
|
pub mod build_config;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
|
|||||||
@@ -15,33 +15,40 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Registry configuration management for DCR.
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Represents a registry configuration entry with its URL and priority.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Registry {
|
pub struct Registry {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the overall configuration loaded from config.toml, mapping registry names to their details.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub registry: HashMap<String, Registry>,
|
pub registry: HashMap<String, Registry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manages the DCR registry configuration, loading from ~/.dcr/config.toml.
|
||||||
pub struct RegistryManager {
|
pub struct RegistryManager {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the user's home directory path, used to locate the config file.
|
||||||
fn home_dir() -> Option<PathBuf> {
|
fn home_dir() -> Option<PathBuf> {
|
||||||
crate::utils::fs::home_dir()
|
crate::utils::fs::home_dir()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistryManager {
|
impl RegistryManager {
|
||||||
|
/// Loads the registry configuration from ~/.dcr/config.toml.
|
||||||
|
/// Returns an error if the config file is missing.
|
||||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let dcr_dir = home_dir()
|
let dcr_dir = home_dir()
|
||||||
.ok_or("Cannot determine home directory")?
|
.ok_or("Cannot determine home directory")?
|
||||||
@@ -53,6 +60,7 @@ impl RegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&config_path)?;
|
let content = fs::read_to_string(&config_path)?;
|
||||||
|
// Deserialize the TOML content into the Config struct
|
||||||
let config: Config = toml::from_str(&content)?;
|
let config: Config = toml::from_str(&content)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ use crate::platform;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Runs the built binary for a project, forwarding `bin_args`.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `project_name`: Package/binary name used to resolve the artifact path.
|
||||||
|
/// - `profile`: Build profile segment under `target/`.
|
||||||
|
/// - `target_dir`: Optional artifact directory override.
|
||||||
|
/// - `bin_args`: Arguments passed to the process.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Child exit code, or `1` if the binary is missing or cannot be spawned.
|
||||||
pub fn run_binary(
|
pub fn run_binary(
|
||||||
project_name: &str,
|
project_name: &str,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -26,6 +36,7 @@ pub fn run_binary(
|
|||||||
bin_args: &[String],
|
bin_args: &[String],
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
||||||
|
// Check if the binary exists before attempting to run it
|
||||||
if Path::new(&bin_path).exists() {
|
if Path::new(&bin_path).exists() {
|
||||||
let status = Command::new(&bin_path).args(bin_args).status();
|
let status = Command::new(&bin_path).args(bin_args).status();
|
||||||
match status {
|
match status {
|
||||||
@@ -37,5 +48,6 @@ pub fn run_binary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Return 1 if binary not found or execution failed
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,14 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// creating git init and .gitignore
|
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
|
||||||
|
///
|
||||||
|
/// Executes `git init` via the VCS utility module, creates a default `.gitignore` if none exists,
|
||||||
|
/// or updates an existing `.gitignore` to ensure `.dcr/` and build outputs are ignored.
|
||||||
pub fn init(path: &Path) -> Result<(), String> {
|
pub fn init(path: &Path) -> Result<(), String> {
|
||||||
crate::utils::git::git_init(path)?;
|
crate::utils::git::git_init(path)?;
|
||||||
|
|
||||||
|
// Populate default .gitignore entries or append DCR metadata rules.
|
||||||
let gitignore_path = path.join(".gitignore");
|
let gitignore_path = path.join(".gitignore");
|
||||||
if !gitignore_path.exists() {
|
if !gitignore_path.exists() {
|
||||||
fs::write(&gitignore_path, "/target\n.dcr/\n")
|
fs::write(&gitignore_path, "/target\n.dcr/\n")
|
||||||
|
|||||||
@@ -15,10 +15,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// Module for version control system (VCS) integration.
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Represents the version control system kind to use.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum VcsKind {
|
pub enum VcsKind {
|
||||||
Git,
|
Git,
|
||||||
@@ -26,6 +28,7 @@ pub enum VcsKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VcsKind {
|
impl VcsKind {
|
||||||
|
/// Parses the given string into a VcsKind.
|
||||||
pub fn parse(s: &str) -> Result<Self, String> {
|
pub fn parse(s: &str) -> Result<Self, String> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"git" => Ok(Self::Git),
|
"git" => Ok(Self::Git),
|
||||||
@@ -35,6 +38,7 @@ impl VcsKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes the VCS for the provided path.
|
||||||
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
|
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
|
||||||
match kind {
|
match kind {
|
||||||
VcsKind::Git => git::init(path),
|
VcsKind::Git => git::init(path),
|
||||||
@@ -42,8 +46,9 @@ pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for git repo in parents to avoid nested repos
|
/// Finds an existing VCS in the parent directories of the given path.
|
||||||
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
|
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
|
||||||
|
// look for git repo in parents to avoid nested repos
|
||||||
let mut current = if path.is_file() {
|
let mut current = if path.is_file() {
|
||||||
path.parent()
|
path.parent()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
|
/// Represents a member of the workspace.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WorkspaceMember {
|
pub struct WorkspaceMember {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -29,12 +30,14 @@ pub struct WorkspaceMember {
|
|||||||
pub main: bool,
|
pub main: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the workspace as a collection of members.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub members: Vec<WorkspaceMember>,
|
pub members: Vec<WorkspaceMember>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
|
/// Returns the main workspace member, preferring the one with `main: true` or the first member.
|
||||||
pub fn main_member(&self) -> Option<&WorkspaceMember> {
|
pub fn main_member(&self) -> Option<&WorkspaceMember> {
|
||||||
self.members
|
self.members
|
||||||
.iter()
|
.iter()
|
||||||
@@ -43,6 +46,17 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the workspace configuration from the TOML config.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `config`: Root `dcr.toml` (may be workspace-only or mixed).
|
||||||
|
/// - `profile` / `target`: Select profile- and target-specific `[workspace.*]` tables.
|
||||||
|
/// - `root`: Workspace root used to resolve member paths.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(None)` if no workspace table applies.
|
||||||
|
/// - `Ok(Some(Workspace))` with members (and optional main).
|
||||||
|
/// - `Err` on invalid member paths or config shape.
|
||||||
pub fn parse_workspace(
|
pub fn parse_workspace(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
@@ -50,6 +64,7 @@ pub fn parse_workspace(
|
|||||||
root: &Path,
|
root: &Path,
|
||||||
) -> Result<Option<Workspace>, String> {
|
) -> Result<Option<Workspace>, String> {
|
||||||
let mut table = None;
|
let mut table = None;
|
||||||
|
// Create ordered list of possible config keys for lookup
|
||||||
let combinations = if let Some(t) = target {
|
let combinations = if let Some(t) = target {
|
||||||
let normalized_t = normalize_target_os(t);
|
let normalized_t = normalize_target_os(t);
|
||||||
vec![
|
vec![
|
||||||
@@ -103,6 +118,7 @@ pub fn parse_workspace(
|
|||||||
Ok(Some(ws))
|
Ok(Some(ws))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a path, making it absolute if relative to root.
|
||||||
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
|
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
|
||||||
let p = Path::new(raw);
|
let p = Path::new(raw);
|
||||||
let full = if p.is_absolute() {
|
let full = if p.is_absolute() {
|
||||||
@@ -113,6 +129,7 @@ fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
|
|||||||
Ok(full)
|
Ok(full)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses dependencies list from TOML value into Vec<String>.
|
||||||
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
|
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
|
||||||
let Some(value) = value else {
|
let Some(value) = value else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -130,12 +147,14 @@ fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs a topological sort on members to handle dependencies and detect cycles.
|
||||||
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
|
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for m in members {
|
for m in members {
|
||||||
map.insert(m.name.clone(), m.clone());
|
map.insert(m.name.clone(), m.clone());
|
||||||
}
|
}
|
||||||
let mut state: HashMap<String, u8> = HashMap::new();
|
let mut state: HashMap<String, u8> = HashMap::new();
|
||||||
|
// Use 0=unvisited, 1=visiting, 2=visited for cycle detection
|
||||||
let mut order: Vec<WorkspaceMember> = Vec::new();
|
let mut order: Vec<WorkspaceMember> = Vec::new();
|
||||||
|
|
||||||
for name in map.keys() {
|
for name in map.keys() {
|
||||||
@@ -146,12 +165,14 @@ fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String
|
|||||||
Ok(order)
|
Ok(order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursive function for DFS-based topological sort with cycle detection.
|
||||||
fn visit(
|
fn visit(
|
||||||
name: &str,
|
name: &str,
|
||||||
map: &HashMap<String, WorkspaceMember>,
|
map: &HashMap<String, WorkspaceMember>,
|
||||||
state: &mut HashMap<String, u8>,
|
state: &mut HashMap<String, u8>,
|
||||||
order: &mut Vec<WorkspaceMember>,
|
order: &mut Vec<WorkspaceMember>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
// Check current visit state to detect cycles
|
||||||
match state.get(name).copied().unwrap_or(0) {
|
match state.get(name).copied().unwrap_or(0) {
|
||||||
1 => return Err(format!("workspace dependency cycle at {name}")),
|
1 => return Err(format!("workspace dependency cycle at {name}")),
|
||||||
2 => return Ok(()),
|
2 => return Ok(()),
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ mod core;
|
|||||||
mod platform;
|
mod platform;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
/// Main entry point for DCR, a Cargo-like C/C++ project manager.
|
||||||
|
///
|
||||||
|
/// Collects command-line arguments via `std::env::args()` and dispatches to the
|
||||||
|
/// appropriate subcommand handler. With fewer than two arguments, shows help and
|
||||||
|
/// exits with code 0; otherwise exits with the handler's exit code.
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@
|
|||||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
use crate::utils::build::default_target_triple;
|
use crate::utils::build::default_target_triple;
|
||||||
|
|
||||||
|
/// Returns the host target triple used for default artifact paths on BSD.
|
||||||
fn host_triple() -> String {
|
fn host_triple() -> String {
|
||||||
default_target_triple()
|
default_target_triple()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to a binary for the given profile, name, and optional target directory.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => join_dir(dir, name),
|
Some(dir) => join_dir(dir, name),
|
||||||
@@ -29,6 +31,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to a static library for the given profile, name, and optional target directory.
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.a");
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -37,10 +40,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
bin_path(profile, name, target_dir)
|
bin_path(profile, name, target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to an EFI binary for the given profile, name, and optional target directory.
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("{name}.efi");
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -49,6 +54,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to a shared library for the given profile, name, and optional target directory.
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.so");
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
|
|||||||
@@ -18,10 +18,13 @@
|
|||||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
use crate::utils::build::default_target_triple;
|
use crate::utils::build::default_target_triple;
|
||||||
|
|
||||||
|
/// Returns the default target triple string for the host platform.
|
||||||
fn host_triple() -> String {
|
fn host_triple() -> String {
|
||||||
default_target_triple()
|
default_target_triple()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to a binary executable.
|
||||||
|
/// If `target_dir` is provided, joins it with the name; otherwise uses default_bin_rel with the host triple.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => join_dir(dir, name),
|
Some(dir) => join_dir(dir, name),
|
||||||
@@ -29,6 +32,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to a static library archive.
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.a");
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -37,10 +41,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to an ELF binary.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
bin_path(profile, name, target_dir)
|
bin_path(profile, name, target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to an EFI binary.
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("{name}.efi");
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -49,6 +55,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to a shared library.
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.so");
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/// macOS-specific utilities for constructing paths to build artifacts.
|
||||||
|
///
|
||||||
|
/// This module defines functions to generate paths for binaries, static
|
||||||
|
/// libraries, shared libraries, ELF files, and EFI executables on macOS.
|
||||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
|
||||||
|
/// Constructs the path to a binary for the given profile, name, and optional target directory.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => join_dir(dir, name),
|
Some(dir) => join_dir(dir, name),
|
||||||
@@ -24,6 +29,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to a static library archive for the given profile, name, and optional target directory.
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.a");
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -32,10 +38,12 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to an ELF binary for the given profile, name, and optional target directory.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
bin_path(profile, name, target_dir)
|
bin_path(profile, name, target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to an EFI executable for the given profile, name, and optional target directory.
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("{name}.efi");
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -44,6 +52,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the path to a shared library for the given profile, name, and optional target directory.
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.dylib");
|
let file = format!("lib{name}.dylib");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ pub mod posix;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
|
/// Host OS path for a binary artifact.
|
||||||
|
///
|
||||||
|
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `profile`: Build profile directory segment.
|
||||||
|
/// - `name`: Binary basename (may already include extension on some hosts).
|
||||||
|
/// - `target_dir`: Optional absolute/relative artifact directory override.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Path string suitable for the current host layout.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -76,6 +87,9 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host OS path for an ELF-style artifact (`profile`, `name`, optional `target_dir`).
|
||||||
|
///
|
||||||
|
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -112,6 +126,9 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host OS path for an EFI (`.efi`) artifact (`profile`, `name`, optional `target_dir`).
|
||||||
|
///
|
||||||
|
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -148,6 +165,9 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host OS path for a static library (`profile`, `name`, optional `target_dir`).
|
||||||
|
///
|
||||||
|
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -184,6 +204,9 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Host OS path for a shared library (`profile`, `name`, optional `target_dir`).
|
||||||
|
///
|
||||||
|
/// Backend is chosen at compile time via `cfg!(target_os = …)`, not by the build target triple.
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,14 +17,19 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Joins the given directory and file into a single path string, trimming trailing slashes
|
||||||
|
/// for cross-platform compatibility.
|
||||||
pub fn join_dir(dir: &str, file: &str) -> String {
|
pub fn join_dir(dir: &str, file: &str) -> String {
|
||||||
let trimmed = dir.trim_end_matches(['/', '\\']);
|
let trimmed = dir.trim_end_matches(['/', '\\']);
|
||||||
Path::new(trimmed).join(file).to_string_lossy().into_owned()
|
Path::new(trimmed).join(file).to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(windows), allow(dead_code))]
|
#[cfg_attr(not(windows), allow(dead_code))]
|
||||||
|
/// Adds `.exe` suffix to the name if it does not already have a recognized executable
|
||||||
|
/// extension (`.exe`, `.dll`, `.sys`, `.com`, `.efi`) or any extension at all.
|
||||||
pub fn with_exe_suffix(name: &str) -> String {
|
pub fn with_exe_suffix(name: &str) -> String {
|
||||||
let lower = name.to_ascii_lowercase();
|
let lower = name.to_ascii_lowercase();
|
||||||
|
// Check if name already qualifies for no suffix (has executable ext or any ext)
|
||||||
if lower.ends_with(".exe")
|
if lower.ends_with(".exe")
|
||||||
|| lower.ends_with(".dll")
|
|| lower.ends_with(".dll")
|
||||||
|| lower.ends_with(".sys")
|
|| lower.ends_with(".sys")
|
||||||
@@ -38,6 +43,7 @@ pub fn with_exe_suffix(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default relative binary path: `./target[/<triple>]/<profile>/<name>`.
|
||||||
pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String {
|
pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String {
|
||||||
let mut p = PathBuf::from("./target");
|
let mut p = PathBuf::from("./target");
|
||||||
if let Some(t) = triple_segment {
|
if let Some(t) = triple_segment {
|
||||||
@@ -46,6 +52,7 @@ pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>)
|
|||||||
p.join(profile).join(name).to_string_lossy().into_owned()
|
p.join(profile).join(name).to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default relative library path: `./target[/<triple>]/<profile>/<file>`.
|
||||||
pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String {
|
pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String {
|
||||||
let mut p = PathBuf::from("./target");
|
let mut p = PathBuf::from("./target");
|
||||||
if let Some(t) = triple_segment {
|
if let Some(t) = triple_segment {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
/// POSIX platform-specific path utilities.
|
||||||
|
/// Provides functions to build paths for binaries, libraries, and EFI files based on profile and optional target directory.
|
||||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
|
||||||
|
/// Returns the path to a binary executable for the given profile, name, and optional target directory.
|
||||||
|
/// If a target directory is provided, it joins the directory with the name; otherwise, it uses the default binary relative path.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => join_dir(dir, name),
|
Some(dir) => join_dir(dir, name),
|
||||||
@@ -7,6 +11,8 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to a static library for the given profile, name, and optional target directory.
|
||||||
|
/// The library file is named "lib{name}.a". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.a");
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -15,10 +21,14 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to an ELF binary for the given profile, name, and optional target directory.
|
||||||
|
/// Delegates to bin_path since ELF binaries are standard binaries.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
bin_path(profile, name, target_dir)
|
bin_path(profile, name, target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to an EFI executable for the given profile, name, and optional target directory.
|
||||||
|
/// The EFI file is named "{name}.efi". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("{name}.efi");
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -27,6 +37,8 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to a shared library for the given profile, name, and optional target directory.
|
||||||
|
/// The shared library file is named "lib{name}.so". If a target directory is provided, it joins the directory with the file name; otherwise, it uses the default library relative path.
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = format!("lib{name}.so");
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix};
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix};
|
||||||
|
|
||||||
|
/// Returns the path to the binary executable for the given profile, name, and optional target directory.
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
let file = with_exe_suffix(name);
|
let file = with_exe_suffix(name);
|
||||||
match target_dir {
|
match target_dir {
|
||||||
@@ -25,7 +26,10 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the library file for the given profile, name, and optional target directory.
|
||||||
|
/// The library name is forced to end with ".lib".
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
// Ensure the library file name ends with ".lib"
|
||||||
let file = if name.to_ascii_lowercase().ends_with(".lib") {
|
let file = if name.to_ascii_lowercase().ends_with(".lib") {
|
||||||
name.to_string()
|
name.to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -37,11 +41,15 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the ELF binary for the given profile, name, and optional target directory.
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
bin_path(profile, name, target_dir)
|
bin_path(profile, name, target_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the EFI binary for the given profile, name, and optional target directory.
|
||||||
|
/// The binary name is forced to end with ".efi".
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
// Ensure the EFI file name ends with ".efi"
|
||||||
let file = if name.to_ascii_lowercase().ends_with(".efi") {
|
let file = if name.to_ascii_lowercase().ends_with(".efi") {
|
||||||
name.to_string()
|
name.to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -53,7 +61,10 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the shared library (DLL) for the given profile, name, and optional target directory.
|
||||||
|
/// The library name is forced to end with ".dll".
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
// Ensure the shared library file name ends with ".dll"
|
||||||
let file = if name.to_ascii_lowercase().ends_with(".dll") {
|
let file = if name.to_ascii_lowercase().ends_with(".dll") {
|
||||||
name.to_string()
|
name.to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ use crate::core::build_config::Config;
|
|||||||
use crate::utils::log::warn;
|
use crate::utils::log::warn;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Version information parsed from a version string.
|
||||||
|
///
|
||||||
|
/// Contains the full version, major, minor, patch, suffix, and suffix_dash components.
|
||||||
pub struct VersionInfo {
|
pub struct VersionInfo {
|
||||||
pub full: String,
|
pub full: String,
|
||||||
pub major: String,
|
pub major: String,
|
||||||
@@ -28,6 +31,7 @@ pub struct VersionInfo {
|
|||||||
pub suffix_dash: String,
|
pub suffix_dash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a version string into VersionInfo struct.
|
||||||
pub fn parse_version_info(version: &str) -> VersionInfo {
|
pub fn parse_version_info(version: &str) -> VersionInfo {
|
||||||
let mut full = version.trim().to_string();
|
let mut full = version.trim().to_string();
|
||||||
if full.is_empty() {
|
if full.is_empty() {
|
||||||
@@ -56,6 +60,7 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Substitutes profile, name, and version variables into a template string.
|
||||||
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
|
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
|
||||||
let s = template
|
let s = template
|
||||||
.replace("{profile}", profile)
|
.replace("{profile}", profile)
|
||||||
@@ -63,6 +68,7 @@ pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name:
|
|||||||
substitute_version_vars(&s, info)
|
substitute_version_vars(&s, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Substitutes version-specific variables into a template string.
|
||||||
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
|
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
|
||||||
template
|
template
|
||||||
.replace("{version}", &info.full)
|
.replace("{version}", &info.full)
|
||||||
@@ -73,6 +79,7 @@ pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
|
|||||||
.replace("{version_suffix_dash}", &info.suffix_dash)
|
.replace("{version_suffix_dash}", &info.suffix_dash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes a target string to a relative path for artifact output directory.
|
||||||
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
||||||
let trimmed = normalize_target_os(target.trim());
|
let trimmed = normalize_target_os(target.trim());
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -82,6 +89,7 @@ pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the native host target directory relative to project root for given profile.
|
||||||
pub fn native_host_target_rel(profile: &str) -> PathBuf {
|
pub fn native_host_target_rel(profile: &str) -> PathBuf {
|
||||||
if cfg!(any(
|
if cfg!(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -98,6 +106,18 @@ pub fn native_host_target_rel(profile: &str) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the artifact output directory for a project/workspace build.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `project_root`: Package root.
|
||||||
|
/// - `workspace_root`: If set, artifacts go under the workspace `target/`.
|
||||||
|
/// - `profile`: Build profile directory segment.
|
||||||
|
/// - `build_target`: Target triple or short name (may be empty).
|
||||||
|
/// - `out_dir`: Explicit `out_dir` from config (wins when non-empty).
|
||||||
|
/// - `has_explicit_target`: Whether CLI/config set a target (changes layout).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Absolute directory path as a string.
|
||||||
pub fn resolve_artifact_target_dir(
|
pub fn resolve_artifact_target_dir(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
workspace_root: Option<&Path>,
|
workspace_root: Option<&Path>,
|
||||||
@@ -145,19 +165,35 @@ pub fn resolve_artifact_target_dir(
|
|||||||
project_root.join(base).to_string_lossy().into_owned()
|
project_root.join(base).to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes kind string to standard value, defaulting to "bin" if empty.
|
||||||
pub fn normalize_kind(kind: &str) -> &str {
|
pub fn normalize_kind(kind: &str) -> &str {
|
||||||
let trimmed = kind.trim();
|
let trimmed = kind.trim();
|
||||||
if trimmed.is_empty() { "bin" } else { trimmed }
|
if trimmed.is_empty() { "bin" } else { trimmed }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the kind is a flat binary artifact.
|
||||||
pub fn is_flat_bin(kind: &str) -> bool {
|
pub fn is_flat_bin(kind: &str) -> bool {
|
||||||
kind == "flat-bin"
|
kind == "flat-bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true for kinds that skip the normal link/archive step: `none`, `custom`, or `flat-bin`.
|
||||||
|
///
|
||||||
|
/// Callers that still need a flat-binary path use [`is_flat_bin`] separately
|
||||||
|
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before an early return).
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `kind`: Normalized artifact kind string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` for `none`, `custom`, or `flat-bin`.
|
||||||
|
///
|
||||||
|
/// Callers that still need a flat-binary link use [`is_flat_bin`] separately
|
||||||
|
/// (e.g. `is_compile_only(kind) && !is_flat_bin(kind)` before early return).
|
||||||
pub fn is_compile_only(kind: &str) -> bool {
|
pub fn is_compile_only(kind: &str) -> bool {
|
||||||
matches!(kind, "none" | "custom" | "flat-bin")
|
matches!(kind, "none" | "custom" | "flat-bin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes platform string, returning None if empty.
|
||||||
pub fn normalize_platform(platform: &str) -> Option<&str> {
|
pub fn normalize_platform(platform: &str) -> Option<&str> {
|
||||||
let trimmed = platform.trim();
|
let trimmed = platform.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -167,6 +203,13 @@ pub fn normalize_platform(platform: &str) -> Option<&str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands short OS names (`linux`/`macos`/`windows`) to default triples; leaves others as-is.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `target`: Short name, full triple, or empty.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Canonical triple string, empty input unchanged, unknown bare names returned with a warning.
|
||||||
pub fn normalize_target_os(target: &str) -> &str {
|
pub fn normalize_target_os(target: &str) -> &str {
|
||||||
match target {
|
match target {
|
||||||
"" => "",
|
"" => "",
|
||||||
@@ -184,6 +227,7 @@ pub fn normalize_target_os(target: &str) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the default target triple for the current host platform.
|
||||||
pub fn default_target_triple() -> String {
|
pub fn default_target_triple() -> String {
|
||||||
let arch = std::env::consts::ARCH;
|
let arch = std::env::consts::ARCH;
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
@@ -214,6 +258,7 @@ pub fn default_target_triple() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepends a --target flag to flags vector if using Clang and not already present.
|
||||||
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
|
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
|
||||||
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
|
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
|
||||||
return;
|
return;
|
||||||
@@ -231,6 +276,7 @@ pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves a string value from config, falling back to empty string if missing.
|
||||||
pub fn get_config_str(config: &Config, key: &str) -> String {
|
pub fn get_config_str(config: &Config, key: &str) -> String {
|
||||||
config
|
config
|
||||||
.get(key)
|
.get(key)
|
||||||
@@ -239,6 +285,7 @@ pub fn get_config_str(config: &Config, key: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves profile-specific table from config.
|
||||||
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
|
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
|
||||||
config
|
config
|
||||||
.get("build")
|
.get("build")
|
||||||
@@ -247,6 +294,7 @@ pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::
|
|||||||
.and_then(|v| v.as_table())
|
.and_then(|v| v.as_table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves an optional string value from config.
|
||||||
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
||||||
let value = config.get(key)?.as_str()?;
|
let value = config.get(key)?.as_str()?;
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
@@ -257,6 +305,7 @@ pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves string value with profile override support.
|
||||||
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||||
let base = get_config_str(config, &format!("build.{field}"));
|
let base = get_config_str(config, &format!("build.{field}"));
|
||||||
let Some(table) = profile_table(config, profile) else {
|
let Some(table) = profile_table(config, profile) else {
|
||||||
@@ -271,6 +320,7 @@ pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves list value from config.
|
||||||
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
||||||
config
|
config
|
||||||
.get(key)
|
.get(key)
|
||||||
@@ -284,6 +334,7 @@ pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves list value with profile override support.
|
||||||
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
|
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
|
||||||
let mut out = get_config_list(config, &format!("build.{field}"));
|
let mut out = get_config_list(config, &format!("build.{field}"));
|
||||||
if let Some(table) = profile_table(config, profile)
|
if let Some(table) = profile_table(config, profile)
|
||||||
@@ -299,6 +350,7 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to check if a string contains a pattern ignoring case.
|
||||||
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
|
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
|
||||||
if pattern.len() > text.len() {
|
if pattern.len() > text.len() {
|
||||||
return false;
|
return false;
|
||||||
@@ -308,6 +360,7 @@ fn contains_ignore_case(text: &str, pattern: &str) -> bool {
|
|||||||
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
|
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if target is a bare-metal or embedded target.
|
||||||
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
|
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
|
||||||
match target {
|
match target {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
@@ -321,6 +374,7 @@ pub fn is_bare_metal_target(target: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns default flags for a given profile.
|
||||||
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
|
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
|
||||||
match profile {
|
match profile {
|
||||||
"release" => &["-O3", "-DNDEBUG"],
|
"release" => &["-O3", "-DNDEBUG"],
|
||||||
@@ -336,6 +390,7 @@ pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves boolean value with profile override support.
|
||||||
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
||||||
let base = config
|
let base = config
|
||||||
.get(&format!("build.{field}"))
|
.get(&format!("build.{field}"))
|
||||||
@@ -345,6 +400,7 @@ pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, defaul
|
|||||||
profile_val.or(base).unwrap_or(default)
|
profile_val.or(base).unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves language from config with profile support.
|
||||||
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
|
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
|
||||||
if let Some(table) = profile_table(config, profile)
|
if let Some(table) = profile_table(config, profile)
|
||||||
&& let Some(value) = table.get("language")
|
&& let Some(value) = table.get("language")
|
||||||
@@ -357,10 +413,12 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves language from config with profile support, defaulting to "c" on error.
|
||||||
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
|
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
|
||||||
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
|
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses language value from config, supporting string or array format.
|
||||||
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
|
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
|
||||||
if let Some(s) = value.as_str() {
|
if let Some(s) = value.as_str() {
|
||||||
let trimmed = s.trim();
|
let trimmed = s.trim();
|
||||||
@@ -389,6 +447,7 @@ pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, St
|
|||||||
Ok(parts.join(","))
|
Ok(parts.join(","))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves compiler name based on language, environment, and toolchain overrides.
|
||||||
pub fn resolve_compiler(
|
pub fn resolve_compiler(
|
||||||
language: &str,
|
language: &str,
|
||||||
compiler: &str,
|
compiler: &str,
|
||||||
@@ -407,6 +466,7 @@ pub fn resolve_compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks environment variables for compiler override.
|
||||||
fn env_override_compiler(lang: &str) -> Option<String> {
|
fn env_override_compiler(lang: &str) -> Option<String> {
|
||||||
if let Ok(value) = std::env::var("DCR_COMPILER") {
|
if let Ok(value) = std::env::var("DCR_COMPILER") {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
@@ -440,6 +500,7 @@ fn env_override_compiler(lang: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks toolchain overrides for compiler.
|
||||||
fn toolchain_override_compiler(
|
fn toolchain_override_compiler(
|
||||||
lang: &str,
|
lang: &str,
|
||||||
tc_cc: Option<&str>,
|
tc_cc: Option<&str>,
|
||||||
@@ -458,6 +519,7 @@ fn toolchain_override_compiler(
|
|||||||
tc_cc.map(|v| v.to_string())
|
tc_cc.map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps assembler compiler name to canonical form.
|
||||||
fn map_asm_compiler(compiler: &str) -> String {
|
fn map_asm_compiler(compiler: &str) -> String {
|
||||||
match compiler.to_lowercase().as_str() {
|
match compiler.to_lowercase().as_str() {
|
||||||
"gas" | "gnu-as" => "as".to_string(),
|
"gas" | "gnu-as" => "as".to_string(),
|
||||||
@@ -468,6 +530,7 @@ fn map_asm_compiler(compiler: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines primary language from comma-separated list.
|
||||||
pub fn primary_language(language: &str) -> String {
|
pub fn primary_language(language: &str) -> String {
|
||||||
let tokens: Vec<&str> = language
|
let tokens: Vec<&str> = language
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -486,6 +549,7 @@ pub fn primary_language(language: &str) -> String {
|
|||||||
language.to_lowercase()
|
language.to_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves tool from environment variable or fallback.
|
||||||
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
||||||
if let Ok(value) = std::env::var(env_key) {
|
if let Ok(value) = std::env::var(env_key) {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
@@ -496,6 +560,7 @@ pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
|||||||
fallback.map(|v| v.to_string())
|
fallback.map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves pkg-config flags for packages.
|
||||||
pub fn resolve_pkg_config_flags(
|
pub fn resolve_pkg_config_flags(
|
||||||
pkgs: &[String],
|
pkgs: &[String],
|
||||||
base_cflags: &[String],
|
base_cflags: &[String],
|
||||||
@@ -512,6 +577,7 @@ pub fn resolve_pkg_config_flags(
|
|||||||
Ok((cflags, ldflags))
|
Ok((cflags, ldflags))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves pkg-config flags, falling back on error.
|
||||||
pub fn resolve_pkg_config_flags_lossy(
|
pub fn resolve_pkg_config_flags_lossy(
|
||||||
pkgs: &[String],
|
pkgs: &[String],
|
||||||
base_cflags: &[String],
|
base_cflags: &[String],
|
||||||
@@ -526,6 +592,7 @@ pub fn resolve_pkg_config_flags_lossy(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs pkg-config command and returns output.
|
||||||
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
||||||
let output = std::process::Command::new("pkg-config")
|
let output = std::process::Command::new("pkg-config")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
@@ -539,6 +606,7 @@ pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
|||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The split_flags function parses pkg-config output, handling quoted strings and whitespace to split into individual flags.
|
||||||
fn split_flags(value: &str) -> Vec<String> {
|
fn split_flags(value: &str) -> Vec<String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut current = String::new();
|
let mut current = String::new();
|
||||||
@@ -578,6 +646,7 @@ fn split_flags(value: &str) -> Vec<String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Test for normalize_target_short_names function.
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_target_short_names() {
|
fn normalize_target_short_names() {
|
||||||
assert_eq!(normalize_target_os(""), "");
|
assert_eq!(normalize_target_os(""), "");
|
||||||
@@ -591,6 +660,7 @@ mod tests {
|
|||||||
assert_eq!(normalize_target_os("unknown"), "unknown");
|
assert_eq!(normalize_target_os("unknown"), "unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for default_target_triple_uses_host_arch function.
|
||||||
#[test]
|
#[test]
|
||||||
fn default_target_triple_uses_host_arch() {
|
fn default_target_triple_uses_host_arch() {
|
||||||
let target = default_target_triple();
|
let target = default_target_triple();
|
||||||
@@ -635,6 +705,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for resolve_artifact_target_dir_native_and_workspace function.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_artifact_target_dir_native_and_workspace() {
|
fn resolve_artifact_target_dir_native_and_workspace() {
|
||||||
let root = Path::new("/proj");
|
let root = Path::new("/proj");
|
||||||
@@ -655,6 +726,7 @@ mod tests {
|
|||||||
assert!(out.contains("dist"));
|
assert!(out.contains("dist"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for prepend_clang_target_flag_adds_once_for_clang function.
|
||||||
#[test]
|
#[test]
|
||||||
fn prepend_clang_target_flag_adds_once_for_clang() {
|
fn prepend_clang_target_flag_adds_once_for_clang() {
|
||||||
let mut flags = vec!["-O2".to_string()];
|
let mut flags = vec!["-O2".to_string()];
|
||||||
@@ -669,6 +741,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for prepend_clang_target_flag_ignores_non_clang_tools function.
|
||||||
#[test]
|
#[test]
|
||||||
fn prepend_clang_target_flag_ignores_non_clang_tools() {
|
fn prepend_clang_target_flag_ignores_non_clang_tools() {
|
||||||
let mut flags = vec!["-O2".to_string()];
|
let mut flags = vec!["-O2".to_string()];
|
||||||
@@ -676,6 +749,7 @@ mod tests {
|
|||||||
assert_eq!(flags, vec!["-O2".to_string()]);
|
assert_eq!(flags, vec!["-O2".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for parse_version_parts function.
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_version_parts() {
|
fn parse_version_parts() {
|
||||||
let info = parse_version_info("1.2.3-beta");
|
let info = parse_version_info("1.2.3-beta");
|
||||||
@@ -687,6 +761,7 @@ mod tests {
|
|||||||
assert_eq!(info.suffix_dash, "-beta");
|
assert_eq!(info.suffix_dash, "-beta");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for normalize_target_with_profile function.
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_target_with_profile() {
|
fn normalize_target_with_profile() {
|
||||||
let linux = normalize_target_os("linux");
|
let linux = normalize_target_os("linux");
|
||||||
@@ -701,6 +776,7 @@ mod tests {
|
|||||||
assert_eq!(normalize_target("", "debug"), None);
|
assert_eq!(normalize_target("", "debug"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for normalize_kind_empty_defaults_to_bin function.
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_kind_empty_defaults_to_bin() {
|
fn normalize_kind_empty_defaults_to_bin() {
|
||||||
assert_eq!(normalize_kind(""), "bin");
|
assert_eq!(normalize_kind(""), "bin");
|
||||||
@@ -709,6 +785,7 @@ mod tests {
|
|||||||
assert_eq!(normalize_kind(" elf "), "elf");
|
assert_eq!(normalize_kind(" elf "), "elf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for normalize_platform_empty_is_none function.
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_platform_empty_is_none() {
|
fn normalize_platform_empty_is_none() {
|
||||||
assert_eq!(normalize_platform(""), None);
|
assert_eq!(normalize_platform(""), None);
|
||||||
@@ -717,6 +794,7 @@ mod tests {
|
|||||||
assert_eq!(normalize_platform(" efi "), Some("efi"));
|
assert_eq!(normalize_platform(" efi "), Some("efi"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for default_profile_flags_by_profile function.
|
||||||
#[test]
|
#[test]
|
||||||
fn default_profile_flags_by_profile() {
|
fn default_profile_flags_by_profile() {
|
||||||
assert!(default_profile_flags("debug").contains(&"-O0"));
|
assert!(default_profile_flags("debug").contains(&"-O0"));
|
||||||
@@ -726,6 +804,7 @@ mod tests {
|
|||||||
assert!(default_profile_flags("unknown").is_empty());
|
assert!(default_profile_flags("unknown").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test for is_bare_metal_detects_embedded_targets function.
|
||||||
#[test]
|
#[test]
|
||||||
fn is_bare_metal_detects_embedded_targets() {
|
fn is_bare_metal_detects_embedded_targets() {
|
||||||
assert!(is_bare_metal_target(Some("aarch64-none-elf")));
|
assert!(is_bare_metal_target(Some("aarch64-none-elf")));
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ use std::io;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Resolves the path to the user's home directory across different operating systems.
|
||||||
|
///
|
||||||
|
/// Checks the `HOME` environment variable (Unix-like systems) first,
|
||||||
|
/// then falls back to `USERPROFILE` (Windows).
|
||||||
pub fn home_dir() -> Option<PathBuf> {
|
pub fn home_dir() -> Option<PathBuf> {
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
return Some(PathBuf::from(home));
|
return Some(PathBuf::from(home));
|
||||||
@@ -29,6 +33,7 @@ pub fn home_dir() -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a byte slice into a lowercase hexadecimal string.
|
||||||
pub fn to_hex(bytes: &[u8]) -> String {
|
pub fn to_hex(bytes: &[u8]) -> String {
|
||||||
let mut out = String::with_capacity(bytes.len() * 2);
|
let mut out = String::with_capacity(bytes.len() * 2);
|
||||||
for b in bytes {
|
for b in bytes {
|
||||||
@@ -37,6 +42,7 @@ pub fn to_hex(bytes: &[u8]) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists all entry names (files and directories) inside a target directory.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
||||||
let path: PathBuf = match dir {
|
let path: PathBuf = match dir {
|
||||||
@@ -53,6 +59,7 @@ pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Searches upwards from a starting path to locate the root directory containing `dcr.toml`.
|
||||||
pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
|
pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
|
||||||
let mut current = start.to_path_buf();
|
let mut current = start.to_path_buf();
|
||||||
loop {
|
loop {
|
||||||
@@ -66,6 +73,15 @@ pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes a closure inside a specified directory and restores the original cwd afterwards.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `dir`: Directory to `cd` into for the duration of `f`.
|
||||||
|
/// - `f`: Work to run with that cwd (its `Result` is returned as-is).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The closure's result, or an error if getting/changing directory fails.
|
||||||
|
/// Cwd is restored even when `f` returns `Err`.
|
||||||
pub fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
|
pub fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<T, String>,
|
F: FnOnce() -> Result<T, String>,
|
||||||
@@ -73,10 +89,14 @@ where
|
|||||||
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
|
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
|
||||||
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
|
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
|
||||||
let result = f();
|
let result = f();
|
||||||
|
// Restore previous working directory regardless of closure result.
|
||||||
let _ = std::env::set_current_dir(prev);
|
let _ = std::env::set_current_dir(prev);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strips Windows verbatim/extended-length prefixes (e.g. `\\?\`, `\\.\`) and standardizes slashes.
|
||||||
|
///
|
||||||
|
/// Normalize path separators for cross-platform safety and removes platform-specific extended prefixes.
|
||||||
pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
|
pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
|
||||||
let s = path.to_string_lossy();
|
let s = path.to_string_lossy();
|
||||||
let mut cleaned = s.replace('\\', "/");
|
let mut cleaned = s.replace('\\', "/");
|
||||||
@@ -99,6 +119,9 @@ pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canonicalizes a path to its absolute form and cleans up Windows extended-length prefixes.
|
||||||
|
///
|
||||||
|
/// Falls back to joining relative paths with the current working directory if canonicalization fails.
|
||||||
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||||
let raw = std::fs::canonicalize(path).unwrap_or_else(|_| {
|
let raw = std::fs::canonicalize(path).unwrap_or_else(|_| {
|
||||||
if path.is_absolute() {
|
if path.is_absolute() {
|
||||||
@@ -112,6 +135,7 @@ pub fn canonicalize_path(path: &Path) -> PathBuf {
|
|||||||
strip_verbatim_prefix(raw)
|
strip_verbatim_prefix(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a path relative to a root directory and returns a canonicalized absolute path.
|
||||||
pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
|
pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
|
||||||
let joined = if path.is_absolute() {
|
let joined = if path.is_absolute() {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
@@ -121,12 +145,21 @@ pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
|
|||||||
canonicalize_path(&joined)
|
canonicalize_path(&joined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures the `.dcr` internal metadata directory and its `ide` subfolder exist within a project root.
|
||||||
pub fn ensure_dcr_dir(root: &Path) -> io::Result<PathBuf> {
|
pub fn ensure_dcr_dir(root: &Path) -> io::Result<PathBuf> {
|
||||||
let dcr = root.join(".dcr");
|
let dcr = root.join(".dcr");
|
||||||
std::fs::create_dir_all(dcr.join("ide"))?;
|
std::fs::create_dir_all(dcr.join("ide"))?;
|
||||||
Ok(dcr)
|
Ok(dcr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes `contents` via a `.tmp.<pid>` file then renames into place (best-effort replace).
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `path`: Final destination path (parent dirs are created).
|
||||||
|
/// - `contents`: Bytes to write.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `Ok(())` after a successful rename (with fallbacks if the target already exists).
|
||||||
pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
@@ -145,6 +178,7 @@ pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
|||||||
match std::fs::rename(&tmp, path) {
|
match std::fs::rename(&tmp, path) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// Handle cross-device moves or existing file replacement fallbacks.
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
match std::fs::rename(&tmp, path) {
|
match std::fs::rename(&tmp, path) {
|
||||||
@@ -162,6 +196,9 @@ pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures that `.gitignore` exists in the project directory and includes the `.dcr/` output rule.
|
||||||
|
///
|
||||||
|
/// If `.gitignore` does not exist, creates one with standard `/target` and `.dcr/` entries.
|
||||||
pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> {
|
pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> {
|
||||||
let path = project_dir.join(".gitignore");
|
let path = project_dir.join(".gitignore");
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Checks if the `git` executable is available in the system's PATH.
|
||||||
|
///
|
||||||
|
/// This function executes `git --version` and returns `true` if the command succeeds, `false` otherwise.
|
||||||
pub fn is_git_available() -> bool {
|
pub fn is_git_available() -> bool {
|
||||||
Command::new("git")
|
Command::new("git")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
@@ -9,6 +12,7 @@ pub fn is_git_available() -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes a new Git repository at the specified path.
|
||||||
pub fn git_init(path: &Path) -> Result<(), String> {
|
pub fn git_init(path: &Path) -> Result<(), String> {
|
||||||
if !is_git_available() {
|
if !is_git_available() {
|
||||||
return Err("git executable not found in PATH".to_string());
|
return Err("git executable not found in PATH".to_string());
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
|
|
||||||
use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
|
use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
|
||||||
|
|
||||||
|
/// Logs an error message to the standard error stream.
|
||||||
|
///
|
||||||
|
/// The message is prefixed with a red "error" label for visibility.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn error(msg: &str) {
|
pub fn error(msg: &str) {
|
||||||
eprintln!("{}: {msg}", colored("error", BOLD_RED));
|
eprintln!("{}: {msg}", colored("error", BOLD_RED));
|
||||||
|
|||||||
@@ -14,9 +14,19 @@
|
|||||||
//
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Core utilities module providing access to build, filesystem, git, log, and text helpers.
|
||||||
pub mod build;
|
pub mod build;
|
||||||
|
|
||||||
|
/// File system path utilities and operations.
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
|
||||||
|
/// Git version control system integration.
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
|
||||||
|
/// Logging utilities for application events and debugging.
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
|
||||||
|
/// Text processing and manipulation helpers.
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
@@ -38,11 +38,15 @@ pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
|
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
|
||||||
|
|
||||||
|
/// Applies ANSI escape codes to format the message with the given style.
|
||||||
|
///
|
||||||
|
/// This is a utility for colored output in terminal applications.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn colored(msg: &str, style: &str) -> String {
|
pub fn colored(msg: &str, style: &str) -> String {
|
||||||
format!("{style}{msg}{RESET}")
|
format!("{style}{msg}{RESET}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints `msg` to stdout with the given ANSI `style`.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn printc(msg: &str, style: &str) {
|
pub fn printc(msg: &str, style: &str) {
|
||||||
println!("{style}{msg}{RESET}");
|
println!("{style}{msg}{RESET}");
|
||||||
|
|||||||
Reference in New Issue
Block a user