mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 17:29:36 +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::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 {
|
||||
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
|
||||
pub name: String,
|
||||
/// Local filesystem path when the source uses the `path:` prefix.
|
||||
pub path: Option<String>,
|
||||
/// Git repository URL when the source is a git/GitHub/GitLab URL or prefix.
|
||||
pub git: Option<String>,
|
||||
/// Optional git branch to pin the dependency to.
|
||||
pub branch: Option<String>,
|
||||
/// Optional git tag to pin the dependency to.
|
||||
pub tag: Option<String>,
|
||||
/// Optional git commit hash (revision) to pin the dependency to.
|
||||
pub rev: Option<String>,
|
||||
/// Version string resolved from the package registry when no explicit source is given.
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
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 {
|
||||
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
|
||||
let mut table = Map::new();
|
||||
@@ -135,6 +158,11 @@ pub fn add(args: &[String]) -> i32 {
|
||||
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> {
|
||||
if args.is_empty() {
|
||||
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();
|
||||
} else {
|
||||
// No source provided — try registry auto-lookup
|
||||
// No source provided — resolve version from the package registry
|
||||
match register::resolve_package_from_registry(&name) {
|
||||
Ok(info) => {
|
||||
let version = info
|
||||
@@ -235,6 +263,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
let mut path = None;
|
||||
let mut git = None;
|
||||
|
||||
// Map source prefixes / bare URLs to path or git fields
|
||||
if let Some(p) = source_spec.strip_prefix("path:") {
|
||||
path = Some(p.to_string());
|
||||
} 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));
|
||||
} else if let Some(g) = source_spec.strip_prefix("git:") {
|
||||
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));
|
||||
} 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@") {
|
||||
g.to_string()
|
||||
} else {
|
||||
|
||||
@@ -35,19 +35,31 @@ use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
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 {
|
||||
colored(&format!("{verb:<9}"), style)
|
||||
}
|
||||
|
||||
/// CLI build reporter that prints human-readable progress to stderr/stdout.
|
||||
struct 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) {
|
||||
eprintln!(" {} {}", status(verb, style), rest);
|
||||
}
|
||||
}
|
||||
|
||||
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<'_>) {
|
||||
match event {
|
||||
BuildEvent::TargetStart {
|
||||
@@ -80,6 +92,7 @@ impl BuildReporter for CliReporter {
|
||||
rebuilt,
|
||||
} => {
|
||||
common::finish_progress_line();
|
||||
// Only announce deps that were actually rebuilt this run.
|
||||
if rebuilt {
|
||||
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
|
||||
}
|
||||
@@ -87,6 +100,7 @@ impl BuildReporter for CliReporter {
|
||||
BuildEvent::Compiling { name, version } => {
|
||||
common::finish_progress_line();
|
||||
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()));
|
||||
if !std::io::stderr().is_terminal() {
|
||||
eprintln!("{label}");
|
||||
@@ -101,6 +115,7 @@ impl BuildReporter for CliReporter {
|
||||
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
|
||||
}
|
||||
BuildEvent::CompilerOutput { stream, text } => {
|
||||
// Pause the progress spinner so compiler text is not overwritten.
|
||||
common::interrupt_progress_for_output();
|
||||
if stream == "stderr" {
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -137,6 +163,7 @@ pub fn build(args: &[String]) -> i32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Shared cancel flag so Ctrl-C can stop an in-progress build cooperatively.
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let cancel_clone = cancel.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -166,6 +193,7 @@ pub fn build(args: &[String]) -> i32 {
|
||||
Err(code) => return code,
|
||||
};
|
||||
|
||||
// Prefer CLI --target; otherwise fall back to profile target from dcr.toml.
|
||||
if flags.target.is_none() {
|
||||
let config_path = root.join("dcr.toml");
|
||||
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;
|
||||
match run_build(&root, &req, &mut reporter) {
|
||||
Ok(_) => {
|
||||
// Persist IDE/metadata and optionally echo the main artifact path.
|
||||
if let Some(info) =
|
||||
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(
|
||||
root: &Path,
|
||||
profile: &str,
|
||||
@@ -262,6 +304,7 @@ fn resolve_post_build_info(
|
||||
tc_as.as_deref(),
|
||||
);
|
||||
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 target_dir =
|
||||
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> {
|
||||
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||
let name = get_config_str(&config, "package.name");
|
||||
|
||||
@@ -25,6 +25,15 @@ use glob::glob;
|
||||
use std::fs;
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
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)) {
|
||||
Ok(()) => 0,
|
||||
Err(msg) => {
|
||||
@@ -79,12 +89,20 @@ pub fn clean(args: &[String]) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed flags controlling which artifacts `clean` should remove.
|
||||
struct CleanFlags {
|
||||
/// Optional build profile name (e.g. `debug`, `release`).
|
||||
profile: Option<String>,
|
||||
/// Optional target triple to clean under `target/<triple>/...`.
|
||||
target: Option<String>,
|
||||
/// When true, also clean each workspace member that has a `target` directory.
|
||||
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> {
|
||||
let mut profile: Option<String> = None;
|
||||
let mut target: Option<String> = None;
|
||||
@@ -104,6 +122,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||
continue;
|
||||
}
|
||||
if arg.starts_with("--") {
|
||||
// Accept only known profile names (e.g. debug/release), not arbitrary flags.
|
||||
let candidate = arg.trim_start_matches("--").to_string();
|
||||
if !default_profile_flags(&candidate).is_empty() {
|
||||
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> {
|
||||
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(|| {
|
||||
config
|
||||
.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())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
project_root: &Path,
|
||||
profile: Option<&str>,
|
||||
@@ -192,6 +222,7 @@ fn clean_project_at(
|
||||
};
|
||||
let target_items = check_dir(Some("target")).unwrap_or_default();
|
||||
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() {
|
||||
target_items.contains(&parent_dir.to_string())
|
||||
} 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> {
|
||||
let patterns = match config.get("build.clean") {
|
||||
Some(v) => v
|
||||
@@ -260,6 +295,10 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
||||
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 {
|
||||
let version = config
|
||||
.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";
|
||||
|
||||
/// Metadata for the latest GitHub release of DCR.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Release {
|
||||
/// Release tag, typically including a leading `v` (e.g. `v1.2.3`).
|
||||
tag_name: String,
|
||||
/// Downloadable assets attached to the release.
|
||||
assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
|
||||
/// A single downloadable asset from a GitHub release.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReleaseAsset {
|
||||
/// Asset file name as published on GitHub.
|
||||
name: String,
|
||||
/// Direct URL used to download the asset.
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
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) {
|
||||
warn("This dcr binary is managed by pacman/AUR");
|
||||
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');
|
||||
if latest_version == 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);
|
||||
|
||||
// Replace the running executable in place, then clean up the temp file.
|
||||
if self_replace::self_replace(&temp_path).is_err() {
|
||||
let _ = fs::remove_file(&temp_path);
|
||||
error("Failed to replace current binary");
|
||||
@@ -124,6 +141,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Fetches the latest DCR release metadata from the GitHub Releases API.
|
||||
fn fetch_latest_release() -> Result<Release, String> {
|
||||
let response = ureq::get(LATEST_RELEASE_URL)
|
||||
.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())
|
||||
}
|
||||
|
||||
/// Downloads a release asset from the given URL into memory.
|
||||
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||
let response = ureq::get(url)
|
||||
.set("User-Agent", "dcr-updater")
|
||||
@@ -155,9 +174,11 @@ fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Builds the list of expected asset file names for the given build target.
|
||||
fn asset_candidates(target: &str) -> Vec<String> {
|
||||
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") {
|
||||
names.push(format!("dcr-{target}.exe"));
|
||||
}
|
||||
@@ -165,6 +186,7 @@ fn asset_candidates(target: &str) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Creates a unique temporary path next to the current executable for the download.
|
||||
fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
||||
let stamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -177,6 +199,7 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
|
||||
current_exe.with_extension(extension)
|
||||
}
|
||||
|
||||
/// Sets executable permissions on Unix; no-op on other platforms.
|
||||
fn set_executable_permissions(_path: &Path) {
|
||||
#[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")]
|
||||
fn pacman_owned_package(path: &Path) -> Option<String> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Non-Linux stub: package-manager ownership is not checked.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn pacman_owned_package(_path: &Path) -> Option<String> {
|
||||
None
|
||||
|
||||
@@ -18,17 +18,36 @@
|
||||
use crate::config::{PROFILE, flags};
|
||||
use crate::utils::log::warn;
|
||||
|
||||
/// Flags shared by the `build` and `run` CLI commands.
|
||||
pub struct BuildRunFlags {
|
||||
/// Build profile name (for example `debug` or `release`).
|
||||
pub profile: String,
|
||||
/// Optional target triple or platform name.
|
||||
pub target: Option<String>,
|
||||
/// Optional workspace member name (`--workspace <name>`).
|
||||
pub workspace: Option<String>,
|
||||
/// Force a rebuild even when artifacts appear up to date.
|
||||
pub force: bool,
|
||||
/// Clean build outputs before building.
|
||||
pub clean: bool,
|
||||
/// Enable verbose logging during the build or run.
|
||||
pub verbose: bool,
|
||||
/// Print the final artifact path to the user.
|
||||
pub print_artifact_path: bool,
|
||||
/// Arguments forwarded to the binary after a `--` separator.
|
||||
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]) {
|
||||
match args.iter().position(|a| a == "--") {
|
||||
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> {
|
||||
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
||||
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();
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
// DCR build/run options are long flags only (`--name`).
|
||||
if !arg.starts_with("--") {
|
||||
warn("Unknown argument");
|
||||
return Err(1);
|
||||
@@ -87,6 +120,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Profile names come from config::flags; only one profile may be set.
|
||||
if flags(candidate).is_some() {
|
||||
if profile != PROFILE {
|
||||
warn("Duplicate profile flag");
|
||||
@@ -115,10 +149,12 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Converts string literals into owned `String` CLI arguments for tests.
|
||||
fn s(args: &[&str]) -> Vec<String> {
|
||||
args.iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Checks that without `--`, the full list stays on the DCR side.
|
||||
#[test]
|
||||
fn split_double_dash_empty_tail() {
|
||||
let args = s(&["--release", "--force"]);
|
||||
@@ -127,6 +163,7 @@ mod tests {
|
||||
assert!(right.is_empty());
|
||||
}
|
||||
|
||||
/// Checks that arguments after `--` are split into the bin-args slice.
|
||||
#[test]
|
||||
fn split_double_dash_with_bin_args() {
|
||||
let args = s(&["--release", "--", "--test_help", "x"]);
|
||||
@@ -135,6 +172,7 @@ mod tests {
|
||||
assert_eq!(right, &s(&["--test_help", "x"]));
|
||||
}
|
||||
|
||||
/// Checks profile parsing and that bin args after `--` are forwarded.
|
||||
#[test]
|
||||
fn parse_run_flags_forwards_after_double_dash() {
|
||||
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 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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -31,6 +37,7 @@ pub fn fmt(args: &[String]) -> i32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Collect all matching files from src/ and tests/ using glob patterns
|
||||
let patterns = [
|
||||
"src/**/*.c",
|
||||
"src/**/*.cpp",
|
||||
@@ -66,6 +73,7 @@ pub fn fmt(args: &[String]) -> i32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Format the collected files
|
||||
println!(
|
||||
" {} {} files",
|
||||
colored("Formatting", BOLD_GREEN),
|
||||
|
||||
@@ -34,14 +34,20 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
|
||||
/// Used by `gen` commands where the project may not have been built yet.
|
||||
/// Path-dep include/lib/libs metadata for `dcr gen` (project may not be built yet).
|
||||
///
|
||||
/// Explicit include/lib lists may point at missing paths; default `include`/`lib`
|
||||
/// candidates are added only when those directories exist.
|
||||
struct GenDeps {
|
||||
include_dirs: Vec<String>,
|
||||
lib_dirs: 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 {
|
||||
let deps_val = match config.get("dependencies") {
|
||||
Some(v) => v,
|
||||
@@ -168,6 +174,7 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
|
||||
|
||||
// ── public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Per-project (or workspace member) metadata collected for `dcr gen`.
|
||||
pub struct ProjectInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
@@ -201,8 +208,13 @@ pub struct ProjectInfo {
|
||||
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 {
|
||||
let subcommand = match args.first() {
|
||||
Some(s) if s == "--help" => {
|
||||
@@ -253,6 +265,7 @@ pub fn r#gen(args: &[String]) -> i32 {
|
||||
|
||||
// ── shared: collect per-project data ─────────────────────────────────────────
|
||||
|
||||
/// Collects [`ProjectInfo`] with cwd set to the project root so relative paths resolve.
|
||||
fn collect_project_info(
|
||||
root: &Path,
|
||||
profile: &str,
|
||||
@@ -265,6 +278,9 @@ fn collect_project_info(
|
||||
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(
|
||||
root: &Path,
|
||||
profile: &str,
|
||||
@@ -576,6 +592,10 @@ fn find_in_path(name: &str) -> Option<String> {
|
||||
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> {
|
||||
let t = name.trim();
|
||||
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()))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if let Some(d) = configured {
|
||||
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")
|
||||
}
|
||||
|
||||
/// Converts days since the Unix epoch to `(year, month, day)` (proleptic Gregorian).
|
||||
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||
let z = days + 719468;
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Collects [`ProjectInfo`] for the root package and workspace members (skips failed members).
|
||||
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
||||
let root = canonicalize_path(root);
|
||||
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`: print project metadata JSON; writes `.dcr` meta from the last entry.
|
||||
fn gen_project_info(args: &[String]) -> i32 {
|
||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||
Ok(v) => v,
|
||||
@@ -811,6 +844,7 @@ fn gen_project_info(args: &[String]) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Pretty multi-line JSON object for one [`ProjectInfo`] (used inside a JSON array).
|
||||
fn project_info_to_json(info: &ProjectInfo) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(" {\n");
|
||||
@@ -896,6 +930,7 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
|
||||
|
||||
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
||||
|
||||
/// `dcr gen compile-commands`: write `.dcr/compile_commands.json`.
|
||||
fn gen_compile_commands(args: &[String]) -> i32 {
|
||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||
Ok(v) => v,
|
||||
@@ -913,6 +948,9 @@ fn gen_compile_commands(args: &[String]) -> i32 {
|
||||
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 {
|
||||
if let Err(e) = ensure_dcr_dir(root) {
|
||||
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 {
|
||||
let mut out = String::from("[\n");
|
||||
let mut first = true;
|
||||
@@ -972,6 +1011,8 @@ fn build_compile_commands(projects: &[ProjectInfo], profile: &str) -> String {
|
||||
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> {
|
||||
let mut cmd: Vec<String> = Vec::new();
|
||||
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 {
|
||||
"release" => {
|
||||
cmd.push("-O3".to_string());
|
||||
@@ -1076,6 +1117,7 @@ fn strip_src_prefix(p: &Path) -> PathBuf {
|
||||
|
||||
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
||||
|
||||
/// `dcr gen vscode`: compile_commands plus `.vscode/` tasks, launch, settings, extensions.
|
||||
fn gen_vscode(args: &[String]) -> i32 {
|
||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||
Ok(v) => v,
|
||||
@@ -1216,6 +1258,8 @@ fn gen_tasks_json() -> 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 {
|
||||
if info.profile == profile
|
||||
&& 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 {
|
||||
let d = info.debugger.to_lowercase();
|
||||
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 {
|
||||
let mut configs = Vec::new();
|
||||
|
||||
@@ -1361,6 +1407,7 @@ fn gen_settings_json(root: &Path) -> String {
|
||||
|
||||
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// `dcr gen clion`: compile_commands plus `.idea/` tools, targets, misc, run configs.
|
||||
fn gen_clion(args: &[String]) -> i32 {
|
||||
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||
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 {
|
||||
let bin_path = resolve_launch_program(info, profile);
|
||||
let bin = xml_escape(&bin_path);
|
||||
@@ -1623,6 +1671,7 @@ fn xml_escape(s: &str) -> String {
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Parses `--debug` / `--release` / `--quiet` and resolves the project root.
|
||||
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
|
||||
let mut profile = "debug".to_string();
|
||||
let mut quiet = false;
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
|
||||
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 {
|
||||
println!("DCR (Dexoron Cargo Realization)");
|
||||
println!("C project manager inspired by Cargo.");
|
||||
|
||||
@@ -25,6 +25,10 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -36,6 +40,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse options (--vcs) and collect positional arguments.
|
||||
let mut vcs_str = None;
|
||||
let mut clean_args = Vec::new();
|
||||
let mut iter = args.iter();
|
||||
@@ -59,6 +64,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolve target project name from current directory name and enforce empty directory check.
|
||||
let items = check_dir(None).unwrap_or_default();
|
||||
let project_name = std::env::current_dir()
|
||||
.ok()
|
||||
@@ -84,6 +90,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
.unwrap_or_else(|_| ".".to_string());
|
||||
println!("Initializing the project in {cwd}");
|
||||
|
||||
// Create and initialize default manifest (dcr.toml).
|
||||
let mut config = match Config::new("./dcr.toml") {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
@@ -104,6 +111,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
colored("dcr.toml", BOLD_CYAN)
|
||||
);
|
||||
|
||||
// Create src directory and write template main.c file.
|
||||
if fs::create_dir("src").is_err() {
|
||||
error("Failed to create src/");
|
||||
return 1;
|
||||
@@ -125,7 +133,7 @@ pub fn init(args: &[String]) -> i32 {
|
||||
colored("src/main.c", BOLD_CYAN)
|
||||
);
|
||||
|
||||
// settings VCS
|
||||
// Resolve target VCS provider or detect existing repositories.
|
||||
let mut vcs_kind = VcsKind::Git;
|
||||
if let Some(ref vcs_val) = vcs_str {
|
||||
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::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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
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 src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
||||
|
||||
// Construct source roots from config
|
||||
let mut source_roots: Vec<String> = Vec::new();
|
||||
for raw in &build_roots {
|
||||
let p = Path::new(raw);
|
||||
@@ -126,6 +130,7 @@ pub fn lint(args: &[String]) -> i32 {
|
||||
let mut total_issues = 0u32;
|
||||
let mut failed = 0u32;
|
||||
|
||||
// Execute clang-tidy for each file and count issues
|
||||
for file in &files {
|
||||
let mut cmd = Command::new("clang-tidy");
|
||||
if do_fix {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// 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 build;
|
||||
pub mod clean;
|
||||
|
||||
@@ -25,6 +25,10 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -38,6 +42,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
|
||||
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 clean_args = Vec::new();
|
||||
let mut iter = args.iter();
|
||||
@@ -103,6 +108,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
project_name
|
||||
);
|
||||
|
||||
// Initialize and write project configuration manifest (dcr.toml).
|
||||
let toml_path = format!("./{project_name}/dcr.toml");
|
||||
let mut config = match Config::new(&toml_path) {
|
||||
Ok(cfg) => cfg,
|
||||
@@ -124,6 +130,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
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() {
|
||||
error("Failed to create src/");
|
||||
return 1;
|
||||
@@ -146,7 +153,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
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;
|
||||
if let Some(ref vcs_val) = vcs_str {
|
||||
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::process::Command;
|
||||
|
||||
/// Retrieves the run command from the config, preferring target-specific,
|
||||
/// then profile-specific, then the base `run.cmd`.
|
||||
fn get_run_cmd(
|
||||
config: &Config,
|
||||
profile: &str,
|
||||
@@ -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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
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(
|
||||
root: &Path,
|
||||
flags: &crate::cli::flags::BuildRunFlags,
|
||||
@@ -290,6 +301,7 @@ fn run_project(
|
||||
Err("Fix errors in the code to run the project".to_string())
|
||||
}
|
||||
|
||||
/// Builds the list of arguments for the build command based on the provided flags.
|
||||
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||
let mut args = Vec::new();
|
||||
args.push(format!("--{}", flags.profile));
|
||||
@@ -313,6 +325,7 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||
args
|
||||
}
|
||||
|
||||
/// Constructs build arguments specifically for a workspace member.
|
||||
fn args_for_workspace_build(
|
||||
flags: &crate::cli::flags::BuildRunFlags,
|
||||
member_name: &str,
|
||||
@@ -337,6 +350,7 @@ fn args_for_workspace_build(
|
||||
args
|
||||
}
|
||||
|
||||
/// Executes the given shell command and returns its exit code.
|
||||
fn run_shell(cmd: &str) -> i32 {
|
||||
let status = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").arg("/C").arg(cmd).status()
|
||||
@@ -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 {
|
||||
if bin_args.is_empty() {
|
||||
return run_shell(cmd);
|
||||
@@ -362,6 +377,7 @@ fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
||||
run_shell(&full)
|
||||
}
|
||||
|
||||
/// Formats the run command string for console output, appending arguments if any.
|
||||
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
||||
if bin_args.is_empty() {
|
||||
return cmd.to_string();
|
||||
@@ -374,6 +390,7 @@ fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
||||
display
|
||||
}
|
||||
|
||||
/// Formats the binary path for console display, appending arguments if any.
|
||||
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
||||
if bin_args.is_empty() {
|
||||
return bin_path.to_string();
|
||||
@@ -386,6 +403,7 @@ fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
||||
display
|
||||
}
|
||||
|
||||
/// Escapes the argument for safe shell execution, using double quotes on Windows and single quotes on Unix.
|
||||
fn shell_escape(arg: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
||||
@@ -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 {
|
||||
let info = parse_version_info(version);
|
||||
substitute_vars(cmd, &info, profile, "")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -29,6 +30,7 @@ pub fn setup(args: &[String]) -> i32 {
|
||||
}
|
||||
|
||||
println!("Setting up DCR registries...");
|
||||
// Load and process the registries, returning 0 on success or 1 on error
|
||||
match crate::core::registry::RegistryManager::load() {
|
||||
Ok(manager) => {
|
||||
println!("Loaded {} registries.", manager.config.registry.len());
|
||||
|
||||
@@ -31,6 +31,7 @@ use std::process::Command;
|
||||
|
||||
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||
|
||||
/// Runs project tests and prints a unified testsuite report.
|
||||
pub fn test(args: &[String]) -> i32 {
|
||||
let mut init_header = false;
|
||||
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> {
|
||||
if build::build(&[format!("--{profile}")]) != 0 {
|
||||
return Ok(1);
|
||||
@@ -260,6 +262,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Collects all .c source files from the tests/ directory.
|
||||
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
||||
let tests_dir = Path::new("./tests");
|
||||
if !tests_dir.is_dir() {
|
||||
@@ -278,6 +281,7 @@ fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
|
||||
Ok(sources)
|
||||
}
|
||||
|
||||
/// Returns a list of include directories for test compilation.
|
||||
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
||||
let mut dirs = vec![
|
||||
"./tests".to_string(),
|
||||
@@ -288,6 +292,7 @@ fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
|
||||
dirs
|
||||
}
|
||||
|
||||
/// Compiles a single test source file to an object file.
|
||||
fn compile_test_source(
|
||||
compiler: &str,
|
||||
standard: &str,
|
||||
@@ -317,6 +322,7 @@ fn compile_test_source(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Links the test object file into an executable binary.
|
||||
fn link_test_binary(
|
||||
compiler: &str,
|
||||
ldflags: &[String],
|
||||
@@ -343,6 +349,7 @@ fn link_test_binary(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints the stdout and stderr from a command execution.
|
||||
fn print_command_output(output: &std::process::Output) {
|
||||
if !output.stdout.is_empty() {
|
||||
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> {
|
||||
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||
@@ -363,6 +371,7 @@ fn ensure_test_header() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts test case names from a C source file by parsing TEST_CASE macros.
|
||||
fn extract_test_names_path(path: &Path) -> Vec<String> {
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
@@ -379,6 +388,7 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Prints a test result field with color coding.
|
||||
fn print_field(label: &str, value: i32, color: &str) {
|
||||
if value == 0 {
|
||||
println!("{}: {}", label, value);
|
||||
|
||||
@@ -22,6 +22,10 @@ use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
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 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
@@ -62,6 +66,9 @@ pub fn tree(args: &[String]) -> i32 {
|
||||
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(
|
||||
deps: &toml::value::Table,
|
||||
base_path: &Path,
|
||||
@@ -81,6 +88,7 @@ fn print_deps(
|
||||
let mut sub_deps = None;
|
||||
let mut resolved_path = None;
|
||||
|
||||
// Resolve dependency path if it's a path-based dependency
|
||||
if let Some(path) = &dep_path {
|
||||
let dcr_toml = path.join("dcr.toml");
|
||||
if dcr_toml.exists()
|
||||
@@ -117,6 +125,7 @@ fn print_deps(
|
||||
|
||||
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
|
||||
&& !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> {
|
||||
match value {
|
||||
Value::String(s) => s
|
||||
|
||||
Reference in New Issue
Block a user