feat: complete structured CLI output

- unify human, plain, and JSON CLI output modes

- standardize diagnostics, lifecycle events, colors, quiet, and verbose handling

- preserve argument forwarding and improve CLI help and summaries

- document and release version 0.9.5
This commit is contained in:
2026-10-21 18:28:31 +08:00
parent bfd2b9d251
commit 0f456b4201
63 changed files with 2619 additions and 1317 deletions

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::path::{Path, PathBuf};

View File

@@ -15,9 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -20,15 +20,10 @@
/// 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::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::platform;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use std::{fs, path::Path, process::Command, time::Instant};
/// Builds the project target using the MSVC compiler.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -769,8 +769,10 @@ fn build_project_at_inner(
)?;
}
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
let mut combined_excludes = Vec::new();
for dir in exclude_dirs {
combined_excludes.push(dir.clone());
@@ -1011,6 +1013,8 @@ fn extend_transitive_link_deps(
consumer_deps.include_dirs.extend(deps.include_dirs);
consumer_deps.lib_dirs.extend(deps.lib_dirs);
consumer_deps.libs.extend(deps.libs);
consumer_deps.cflags.extend(deps.cflags);
consumer_deps.ldflags.extend(deps.ldflags);
for child_root in deps.package_roots {
extend_transitive_link_deps(
Path::new(&child_root),

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::path::Path;
use std::time::Instant;
use std::{path::Path, time::Instant};
/// Keeps only assembler-relevant flags from a cflags list.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds an assembly file using the FASM assembler.
///

View File

@@ -15,12 +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/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds GAS (`.s`) sources via the GNU Assembler.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds MASM (`.asm`) sources via `ml`.
///

View File

@@ -16,12 +16,9 @@
// 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::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds assembly sources using the NASM assembler.
///

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::{build::builder::BuildContext, build::common};
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -1,10 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::Language;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common, language::Language};
use std::{fs, path::Path, process::Command};
/// Represents the LLVM IR language implementation for the build system.
pub struct LlvmIr;

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::core::{build::engine::ToolchainExecs, build_config::Config};
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;

View File

@@ -15,12 +15,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use toml::map::Map;
use toml::{Value, map::Map};
use toml_edit::{DocumentMut, Item, Table};
const DEFAULT_VERSION: &str = "0.1.0";
@@ -244,13 +245,19 @@ pub struct WorkspaceMemberConfig {
}
impl Config {
#[allow(dead_code)]
/// 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> {
Self::create(path, None)
}
/// Creates a new Config instance with an explicit package name for the default template when creating a new file.
pub fn create(path: &str, package_name: Option<&str>) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
let (data, doc) = if path.exists() {
load_parts(&path)?
} else {
let content = default_toml_text();
let content = default_toml_text(package_name)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?;
@@ -328,35 +335,35 @@ impl Config {
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(required) = required else {
crate::utils::log::warn(&format!(
warn!(
"package.dcr-version is missing in {}. Add e.g. dcr-version = \"{tool}\" \
so collaborators know which dcr this project targets \
(written automatically by `dcr new` / `dcr init`).",
self.path.display()
));
so collaborators know which dcr this project targets \
(written automatically by `dcr new` / `dcr init`).",
self.path.display(),
);
return;
};
match crate::utils::build::compare_semver(tool, required) {
Some(std::cmp::Ordering::Less) => {
crate::utils::log::warn(&format!(
warn!(
"this project requires dcr {required} (package.dcr-version), \
but you are running dcr {tool}. Some features may be missing or misbehave. \
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
));
);
}
Some(std::cmp::Ordering::Greater) => {
crate::utils::log::warn(&format!(
warn!(
"this project pins dcr {required} (package.dcr-version), \
but you are running newer dcr {tool}. Config/defaults may be outdated. \
Review the changelog and bump package.dcr-version when the project is verified."
));
but you are running newer dcr {tool}. Config/defaults may be outdated. \
Review the changelog and bump package.dcr-version when the project is verified.",
);
}
Some(std::cmp::Ordering::Equal) => {}
None => {
crate::utils::log::warn(&format!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring"
));
warn!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring",
);
}
}
}
@@ -830,14 +837,20 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
Ok((data, doc))
}
fn default_toml_text() -> String {
let name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
let name = package_name
.map(|s| s.to_string())
.or_else(|| {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
})
.unwrap_or_else(|| "project".to_string());
validate_non_empty_string(&name, "package.name")?;
validate_package_name(&name)?;
let dcr_version = env!("CARGO_PKG_VERSION");
format!(
Ok(format!(
"[package]\n\
name = \"{name}\"\n\
version = \"{DEFAULT_VERSION}\"\n\
@@ -854,7 +867,7 @@ fn default_toml_text() -> String {
kind = \"{DEFAULT_KIND}\"\n\
\n\
[dependencies]\n"
)
))
}
fn dcr_version_warn_once(path_key: &str) -> bool {
@@ -1285,6 +1298,22 @@ mod tests {
);
}
#[test]
fn create_rejects_invalid_name_without_writing_manifest() {
let dir = temp_dir("invalid_create_name");
let path = dir.join("dcr.toml");
for name in ["", "bad name", "../bad", "bad\"name", " bad", "bad."] {
let result = Config::create(&path.to_string_lossy(), Some(name));
assert!(matches!(result, Err(ConfigError::Invalid(_))), "{name:?}");
assert!(!path.exists(), "invalid name must not create a manifest");
}
let config = Config::create(&path.to_string_lossy(), Some("valid-name")).unwrap();
assert_eq!(
config.get("package.name").and_then(Value::as_str),
Some("valid-name")
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");

View File

@@ -4,5 +4,7 @@ pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
pub cflags: Vec<String>,
pub ldflags: Vec<String>,
pub package_roots: Vec<String>,
}

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Represents a locked dependency entry containing its name, version, source, and checksum.
#[derive(Debug, Clone)]

View File

@@ -23,6 +23,7 @@ pub mod register;
use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
use std::path::Path;
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
@@ -73,7 +74,22 @@ pub fn resolve_deps(
if let Some(deps) = deps_table {
// Process each dependency declaration in the TOML table
for (name, value) in deps {
if register::is_registry_dep(value) {
if let Some(packages) = pkg_config_dep(value, name)? {
let (cflags, ldflags) = resolve_pkg_config_flags(&packages, &[], &[])?;
resolved.cflags.extend(cflags);
resolved.ldflags.extend(ldflags);
let version = packages
.first()
.and_then(|pkg| run_pkg_config(pkg, "--modversion").ok())
.map(|v| v.trim().to_string())
.unwrap_or_default();
lock_packages.push(DepLock {
name: name.clone(),
version,
checksum: String::new(),
source: format!("pkg-config+{}", packages.join(",")),
});
} else if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
@@ -274,6 +290,48 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
None
}
/// Extracts one or more pkg-config package names from a dependency table.
/// Both `pkg-config` and the legacy-style `pkg_config` spelling are accepted.
fn pkg_config_dep(
value: &toml::Value,
dependency_name: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(table) = value.as_table() else {
return Ok(None);
};
let raw = table.get("pkg-config").or_else(|| table.get("pkg_config"));
let Some(raw) = raw else {
return Ok(None);
};
let packages = if let Some(pkg) = raw.as_str() {
vec![pkg.to_string()]
} else {
raw.as_array()
.ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must be a string or array of strings"
)
})?
.iter()
.map(|value| {
value.as_str().map(str::to_string).ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must contain only strings"
)
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.collect()
};
if packages.is_empty() || packages.iter().any(|pkg| pkg.trim().is_empty()) {
return Err(format!(
"Dependency `{dependency_name}` field `pkg-config` must not be empty"
));
}
Ok(Some(packages))
}
/// 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> {
if let Some(table) = value.as_table() {
@@ -436,6 +494,20 @@ mod tests {
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
}
#[test]
fn pkg_config_dep_accepts_string_and_array() {
let single: Value = toml::from_str("pkg-config = \"fmt\"").unwrap();
assert_eq!(
pkg_config_dep(&single, "fmt"),
Ok(Some(vec!["fmt".to_string()]))
);
let many: Value = toml::from_str("pkg_config = [\"fmt\", \"zlib\"]").unwrap();
assert_eq!(
pkg_config_dep(&many, "base"),
Ok(Some(vec!["fmt".to_string(), "zlib".to_string()]))
);
}
/// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
#[test]
fn default_lib_dirs_include_packaged_library_output() {

View File

@@ -17,9 +17,7 @@
/// Registry configuration management for DCR.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::{collections::HashMap, fs, path::PathBuf};
/// Represents a registry configuration entry with its URL and priority.
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -16,8 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::platform;
use std::path::Path;
use std::process::Command;
use std::{path::Path, process::Command};
/// Runs the built binary for a project, forwarding `bin_args`.
///

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
///