mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 08:48:30 +00:00
Compare commits
2 Commits
3c25539c11
...
1e3eb658dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e3eb658dc | |||
| f5dad74498 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ personal/
|
||||
.*
|
||||
!.gitignore
|
||||
!.github/
|
||||
graphify-out/
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries"
|
||||
|
||||
### RU
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
- **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`.
|
||||
По умолчанию `auto`: **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
|
||||
- **Полные Path/Git-зависимости** — DCR-пакеты из `path` и `git` собираются перед потребителем; Git-код кешируется в `~/.dcr/git`, поддерживает `branch`/`tag`/`rev` и фиксирует точный commit в `dcr.lock`.
|
||||
- **Транзитивные зависимости и защита от циклов** — DCR рекурсивно собирает DCR-пакеты и показывает цепочку циклической зависимости.
|
||||
- **Внешние готовые библиотеки через path/Git** — `include`, `lib` и `libs` подключают библиотеку без `dcr.toml`; Git prebuilt-источники тоже фиксируются по точному commit.
|
||||
- **Явный режим зависимости** — `mode = "build"` и `mode = "prebuild"` позволяют выбрать сборку DCR-пакета или подключение готовых артефактов, даже когда репозиторий содержит оба варианта.
|
||||
|
||||
**Удалено:**
|
||||
|
||||
@@ -19,6 +23,10 @@
|
||||
|
||||
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
|
||||
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
|
||||
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer; Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and lock the exact commit in `dcr.lock`.
|
||||
- **Transitive dependencies and cycle protection** — DCR recursively builds DCR packages and reports the dependency cycle chain.
|
||||
- **Prebuilt external libraries through path/Git** — `include`, `lib`, and `libs` link libraries without `dcr.toml`; Git prebuilt sources are locked to their exact commit as well.
|
||||
- **Explicit dependency mode** — `mode = "build"` and `mode = "prebuild"` select DCR package builds or ready-made artifacts when a repository contains both forms.
|
||||
|
||||
**Removed:**
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dcr"
|
||||
version = "0.8.5"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"fatfs",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dcr"
|
||||
version = "0.8.5"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||
@@ -89,3 +89,12 @@ lto = false
|
||||
codegen-units = 16
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[profile.dev]
|
||||
incremental = false
|
||||
opt-level = 0
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 1
|
||||
debug = false
|
||||
@@ -39,7 +39,10 @@
|
||||
|
||||
```sh
|
||||
yay -S dcr
|
||||
# or
|
||||
paru -S dcr
|
||||
```
|
||||
> 💡 Do you like DCR? Vote for the package on the [AUR](https://aur.archlinux.org/packages/dcr) page—it will help the project get into the official "extra" repository!
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,10 +4,15 @@ sidebar_label: Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 0.9.0 (2026-08-31)
|
||||
|
||||
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
|
||||
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
|
||||
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer. Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and record the exact commit in `dcr.lock`.
|
||||
- **Build and prebuild modes** — `path` and `git` dependencies can build a DCR package from `dcr.toml` or link existing `include`/`lib`/`libs` artifacts; `mode = "build"` / `"prebuild"` selects explicitly when both forms are present.
|
||||
- **Prebuilt dependency examples** — documentation now shows the `dcr-math-lib` Git repository and recommended build/prebuilt layouts for both `git` and `path`.
|
||||
- **Transitive dependency builds and cycle reporting** — DCR recursively builds DCR packages and reports a clear dependency chain for cycles.
|
||||
- **Bare path arguments for `dcr add`** — `dcr add mylib ../mylib` now works in addition to the explicit `path:` form.
|
||||
- **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
|
||||
|
||||
## 0.8.5 (2026-08-08)
|
||||
|
||||
@@ -29,7 +29,7 @@ Flags:
|
||||
- `--tag <name>` — git tag
|
||||
- `--rev <hash>` — git commit
|
||||
|
||||
If source is omitted — DCR searches connected registries.
|
||||
If source is omitted — DCR searches connected registries. A bare relative or absolute path is accepted as a path dependency.
|
||||
|
||||
## `dcr tree`
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ The version string is used as-is for registry lookup.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
fmt = { git = "https://github.com/fmtlib/fmt", tag = "10.1.1" }
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0" }
|
||||
```
|
||||
|
||||
Fields: `git`, `branch`, `tag`, `rev`.
|
||||
Fields: `git`, `branch`, `tag`, `rev`, `mode`, `include`, `lib`, `libs`.
|
||||
|
||||
### Table (path)
|
||||
|
||||
@@ -35,6 +35,15 @@ Fields: `git`, `branch`, `tag`, `rev`.
|
||||
mylib = { path = "../mylib" }
|
||||
```
|
||||
|
||||
For a prebuilt external library, specify its layout explicitly. The three
|
||||
arrays are paths relative to the dependency root (`include`, `lib`) and linker
|
||||
names (`libs`):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { path = "../deps/dcr-math", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
## Registry
|
||||
|
||||
DCR uses a package registry for dependency lookup by name.
|
||||
@@ -42,7 +51,7 @@ DCR uses a package registry for dependency lookup by name.
|
||||
```toml
|
||||
# ~/.dcr/config.toml
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
url = "https://index.dcr.dexoron.tech"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
@@ -50,7 +59,23 @@ Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrid
|
||||
|
||||
## Git dependencies
|
||||
|
||||
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
|
||||
Git dependencies are cloned into `~/.dcr/git/`; their exact checked-out commit is written to `dcr.lock`.
|
||||
|
||||
Both Git and path dependencies have two modes:
|
||||
|
||||
- **DCR package:** the source has `dcr.toml`. DCR builds it before the consumer and uses the package name from its manifest for linking.
|
||||
- **Prebuilt library:** it provides existing headers and binaries through `include`, `lib`, and `libs`. DCR passes them to the compiler/linker without building the dependency.
|
||||
|
||||
The mode is normally detected automatically. Use `mode = "build"` or `mode = "prebuild"` when a source contains both a manifest and prebuilt artifacts and you need to choose explicitly.
|
||||
|
||||
`dcr-math-lib` publishes both forms. Select its prebuilt library explicitly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
DCR supports specifying branch/tag/rev for git sources:
|
||||
|
||||
- `branch` — switch to a branch
|
||||
- `tag` — switch to a tag
|
||||
@@ -59,7 +84,7 @@ Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying
|
||||
|
||||
## Path dependencies
|
||||
|
||||
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
|
||||
Local paths may point at a DCR package or a prebuilt external library. A path DCR package is built before its consumer, including its own path/git dependencies. DCR detects dependency cycles. A prebuilt library is never built by DCR; it is linked from the directories declared in its dependency entry.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
@@ -67,18 +92,51 @@ mylib = { path = "/abs/path/to/lib" }
|
||||
mylib = { path = "../relative/path" }
|
||||
```
|
||||
|
||||
*Note: Header resolution automatically checks the packaged `target/include` directory of path dependencies, ensuring consumer packages can find headers from compiled static or dynamic libraries.*
|
||||
For DCR packages, the package name from `dcr.toml` is used for linking (so the dependency key may differ from the library name). Header resolution includes the packaged `target/include` directory and link search includes `target/lib`.
|
||||
|
||||
### Recommended library layouts
|
||||
|
||||
Buildable DCR library:
|
||||
|
||||
```text
|
||||
dcr-math-lib/
|
||||
├── dcr.toml
|
||||
└── src/
|
||||
├── dcr_math.c
|
||||
└── dcr_math.h
|
||||
```
|
||||
|
||||
Prebuilt library:
|
||||
|
||||
```text
|
||||
dcr-math-lib/
|
||||
├── include/
|
||||
│ └── dcr_math.h
|
||||
└── lib/
|
||||
└── libdcr-math.a # or .so / .lib
|
||||
```
|
||||
|
||||
`libs` contains linker names without the `lib` prefix and file extension: use
|
||||
`["dcr-math"]` for `libdcr-math.a`.
|
||||
|
||||
If one repository contains both forms, keep the prebuilt files in `include/` and `lib/` and select the mode explicitly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
|
||||
```
|
||||
|
||||
For a local copy, replace `git` with `path = "../dcr-math-lib"`. In `prebuild` mode DCR does not run the dependency build, even when `dcr.toml` is present.
|
||||
|
||||
## dcr.lock
|
||||
|
||||
Dependency lock file. Contains package names and sources.
|
||||
|
||||
Created during `dcr build` when registry dependencies are present. Not updated during `dcr add` — only on the next `dcr build`.
|
||||
Created during `dcr build` for resolved registry, path, and Git dependencies. Not updated during `dcr add` — only on the next `dcr build`.
|
||||
|
||||
## Resolution process
|
||||
|
||||
1. Load all dependencies (registry → git → path)
|
||||
2. For path deps: recursively read their `dcr.toml`
|
||||
3. For registry deps: search `index.json`
|
||||
4. For git deps: clone to cache
|
||||
5. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
|
||||
1. Resolve dependency sources (registry → git → path)
|
||||
2. Fetch git dependencies into `~/.dcr/git/` and check out the requested ref
|
||||
3. Build DCR package dependencies recursively
|
||||
4. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
|
||||
|
||||
@@ -288,8 +288,15 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
|| 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 have a prefix (path:, git:, github:, gitlab:) or be a full URL");
|
||||
error(
|
||||
"Source must be a path, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
|
||||
);
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::core::build::steps::{
|
||||
get_build_steps_with_profile, run_build_steps, verify_expectations,
|
||||
};
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::deps::{register, resolve_deps};
|
||||
use crate::core::deps::resolve_deps;
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{
|
||||
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
|
||||
@@ -335,6 +335,81 @@ fn build_project_at(
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<(), String> {
|
||||
let mut dependency_stack = Vec::new();
|
||||
build_project_at_with_stack(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
&mut dependency_stack,
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a package while tracking the dependency chain to reject cycles.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_with_stack(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
let root = project_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| project_root.to_path_buf());
|
||||
if let Some(index) = dependency_stack.iter().position(|item| item == &root) {
|
||||
let mut chain: Vec<String> = dependency_stack[index..]
|
||||
.iter()
|
||||
.map(|item| item.display().to_string())
|
||||
.collect();
|
||||
chain.push(root.display().to_string());
|
||||
return Err(format!("Dependency cycle detected: {}", chain.join(" -> ")));
|
||||
}
|
||||
dependency_stack.push(root);
|
||||
let result = build_project_at_inner(
|
||||
project_root,
|
||||
profile,
|
||||
target,
|
||||
explicit_target,
|
||||
exclude_dirs,
|
||||
force,
|
||||
verbose,
|
||||
workspace_root,
|
||||
cancel,
|
||||
rep,
|
||||
dependency_stack,
|
||||
);
|
||||
dependency_stack.pop();
|
||||
result
|
||||
}
|
||||
|
||||
/// Builds one DCR package after the dependency-cycle guard has been installed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_project_at_inner(
|
||||
project_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
explicit_target: bool,
|
||||
exclude_dirs: &[std::path::PathBuf],
|
||||
force: bool,
|
||||
verbose: bool,
|
||||
workspace_root: Option<&Path>,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
dependency_stack: &mut Vec<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return Err("Build interrupted".to_string());
|
||||
@@ -641,38 +716,34 @@ fn build_project_at(
|
||||
}
|
||||
}
|
||||
|
||||
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
|
||||
let mut resolved = resolve_deps(&config, profile, build_target, project_root)?;
|
||||
resolved.include_dirs.extend(ws_include_dirs);
|
||||
resolved.lib_dirs.extend(ws_lib_dirs);
|
||||
resolved.libs.extend(ws_libs);
|
||||
|
||||
// Registry dependencies are cached under the DCR registry root. Build
|
||||
// them as normal DCR projects before the current project is linked.
|
||||
if let Some(deps) = deps_table {
|
||||
for (name, value) in deps {
|
||||
if register::is_registry_dep(value) {
|
||||
let pkg_info = register::resolve_package_from_registry(name)?;
|
||||
let version = pkg_info
|
||||
.get("latest_version")
|
||||
.or_else(|| pkg_info.get("version"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
|
||||
let include_dir = dep_root.join("target").join("include");
|
||||
let lib_dir = dep_root.join("target").join("lib");
|
||||
|
||||
if !include_dir.exists() || !lib_dir.exists() {
|
||||
rep.on_event(BuildEvent::DepBuilding { name, version });
|
||||
if !dep_root.join("dcr.toml").is_file() {
|
||||
return Err(format!(
|
||||
"Registry dependency `{}` is missing dcr.toml at {}",
|
||||
name,
|
||||
dep_root.display()
|
||||
));
|
||||
let mut built_roots = std::collections::HashSet::new();
|
||||
let mut transitive_roots = std::collections::HashSet::new();
|
||||
let direct_package_roots = resolved.package_roots.clone();
|
||||
for dep_root in &direct_package_roots {
|
||||
if !built_roots.insert(dep_root.clone()) {
|
||||
continue;
|
||||
}
|
||||
build_project_at(
|
||||
&dep_root,
|
||||
let dep_root = Path::new(dep_root);
|
||||
let dep_config =
|
||||
Config::open(&dep_root.join("dcr.toml").to_string_lossy()).map_err(|_| {
|
||||
format!(
|
||||
"DCR dependency is missing dcr.toml at {}",
|
||||
dep_root.display()
|
||||
)
|
||||
})?;
|
||||
let name = get_config_str(&dep_config, "package.name");
|
||||
let version = get_config_str(&dep_config, "package.version");
|
||||
rep.on_event(BuildEvent::DepBuilding {
|
||||
name: &name,
|
||||
version: &version,
|
||||
});
|
||||
build_project_at_with_stack(
|
||||
dep_root,
|
||||
profile,
|
||||
build_target,
|
||||
has_explicit_target,
|
||||
@@ -682,21 +753,20 @@ fn build_project_at(
|
||||
None,
|
||||
cancel,
|
||||
&mut *rep,
|
||||
dependency_stack,
|
||||
)?;
|
||||
rep.on_event(BuildEvent::DepReady {
|
||||
name,
|
||||
version,
|
||||
name: &name,
|
||||
version: &version,
|
||||
rebuilt: true,
|
||||
});
|
||||
} else {
|
||||
rep.on_event(BuildEvent::DepReady {
|
||||
name,
|
||||
version,
|
||||
rebuilt: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
extend_transitive_link_deps(
|
||||
dep_root,
|
||||
profile,
|
||||
build_target,
|
||||
&mut resolved,
|
||||
&mut transitive_roots,
|
||||
)?;
|
||||
}
|
||||
|
||||
let (resolved_cflags, resolved_ldflags) =
|
||||
@@ -919,6 +989,40 @@ fn build_project_at(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds link flags needed by dependencies of a static DCR library.
|
||||
/// Static archives do not contain their own dependency objects, so the final
|
||||
/// consumer must link every transitive package in dependency order.
|
||||
fn extend_transitive_link_deps(
|
||||
package_root: &Path,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
consumer_deps: &mut crate::core::deps::common::ResolvedDeps,
|
||||
visited: &mut std::collections::HashSet<PathBuf>,
|
||||
) -> Result<(), String> {
|
||||
let root = package_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| package_root.to_path_buf());
|
||||
if !visited.insert(root.clone()) {
|
||||
return Ok(());
|
||||
}
|
||||
let config =
|
||||
Config::open(&root.join("dcr.toml").to_string_lossy()).map_err(|err| err.to_string())?;
|
||||
let deps = resolve_deps(&config, profile, target, &root)?;
|
||||
consumer_deps.include_dirs.extend(deps.include_dirs);
|
||||
consumer_deps.lib_dirs.extend(deps.lib_dirs);
|
||||
consumer_deps.libs.extend(deps.libs);
|
||||
for child_root in deps.package_roots {
|
||||
extend_transitive_link_deps(
|
||||
Path::new(&child_root),
|
||||
profile,
|
||||
target,
|
||||
consumer_deps,
|
||||
visited,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts relative paths in flags to absolute paths based on project root for include and link flags.
|
||||
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
|
||||
for prefix in prefixes {
|
||||
|
||||
@@ -4,4 +4,5 @@ pub struct ResolvedDeps {
|
||||
pub include_dirs: Vec<String>,
|
||||
pub lib_dirs: Vec<String>,
|
||||
pub libs: Vec<String>,
|
||||
pub package_roots: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ fn dep_version(path: &Path) -> String {
|
||||
|
||||
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
|
||||
///
|
||||
/// Registry and path deps contribute to `ResolvedDeps`. Git deps are recorded in the lock only
|
||||
/// (no fetch or include/lib resolution).
|
||||
/// Registry, path, and git deps contribute to `ResolvedDeps`. Path and git sources support both
|
||||
/// DCR packages (built from `dcr.toml`) and prebuilt libraries (described with include/lib/libs).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `config`: Loaded `dcr.toml`.
|
||||
@@ -88,6 +88,9 @@ pub fn resolve_deps(
|
||||
.to_string(),
|
||||
);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
@@ -107,6 +110,8 @@ pub fn resolve_deps(
|
||||
});
|
||||
} else if let Some(path) = path_dep_path(value) {
|
||||
let dep_root = project_root.join(path);
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
if let Some(table) = value.as_table() {
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
for inc in includes {
|
||||
@@ -118,10 +123,7 @@ pub fn resolve_deps(
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
@@ -143,16 +145,29 @@ pub fn resolve_deps(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
resolved.libs.push(name.clone());
|
||||
resolved
|
||||
.libs
|
||||
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
|
||||
}
|
||||
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
} else if !has_complete_prebuilt_layout(value) {
|
||||
return Err(format!(
|
||||
"Path dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
|
||||
lock_packages.push(DepLock {
|
||||
@@ -162,11 +177,46 @@ pub fn resolve_deps(
|
||||
source: format!("path+{}", dep_root.display()),
|
||||
});
|
||||
} else if let Some(git_info) = git_dep(value) {
|
||||
let dep_root = register::fetch_git_dependency(
|
||||
git_info.url,
|
||||
git_info.branch.as_deref(),
|
||||
git_info.tag.as_deref(),
|
||||
git_info.rev.as_deref(),
|
||||
)?;
|
||||
let dcr_package_name = package_name(&dep_root);
|
||||
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
|
||||
let (has_include, has_lib, has_libs) = value
|
||||
.as_table()
|
||||
.map(|table| push_explicit_layout(&mut resolved, &dep_root, table))
|
||||
.unwrap_or((false, false, false));
|
||||
if !has_include {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
if use_dcr_package {
|
||||
push_package_output_dirs(&mut resolved, &dep_root);
|
||||
}
|
||||
}
|
||||
if !has_lib {
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
}
|
||||
if !has_libs && let Some(package_name) = dcr_package_name.as_deref() {
|
||||
resolved.libs.push(package_name.to_string());
|
||||
}
|
||||
if use_dcr_package {
|
||||
resolved
|
||||
.package_roots
|
||||
.push(dep_root.to_string_lossy().to_string());
|
||||
} else if !(has_include && has_lib && has_libs) {
|
||||
return Err(format!(
|
||||
"Git dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
|
||||
name
|
||||
));
|
||||
}
|
||||
let commit = register::git_commit(&dep_root)?;
|
||||
lock_packages.push(DepLock {
|
||||
name: name.clone(),
|
||||
version: git_info.version.unwrap_or_default(),
|
||||
checksum: String::new(),
|
||||
source: format!("git+{}", git_info.url),
|
||||
version: git_info.version.unwrap_or(commit.clone()),
|
||||
checksum: commit.clone(),
|
||||
source: format!("git+{}#{}", git_info.url, commit),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -187,6 +237,9 @@ pub fn resolve_deps(
|
||||
struct GitDep<'a> {
|
||||
url: &'a str,
|
||||
version: Option<String>,
|
||||
branch: Option<String>,
|
||||
tag: Option<String>,
|
||||
rev: Option<String>,
|
||||
}
|
||||
|
||||
/// Parses a git dependency from a TOML value table.
|
||||
@@ -198,7 +251,25 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
return Some(GitDep { url, version });
|
||||
let branch = table
|
||||
.get("branch")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
let tag = table
|
||||
.get("tag")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
let rev = table
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string);
|
||||
return Some(GitDep {
|
||||
url,
|
||||
version,
|
||||
branch,
|
||||
tag,
|
||||
rev,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -226,6 +297,117 @@ fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
|
||||
push_if_exists(paths, &dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds explicitly configured prebuilt include/lib directories and linker names.
|
||||
fn push_explicit_layout(
|
||||
resolved: &mut ResolvedDeps,
|
||||
dep_root: &Path,
|
||||
table: &toml::map::Map<String, toml::Value>,
|
||||
) -> (bool, bool, bool) {
|
||||
let mut has_include = false;
|
||||
let mut has_lib = false;
|
||||
let mut has_libs = false;
|
||||
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
|
||||
has_include = true;
|
||||
for inc in includes {
|
||||
if let Some(path) = inc.as_str() {
|
||||
resolved
|
||||
.include_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
has_lib = true;
|
||||
for path in lib_dirs {
|
||||
if let Some(path) = path.as_str() {
|
||||
resolved
|
||||
.lib_dirs
|
||||
.push(dep_root.join(path).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
|
||||
has_libs = true;
|
||||
for lib in libs {
|
||||
if let Some(name) = lib.as_str() {
|
||||
resolved.libs.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
(has_include, has_lib, has_libs)
|
||||
}
|
||||
|
||||
/// Reports whether a source without a DCR manifest fully describes its prebuilt layout.
|
||||
fn has_complete_prebuilt_layout(value: &toml::Value) -> bool {
|
||||
value.as_table().is_some_and(|table| {
|
||||
["include", "lib", "libs"].iter().all(|field| {
|
||||
table
|
||||
.get(*field)
|
||||
.and_then(|value| value.as_array())
|
||||
.is_some_and(|values| !values.is_empty())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Selects automatic, forced-build, or forced-prebuilt dependency resolution.
|
||||
fn use_dcr_package(
|
||||
value: &toml::Value,
|
||||
has_manifest: bool,
|
||||
dependency_name: &str,
|
||||
) -> Result<bool, String> {
|
||||
let mode = value
|
||||
.as_table()
|
||||
.and_then(|table| table.get("mode"))
|
||||
.map(|value| {
|
||||
value.as_str().ok_or_else(|| {
|
||||
format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
match mode {
|
||||
None => Ok(has_manifest),
|
||||
Some("build") if has_manifest => Ok(true),
|
||||
Some("build") => Err(format!(
|
||||
"Dependency `{dependency_name}` uses mode = \"build\" but has no dcr.toml"
|
||||
)),
|
||||
Some("prebuild") => Ok(false),
|
||||
Some(_) => Err(format!(
|
||||
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds DCR's packaged output directories even before the dependency is built.
|
||||
fn push_package_output_dirs(resolved: &mut ResolvedDeps, dep_root: &Path) {
|
||||
push_unique_path(
|
||||
&mut resolved.include_dirs,
|
||||
dep_root.join("target").join("include"),
|
||||
);
|
||||
push_unique_path(&mut resolved.lib_dirs, dep_root.join("target").join("lib"));
|
||||
}
|
||||
|
||||
/// Adds a path once, preserving the linker and include-search order.
|
||||
fn push_unique_path(paths: &mut Vec<String>, path: std::path::PathBuf) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a DCR package name when the dependency has a manifest.
|
||||
fn package_name(path: &Path) -> Option<String> {
|
||||
Config::open(&path.join("dcr.toml").to_string_lossy())
|
||||
.ok()
|
||||
.and_then(|config| {
|
||||
config
|
||||
.get("package.name")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// Represents a registry configuration entry with its URL and priority.
|
||||
@@ -149,6 +151,95 @@ pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
|
||||
dep_root.join("target").join("lib")
|
||||
}
|
||||
|
||||
/// Fetches a git dependency into the DCR cache and returns its working tree.
|
||||
/// The URL and selected ref determine the cache key; an existing checkout is reused.
|
||||
pub fn fetch_git_dependency(
|
||||
url: &str,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
if !crate::utils::git::is_git_available() {
|
||||
return Err("git executable not found in PATH".to_string());
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(branch.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(tag.unwrap_or("").as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(rev.unwrap_or("").as_bytes());
|
||||
let key = format!("{:x}", hasher.finalize());
|
||||
let root = dcr_config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("git")
|
||||
.join(&key);
|
||||
if root.join(".git").is_dir() {
|
||||
let status = Command::new("git")
|
||||
.args(["fetch", "--all", "--tags", "--prune"])
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git fetch: {e}"))?;
|
||||
if !status.success() {
|
||||
eprintln!("Warning: git fetch failed for {url}; using the cached checkout");
|
||||
}
|
||||
return checkout_git_ref(&root, branch, tag, rev);
|
||||
}
|
||||
if root.exists() {
|
||||
return Err(format!(
|
||||
"git cache path is not a repository: {}",
|
||||
root.display()
|
||||
));
|
||||
}
|
||||
if let Some(parent) = root.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("failed to create git cache: {e}"))?;
|
||||
}
|
||||
let status = Command::new("git")
|
||||
.args(["clone", "--", url])
|
||||
.arg(&root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git clone: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git clone failed for {url}"));
|
||||
}
|
||||
checkout_git_ref(&root, branch, tag, rev)
|
||||
}
|
||||
|
||||
/// Returns the exact commit checked out in a cached git dependency.
|
||||
pub fn git_commit(root: &Path) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(root)
|
||||
.output()
|
||||
.map_err(|e| format!("failed to execute git rev-parse: {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("failed to read git revision at {}", root.display()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn checkout_git_ref(
|
||||
root: &Path,
|
||||
branch: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
rev: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
let selected = rev.or(tag).or(branch);
|
||||
let Some(reference) = selected else {
|
||||
return Ok(root.to_path_buf());
|
||||
};
|
||||
let status = Command::new("git")
|
||||
.args(["checkout", "--force", reference])
|
||||
.current_dir(root)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to execute git checkout: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("git checkout failed for ref `{reference}`"));
|
||||
}
|
||||
Ok(root.to_path_buf())
|
||||
}
|
||||
|
||||
/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`).
|
||||
///
|
||||
/// Configured registries are sorted by priority, but the lookup currently always reads the
|
||||
|
||||
@@ -16,6 +16,15 @@ fn dcr_add_dependencies() {
|
||||
"path dep not found in toml"
|
||||
);
|
||||
|
||||
// A bare filesystem path is equivalent to the explicit path: form.
|
||||
let out = run_dcr(&["add", "otherlib", "../otherlib"], &dir);
|
||||
assert!(out.status.success(), "dcr add bare path should succeed");
|
||||
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
|
||||
assert!(
|
||||
toml.contains("otherlib = { path = \"../otherlib\" }"),
|
||||
"bare path dep not found in toml"
|
||||
);
|
||||
|
||||
// Test github: prefix
|
||||
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
|
||||
assert!(out.status.success(), "dcr add github should succeed");
|
||||
@@ -96,6 +105,369 @@ fn dcr_builds_lib_package() {
|
||||
assert!(target_dir.join("lib").exists(), "lib directory missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_prebuilt_dependency_without_manifest_links() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping prebuilt path dependency test");
|
||||
return;
|
||||
};
|
||||
if !is_ar_in_path() {
|
||||
eprintln!("ar unavailable; skipping prebuilt path dependency test");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = unique_sandbox_dir("path_prebuilt_dep");
|
||||
let dep = root.join("dep");
|
||||
let app = root.join("app");
|
||||
write_prebuilt_library(
|
||||
&dep,
|
||||
compiler,
|
||||
"prebuilt",
|
||||
"int prebuilt_answer(void);\n",
|
||||
"int prebuilt_answer(void) { return 42; }\n",
|
||||
);
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nprebuilt = { path = \"../dep\", include = [\"include\"], lib = [\"lib\"], libs = [\"prebuilt\"] }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"prebuilt.h\"\nint main(void) { return prebuilt_answer() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"prebuilt path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dcr_dependency_is_built_before_consumer() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping path dependency build test");
|
||||
return;
|
||||
};
|
||||
let root = unique_sandbox_dir("path_dcr_dep");
|
||||
let dep = root.join("dep");
|
||||
let app = root.join("app");
|
||||
std::fs::create_dir_all(dep.join("src")).unwrap();
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
dep.join("dcr.toml"),
|
||||
"[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dep.join("src/mathlib.h"), "int answer(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
dep.join("src/mathlib.c"),
|
||||
"int answer(void) { return 42; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nrenamed = { path = \"../dep\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"mathlib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(dep.join("target/include/mathlib.h").is_file());
|
||||
assert!(dep.join("target/lib").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dcr_dependencies_build_transitively() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping transitive path dependency test");
|
||||
return;
|
||||
};
|
||||
let root = unique_sandbox_dir("path_dcr_transitive");
|
||||
let leaf = root.join("leaf");
|
||||
let middle = root.join("middle");
|
||||
let app = root.join("app");
|
||||
for project in [&leaf, &middle, &app] {
|
||||
std::fs::create_dir_all(project.join("src")).unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
leaf.join("dcr.toml"),
|
||||
"[package]\nname = \"leaf\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(leaf.join("src/leaf.h"), "int leaf_value(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
leaf.join("src/leaf.c"),
|
||||
"int leaf_value(void) { return 40; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
middle.join("dcr.toml"),
|
||||
"[package]\nname = \"middle\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\nleaf = { path = \"../leaf\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(middle.join("src/middle.h"), "int middle_value(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
middle.join("src/middle.c"),
|
||||
"#include \"leaf.h\"\nint middle_value(void) { return leaf_value() + 2; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmiddle = { path = \"../middle\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"middle.h\"\nint main(void) { return middle_value() == 42 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"transitive path dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(leaf.join("target/lib").is_dir());
|
||||
assert!(middle.join("target/lib").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dependency_cycle_reports_the_full_chain() {
|
||||
let root = unique_sandbox_dir("path_dcr_cycle");
|
||||
let first = root.join("first");
|
||||
let second = root.join("second");
|
||||
for project in [&first, &second] {
|
||||
std::fs::create_dir_all(project.join("src")).unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
first.join("dcr.toml"),
|
||||
"[package]\nname = \"first\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nsecond = { path = \"../second\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
second.join("dcr.toml"),
|
||||
"[package]\nname = \"second\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nfirst = { path = \"../first\" }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = run_dcr(&["build"], &first);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(!out.status.success());
|
||||
assert!(stderr.contains("Dependency cycle detected"), "{stderr}");
|
||||
assert!(
|
||||
stderr.contains("first") && stderr.contains("second"),
|
||||
"{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_dcr_dependency_is_cloned_built_and_locked() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping git dependency build test");
|
||||
return;
|
||||
};
|
||||
if !is_git_in_path() {
|
||||
eprintln!("git unavailable; skipping git dependency build test");
|
||||
return;
|
||||
}
|
||||
let root = unique_sandbox_dir("git_dcr_dep");
|
||||
let home = root.join("home");
|
||||
let origin = root.join("origin");
|
||||
let app = root.join("app");
|
||||
std::fs::create_dir_all(origin.join("src")).unwrap();
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
std::fs::write(
|
||||
origin.join("dcr.toml"),
|
||||
"[package]\nname = \"gitmath\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(origin.join("src/gitmath.h"), "int git_answer(void);\n").unwrap();
|
||||
std::fs::write(
|
||||
origin.join("src/gitmath.c"),
|
||||
"int git_answer(void) { return 7; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
for args in [
|
||||
vec!["init"],
|
||||
vec!["add", "."],
|
||||
vec![
|
||||
"-c",
|
||||
"user.email=dcr@example.test",
|
||||
"-c",
|
||||
"user.name=DCR",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
],
|
||||
vec!["tag", "v0.1.0"],
|
||||
] {
|
||||
let status = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(&origin)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
}
|
||||
// TOML basic strings treat backslashes as escapes. Git for Windows emits
|
||||
// paths with `\\`, so normalize the local repository path before embedding
|
||||
// it into dcr.toml (the slash form is accepted by Git on every platform).
|
||||
let origin_url = origin.to_string_lossy().replace('\\', "/");
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitmath = {{ git = \"{origin_url}\", tag = \"v0.1.0\" }}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"gitmath.h\"\nint main(void) { return git_answer() == 7 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
let home_s = home.to_string_lossy().to_string();
|
||||
let out = run_dcr_env(
|
||||
&["build"],
|
||||
&app,
|
||||
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
|
||||
);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"git dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
|
||||
assert!(lock.contains("git+"));
|
||||
assert!(!lock.contains("checksum = \"\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_prebuilt_dependency_without_manifest_links() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping prebuilt git dependency test");
|
||||
return;
|
||||
};
|
||||
if !is_git_in_path() || !is_ar_in_path() {
|
||||
eprintln!("git or ar unavailable; skipping prebuilt git dependency test");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = unique_sandbox_dir("git_prebuilt_dep");
|
||||
let home = root.join("home");
|
||||
let origin = root.join("origin");
|
||||
let app = root.join("app");
|
||||
write_prebuilt_library(
|
||||
&origin,
|
||||
compiler,
|
||||
"gitprebuilt",
|
||||
"int git_prebuilt_answer(void);\n",
|
||||
"int git_prebuilt_answer(void) { return 9; }\n",
|
||||
);
|
||||
std::fs::create_dir_all(app.join("src")).unwrap();
|
||||
for args in [
|
||||
vec!["init"],
|
||||
vec!["add", "."],
|
||||
vec![
|
||||
"-c",
|
||||
"user.email=dcr@example.test",
|
||||
"-c",
|
||||
"user.name=DCR",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
],
|
||||
vec!["tag", "v0.1.0"],
|
||||
] {
|
||||
let status = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(&origin)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
}
|
||||
// Keep the Git source TOML-safe on Windows (`C:/...`, not `C:\\...`).
|
||||
let origin_url = origin.to_string_lossy().replace('\\', "/");
|
||||
std::fs::write(
|
||||
app.join("dcr.toml"),
|
||||
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitprebuilt = {{ git = \"{origin_url}\", tag = \"v0.1.0\", include = [\"include\"], lib = [\"lib\"], libs = [\"gitprebuilt\"] }}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
app.join("src/main.c"),
|
||||
"#include \"gitprebuilt.h\"\nint main(void) { return git_prebuilt_answer() == 9 ? 0 : 1; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let home_s = home.to_string_lossy().to_string();
|
||||
let out = run_dcr_env(
|
||||
&["build"],
|
||||
&app,
|
||||
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
|
||||
);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"prebuilt git dependency build failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
|
||||
assert!(lock.contains("git+"));
|
||||
assert!(!lock.contains("checksum = \"\""));
|
||||
}
|
||||
|
||||
fn write_prebuilt_library(
|
||||
root: &std::path::Path,
|
||||
compiler: &str,
|
||||
name: &str,
|
||||
header: &str,
|
||||
source: &str,
|
||||
) {
|
||||
let include_dir = root.join("include");
|
||||
let lib_dir = root.join("lib");
|
||||
let src_dir = root.join("src");
|
||||
std::fs::create_dir_all(&include_dir).unwrap();
|
||||
std::fs::create_dir_all(&lib_dir).unwrap();
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
std::fs::write(include_dir.join(format!("{name}.h")), header).unwrap();
|
||||
let source_path = src_dir.join(format!("{name}.c"));
|
||||
let object_path = lib_dir.join(format!("{name}.o"));
|
||||
let archive_path = lib_dir.join(format!("lib{name}.a"));
|
||||
std::fs::write(&source_path, source).unwrap();
|
||||
let status = std::process::Command::new(compiler)
|
||||
.args(["-c"])
|
||||
.arg(&source_path)
|
||||
.args(["-o"])
|
||||
.arg(&object_path)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "failed to compile prebuilt library");
|
||||
let status = std::process::Command::new("ar")
|
||||
.args(["rcs"])
|
||||
.arg(&archive_path)
|
||||
.arg(&object_path)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "failed to archive prebuilt library");
|
||||
}
|
||||
|
||||
fn is_ar_in_path() -> bool {
|
||||
std::process::Command::new("ar")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|out| out.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_dependency_is_built_from_cache() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
|
||||
Reference in New Issue
Block a user