mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 09:28:35 +00:00
346 lines
12 KiB
Rust
346 lines
12 KiB
Rust
// DCR — Cargo-like C/C++ project manager.
|
|
//
|
|
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
use crate::core::build_config::Config;
|
|
use crate::core::deps::register;
|
|
use crate::utils::fs::find_project_root;
|
|
use crate::utils::log::error;
|
|
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`, `pkg_config`, 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>,
|
|
/// Package names resolved through pkg-config.
|
|
pub pkg_config: Option<Vec<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);
|
|
printc(
|
|
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
|
|
BOLD_CYAN,
|
|
);
|
|
println!();
|
|
printc("DESCRIPTION:", BOLD_GREEN);
|
|
println!(" Adds a dependency to the project.");
|
|
println!();
|
|
printc("SOURCES:", BOLD_GREEN);
|
|
println!(" github:user/repo GitHub repository");
|
|
println!(" gitlab:user/repo GitLab repository");
|
|
println!(" git:host.com/user/repo Generic git repository");
|
|
println!(" path:./path/to/lib Local path dependency");
|
|
println!(" pkg-config:fmt System pkg-config dependency");
|
|
println!(" <version> Version from registry");
|
|
println!();
|
|
printc("OPTIONS:", BOLD_GREEN);
|
|
println!(" --branch <b> Use a specific branch");
|
|
println!(" --tag <t> Use a specific tag");
|
|
println!(" --rev <r> Use a specific commit hash");
|
|
return 0;
|
|
}
|
|
|
|
let add_args = match parse_add_args(args) {
|
|
Ok(a) => a,
|
|
Err(code) => return code,
|
|
};
|
|
|
|
let start_dir = match std::env::current_dir() {
|
|
Ok(dir) => dir,
|
|
Err(_) => {
|
|
error("Failed to determine current directory");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let root = match find_project_root(&start_dir) {
|
|
Ok(Some(dir)) => dir,
|
|
Ok(None) => {
|
|
error("dcr.toml file not found");
|
|
return 1;
|
|
}
|
|
Err(_) => {
|
|
error("Failed to find project root");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
|
|
Ok(cfg) => cfg,
|
|
Err(err) => {
|
|
error(&err.to_string());
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
// 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();
|
|
table.insert("git".to_string(), Value::String(git_url));
|
|
Value::Table(table)
|
|
} else {
|
|
let mut table = Map::new();
|
|
table.insert("git".to_string(), Value::String(git_url));
|
|
if let Some(b) = add_args.branch {
|
|
table.insert("branch".to_string(), Value::String(b));
|
|
}
|
|
if let Some(t) = add_args.tag {
|
|
table.insert("tag".to_string(), Value::String(t));
|
|
}
|
|
if let Some(r) = add_args.rev {
|
|
table.insert("rev".to_string(), Value::String(r));
|
|
}
|
|
Value::Table(table)
|
|
}
|
|
} else if let Some(packages) = add_args.pkg_config {
|
|
let mut table = Map::new();
|
|
if packages.len() == 1 {
|
|
table.insert("pkg-config".to_string(), Value::String(packages[0].clone()));
|
|
} else {
|
|
table.insert(
|
|
"pkg-config".to_string(),
|
|
Value::Array(packages.into_iter().map(Value::String).collect()),
|
|
);
|
|
}
|
|
Value::Table(table)
|
|
} else if let Some(path) = add_args.path {
|
|
let mut table = Map::new();
|
|
table.insert("path".to_string(), Value::String(path));
|
|
Value::Table(table)
|
|
} else if let Some(version) = add_args.version_from_registry {
|
|
Value::String(version)
|
|
} else {
|
|
error("Dependency source (path, git, or pkg-config) must be provided");
|
|
return 1;
|
|
};
|
|
|
|
let key = format!("dependencies.{}", add_args.name);
|
|
if let Err(err) = config.edit(&key, dep_value) {
|
|
error(&format!("Failed to update dcr.toml: {}", err));
|
|
return 1;
|
|
}
|
|
|
|
println!(
|
|
" {} dependency `{}` to dcr.toml",
|
|
colored("Added", BOLD_GREEN),
|
|
add_args.name
|
|
);
|
|
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>]");
|
|
error("Sources:");
|
|
error(" github:user/repo -> github.com/user/repo");
|
|
error(" gitlab:user/repo -> gitlab.com/user/repo");
|
|
error(" git:host.com/user/repo -> host.com/user/repo");
|
|
error(" path:./path/to/lib -> local path");
|
|
error(" pkg-config:fmt -> system pkg-config package");
|
|
return Err(1);
|
|
}
|
|
|
|
let mut name = String::new();
|
|
let source_spec: String;
|
|
let mut branch = None;
|
|
let mut tag = None;
|
|
let mut rev = None;
|
|
|
|
let mut iter = args.iter();
|
|
if let Some(n) = iter.next() {
|
|
if n.starts_with("--") {
|
|
error("First argument must be the dependency name");
|
|
return Err(1);
|
|
}
|
|
name = n.clone();
|
|
}
|
|
|
|
if let Some(s) = iter.next() {
|
|
if s.starts_with("--") {
|
|
error("Second argument must be the dependency source");
|
|
return Err(1);
|
|
}
|
|
source_spec = s.clone();
|
|
} else {
|
|
// No source provided — resolve version from the package registry
|
|
match register::resolve_package_from_registry(&name) {
|
|
Ok(info) => {
|
|
let version = info
|
|
.get("latest_version")
|
|
.or_else(|| info.get("version"))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| {
|
|
(
|
|
"Registry entry for `".to_string() + &name + "` has no version field",
|
|
1,
|
|
)
|
|
})
|
|
.map_err(|(msg, code)| {
|
|
error(&msg);
|
|
code
|
|
})?;
|
|
return Ok(AddArgs {
|
|
name,
|
|
path: None,
|
|
git: None,
|
|
branch: None,
|
|
tag: None,
|
|
rev: None,
|
|
version_from_registry: Some(version.to_string()),
|
|
pkg_config: None,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
error(&format!("Cannot add `{}` without source: {}", name, e));
|
|
return Err(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
while let Some(arg) = iter.next() {
|
|
match arg.as_str() {
|
|
"--branch" => {
|
|
branch = iter.next().cloned();
|
|
if branch.is_none() {
|
|
error("--branch requires a value");
|
|
return Err(1);
|
|
}
|
|
}
|
|
"--tag" => {
|
|
tag = iter.next().cloned();
|
|
if tag.is_none() {
|
|
error("--tag requires a value");
|
|
return Err(1);
|
|
}
|
|
}
|
|
"--rev" => {
|
|
rev = iter.next().cloned();
|
|
if rev.is_none() {
|
|
error("--rev requires a value");
|
|
return Err(1);
|
|
}
|
|
}
|
|
_ => {
|
|
error(&format!("Unknown argument: {}", arg));
|
|
return Err(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut path = None;
|
|
let mut git = None;
|
|
let mut pkg_config = None;
|
|
|
|
// Map source prefixes / bare URLs to path or git fields
|
|
if let Some(p) = source_spec
|
|
.strip_prefix("pkg-config:")
|
|
.or_else(|| source_spec.strip_prefix("pkg:"))
|
|
{
|
|
let packages = p
|
|
.split(',')
|
|
.map(str::trim)
|
|
.filter(|name| !name.is_empty())
|
|
.map(str::to_string)
|
|
.collect::<Vec<_>>();
|
|
if packages.is_empty() {
|
|
error("pkg-config source must name at least one package");
|
|
return Err(1);
|
|
}
|
|
pkg_config = Some(packages);
|
|
} else if let Some(p) = source_spec.strip_prefix("path:") {
|
|
path = Some(p.to_string());
|
|
} else if let Some(g) = source_spec.strip_prefix("github:") {
|
|
git = Some(format!("https://github.com/{}", g));
|
|
} else if let Some(g) = source_spec.strip_prefix("gitlab:") {
|
|
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 with no host — default to GitHub
|
|
git = Some(format!("https://github.com/{}", g));
|
|
} else {
|
|
// 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 {
|
|
format!("https://{}", g)
|
|
};
|
|
git = Some(url);
|
|
}
|
|
} else if source_spec.starts_with("http://")
|
|
|| source_spec.starts_with("https://")
|
|
|| source_spec.starts_with("git@")
|
|
{
|
|
git = Some(source_spec);
|
|
} else if source_spec.starts_with("./")
|
|
|| source_spec.starts_with("../")
|
|
|| std::path::Path::new(&source_spec).is_absolute()
|
|
{
|
|
path = Some(source_spec);
|
|
} else {
|
|
error(
|
|
"Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
|
|
);
|
|
return Err(1);
|
|
}
|
|
|
|
Ok(AddArgs {
|
|
name,
|
|
path,
|
|
git,
|
|
branch,
|
|
tag,
|
|
rev,
|
|
version_from_registry: None,
|
|
pkg_config,
|
|
})
|
|
}
|