mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-25 15:22:57 +00:00
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:
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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`.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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`.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user