10 Commits

25 changed files with 1314 additions and 143 deletions

View File

@@ -1191,6 +1191,7 @@ jobs:
- name: Update Formula/dcr.rb - name: Update Formula/dcr.rb
run: | run: |
VERSION="${{ needs.setup.outputs.version }}" VERSION="${{ needs.setup.outputs.version }}"
cat > Formula/dcr.rb <<FORMULA cat > Formula/dcr.rb <<FORMULA
class Dcr < Formula class Dcr < Formula
desc "Cargo-like utility to manage C/C++ projects" desc "Cargo-like utility to manage C/C++ projects"
@@ -1199,37 +1200,36 @@ jobs:
on_macos do on_macos do
on_intel do on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-apple-darwin-#{version}" url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_x86 }}" sha256 "${{ steps.sha.outputs.macos_x86 }}"
end end
on_arm do on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-apple-darwin-#{version}" url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_arm }}" sha256 "${{ steps.sha.outputs.macos_arm }}"
end end
end end
on_linux do on_linux do
on_intel do on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-unknown-linux-gnu-#{version}" url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_x86 }}" sha256 "${{ steps.sha.outputs.linux_x86 }}"
end end
on_arm do on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-unknown-linux-gnu-#{version}" url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_arm }}" sha256 "${{ steps.sha.outputs.linux_arm }}"
end end
end end
def install
bin.install Dir["dcr*"].first => "dcr"
end
resource "man-pages" do resource "man-pages" do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-man-pages-#{version}.tar.gz" url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-man-pages-${VERSION}.tar.gz"
sha256 "${{ steps.sha.outputs.man_pages }}" sha256 "${{ steps.sha.outputs.man_pages }}"
end end
def install def install
bin.install Dir["dcr*"].first => "dcr" bin.install Dir["dcr*"].first => "dcr"
resource("man-pages").stage do resource("man-pages").stage do
man1.install Dir["usr/share/man/man1/*.1"] man1.install Dir["usr/share/man/man1/*.1"]
end end
@@ -1326,4 +1326,4 @@ jobs:
aws s3 sync dist/ "s3://${{ secrets.R2_BUCKET_NAME }}/${TAG}/" \ aws s3 sync dist/ "s3://${{ secrets.R2_BUCKET_NAME }}/${TAG}/" \
--endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \ --endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \
--region auto --region auto

3
.gitignore vendored
View File

@@ -8,4 +8,5 @@ sandbox/**
personal/ personal/
.* .*
!.gitignore !.gitignore
!.github/ !.github/
graphify-out/

View File

@@ -1,6 +1,36 @@
# Changelog # Changelog
## [Unreleased] ## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU
**Добавлено:**
- **Зависимости через `pkg-config`** — `fmt = { pkg-config = "fmt" }` или массив имён пакетов (`pkg-config = ["openssl", "libcrypto"]`) подключает системные библиотеки через `.pc`-метаданные.
- **Поддержка префиксов в `dcr add`** — команды `dcr add fmt pkg-config:fmt` и `dcr add openssl pkg:openssl,libcrypto` добавляют зависимости от системных пакетов в `dcr.toml`.
- **Полный набор флагов** — DCR собирает и пробрасывает compiler/linker flags (`-I`, `-L`, `-l`, `-pthread` и другие), включая транзитивные зависимости и интеграцию с `dcr gen` / `dcr test`.
- **Фиксация в `dcr.lock`** — записывается источник `pkg-config+...` и обнаруженная версия пакета.
- **Отображение в `dcr tree`** — `pkg-config` зависимости наглядно помечаются в дереве зависимостей.
**Исправлено:**
- **Ошибка создания проекта в папках со спецсимволами** — `dcr new` больше не завершается ошибкой `Failed to create dcr.toml`, если родительская директория содержит пробелы или не-ASCII символы.
### EN
**Added:**
- **`pkg-config` dependencies** — `fmt = { pkg-config = "fmt" }` or an array of package names (`pkg-config = ["openssl", "libcrypto"]`) connects system libraries through their `.pc` metadata.
- **Prefix support in `dcr add`** — `dcr add fmt pkg-config:fmt` and `dcr add openssl pkg:openssl,libcrypto` add system package dependencies to `dcr.toml`.
- **Complete flag propagation** — compiler/linker flags (`-I`, `-L`, `-l`, `-pthread`, and others) are preserved and passed to builds, `dcr test`, and IDE integrations via `dcr gen`.
- **Lockfile entries** — `dcr.lock` records the `pkg-config+...` source and detected package version.
- **`dcr tree` representation** — `pkg-config` dependencies are clearly labeled in the dependency tree.
**Fixed:**
- **Project creation failure in special character directories** — `dcr new` no longer fails with `Failed to create dcr.toml` when invoked inside parent folders with spaces or non-ASCII characters.
## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries"
### RU ### RU
@@ -8,6 +38,10 @@
- **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`. - **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`.
По умолчанию `auto`: **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN). По умолчанию `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 +53,10 @@
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`. - **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). 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:** **Removed:**

2
Cargo.lock generated
View File

@@ -154,7 +154,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.8.5" version = "0.9.1"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"fatfs", "fatfs",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.8.5" version = "0.9.1"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -88,4 +88,13 @@ opt-level = 1
lto = false lto = false
codegen-units = 16 codegen-units = 16
panic = "abort" panic = "abort"
strip = true strip = true
[profile.dev]
incremental = false
opt-level = 0
debug = "line-tables-only"
[profile.dev.package."*"]
opt-level = 1
debug = false

View File

@@ -39,7 +39,10 @@
```sh ```sh
yay -S dcr 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> </td>
</tr> </tr>

View File

@@ -4,10 +4,22 @@ sidebar_label: Changelog
# Changelog # Changelog
## Unreleased ## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.
- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`.
- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`).
- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation.
## 0.9.0 (2026-08-31)
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`. - **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). 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`. - **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
## 0.8.5 (2026-08-08) ## 0.8.5 (2026-08-08)

View File

@@ -15,6 +15,8 @@ dcr add mylib path:./lib # explicit path prefix
dcr add mylib git:https://github.com/user/repo # explicit git prefix dcr add mylib git:https://github.com/user/repo # explicit git prefix
dcr add mylib github:user/repo # GitHub shorthand dcr add mylib github:user/repo # GitHub shorthand
dcr add mylib gitlab:user/repo # GitLab shorthand dcr add mylib gitlab:user/repo # GitLab shorthand
dcr add fmt pkg-config:fmt # system package via pkg-config
dcr add openssl pkg:openssl,libcrypto # multiple pkg-config packages
``` ```
Source prefixes: Source prefixes:
@@ -22,6 +24,7 @@ Source prefixes:
- `git:` — generic git URL - `git:` — generic git URL
- `github:` — expands to `https://github.com/<user>/<repo>` - `github:` — expands to `https://github.com/<user>/<repo>`
- `gitlab:` — expands to `https://gitlab.com/<user>/<repo>` - `gitlab:` — expands to `https://gitlab.com/<user>/<repo>`
- `pkg-config:` / `pkg:` — one or more packages resolved by `pkg-config`
- `http://` / `https://` / `git@` — full URL - `http://` / `https://` / `git@` — full URL
Flags: Flags:
@@ -29,7 +32,12 @@ Flags:
- `--tag <name>` — git tag - `--tag <name>` — git tag
- `--rev <hash>` — git commit - `--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.
For `pkg-config` dependencies, the referenced packages must already be installed
and provide `.pc` files. DCR invokes `pkg-config` during the build (using
`PATH` and `PKG_CONFIG_PATH`) and forwards the returned compiler and linker
flags; it does not download or build these packages.
## `dcr tree` ## `dcr tree`

View File

@@ -111,7 +111,11 @@ Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) ar
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) | | `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
Settings from raw config (not in typed struct): Settings from raw config (not in typed struct):
- `pkg_config` — list of pkg-config packages - `pkg_config` — list of packages queried for build-wide compiler/linker flags
Dependencies can also declare a system package directly, for example
`fmt = { pkg-config = "fmt" }`. This records the package in `dcr.lock` and applies
the flags returned by `pkg-config --cflags` and `pkg-config --libs`.
- `ldscript` — linker script path - `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps - `build.steps` / `build.post_steps` — codegen steps

View File

@@ -6,7 +6,7 @@ sidebar_label: Dependencies
## Formats ## Formats
The `[dependencies]` section supports three formats: The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies:
### String (registry) ### String (registry)
@@ -23,10 +23,10 @@ The version string is used as-is for registry lookup.
```toml ```toml
[dependencies] [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) ### Table (path)
@@ -35,6 +35,32 @@ Fields: `git`, `branch`, `tag`, `rev`.
mylib = { path = "../mylib" } 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"] }
```
### Table (`pkg-config`)
Use an installed `.pc` package when the library is provided by the operating system
or another system package manager. DCR invokes `pkg-config --cflags` and
`pkg-config --libs`, preserving include, library, linker, and other flags:
```toml
[dependencies]
fmt = { pkg-config = "fmt" }
openssl = { pkg-config = ["openssl", "libcrypto"] }
```
The `pkg-config` executable must be available in `PATH`. The named `.pc` files must
be available through pkg-config's standard search paths or via `PKG_CONFIG_PATH`.
These dependencies are not downloaded or built by DCR; they are recorded in
`dcr.lock` with a `pkg-config+...` source.
## Registry ## Registry
DCR uses a package registry for dependency lookup by name. DCR uses a package registry for dependency lookup by name.
@@ -42,7 +68,7 @@ DCR uses a package registry for dependency lookup by name.
```toml ```toml
# ~/.dcr/config.toml # ~/.dcr/config.toml
[registry.default] [registry.default]
url = "https://index.dcr.pm" url = "https://index.dcr.dexoron.tech"
priority = 1 priority = 1
``` ```
@@ -50,7 +76,23 @@ Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrid
## Git dependencies ## 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 - `branch` — switch to a branch
- `tag` — switch to a tag - `tag` — switch to a tag
@@ -59,7 +101,7 @@ Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying
## Path dependencies ## 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 ```toml
[dependencies] [dependencies]
@@ -67,18 +109,51 @@ mylib = { path = "/abs/path/to/lib" }
mylib = { path = "../relative/path" } 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 ## dcr.lock
Dependency lock file. Contains package names and sources. 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 ## Resolution process
1. Load all dependencies (registry → git → path) 1. Resolve dependency sources (registry → git → path)
2. For path deps: recursively read their `dcr.toml` 2. Fetch git dependencies into `~/.dcr/git/` and check out the requested ref
3. For registry deps: search `index.json` 3. Build DCR package dependencies recursively
4. For git deps: clone to cache 4. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
5. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler

View File

@@ -25,6 +25,9 @@ Generic git repository.
.B path:./path/to/lib .B path:./path/to/lib
Local path dependency. Local path dependency.
.TP .TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages).
.TP
.B <version> .B <version>
Version from registry. Version from registry.
.SH OPTIONS .SH OPTIONS

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual" .TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual"
.SH NAME .SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS .SH SYNOPSIS
@@ -120,6 +120,9 @@ Generic git repository.
.B path:./path/to/lib .B path:./path/to/lib
Local path dependency. Local path dependency.
.TP .TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:name1,name2\fR).
.TP
.B \fI<version>\fR .B \fI<version>\fR
Version from registry (e.g. \fB1.2.3\fR). Version from registry (e.g. \fB1.2.3\fR).
.SH GEN SUBCOMMANDS .SH GEN SUBCOMMANDS

View File

@@ -25,7 +25,7 @@ use toml::map::Map;
/// Parsed arguments for the `dcr add` command. /// Parsed arguments for the `dcr add` command.
/// ///
/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set, /// Exactly one of `path`, `git`, `pkg_config`, or `version_from_registry` is expected to be set,
/// depending on how the dependency source was specified. /// depending on how the dependency source was specified.
pub struct AddArgs { pub struct AddArgs {
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`. /// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
@@ -42,6 +42,8 @@ pub struct AddArgs {
pub rev: Option<String>, pub rev: Option<String>,
/// Version string resolved from the package registry when no explicit source is given. /// Version string resolved from the package registry when no explicit source is given.
pub version_from_registry: Option<String>, 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`. /// Adds a dependency to the current project's `dcr.toml`.
@@ -71,6 +73,7 @@ pub fn add(args: &[String]) -> i32 {
println!(" gitlab:user/repo GitLab repository"); println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository"); println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency"); println!(" path:./path/to/lib Local path dependency");
println!(" pkg-config:fmt System pkg-config dependency");
println!(" <version> Version from registry"); println!(" <version> Version from registry");
println!(); println!();
printc("OPTIONS:", BOLD_GREEN); printc("OPTIONS:", BOLD_GREEN);
@@ -133,6 +136,17 @@ pub fn add(args: &[String]) -> i32 {
} }
Value::Table(table) 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 { } else if let Some(path) = add_args.path {
let mut table = Map::new(); let mut table = Map::new();
table.insert("path".to_string(), Value::String(path)); table.insert("path".to_string(), Value::String(path));
@@ -140,7 +154,7 @@ pub fn add(args: &[String]) -> i32 {
} else if let Some(version) = add_args.version_from_registry { } else if let Some(version) = add_args.version_from_registry {
Value::String(version) Value::String(version)
} else { } else {
error("Dependency source (path or git) must be provided"); error("Dependency source (path, git, or pkg-config) must be provided");
return 1; return 1;
}; };
@@ -171,6 +185,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
error(" gitlab:user/repo -> gitlab.com/user/repo"); error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo"); error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path"); error(" path:./path/to/lib -> local path");
error(" pkg-config:fmt -> system pkg-config package");
return Err(1); return Err(1);
} }
@@ -221,6 +236,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag: None, tag: None,
rev: None, rev: None,
version_from_registry: Some(version.to_string()), version_from_registry: Some(version.to_string()),
pkg_config: None,
}); });
} }
Err(e) => { Err(e) => {
@@ -262,9 +278,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut path = None; let mut path = None;
let mut git = None; let mut git = None;
let mut pkg_config = None;
// Map source prefixes / bare URLs to path or git fields // Map source prefixes / bare URLs to path or git fields
if let Some(p) = source_spec.strip_prefix("path:") { 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()); path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") { } else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g)); git = Some(format!("https://github.com/{}", g));
@@ -288,8 +320,15 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|| source_spec.starts_with("git@") || source_spec.starts_with("git@")
{ {
git = Some(source_spec); 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 { } else {
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL"); error(
"Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
);
return Err(1); return Err(1);
} }
@@ -301,5 +340,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag, tag,
rev, rev,
version_from_registry: None, version_from_registry: None,
pkg_config,
}) })
} }

View File

@@ -42,11 +42,13 @@ struct GenDeps {
include_dirs: Vec<String>, include_dirs: Vec<String>,
lib_dirs: Vec<String>, lib_dirs: Vec<String>,
libs: Vec<String>, libs: Vec<String>,
cflags: Vec<String>,
ldflags: Vec<String>,
} }
/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation. /// Collects dependency metadata from `dcr.toml` for code generation.
/// ///
/// Skips `system` deps and non-path entries. Falls back to common include/lib /// Resolves path layouts and pkg-config flags while skipping system deps. Falls back to common include/lib
/// directory names only when those directories exist. /// directory names only when those directories exist.
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps { fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
let deps_val = match config.get("dependencies") { let deps_val = match config.get("dependencies") {
@@ -56,6 +58,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![], include_dirs: vec![],
lib_dirs: vec![], lib_dirs: vec![],
libs: vec![], libs: vec![],
cflags: vec![],
ldflags: vec![],
}; };
} }
}; };
@@ -66,6 +70,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![], include_dirs: vec![],
lib_dirs: vec![], lib_dirs: vec![],
libs: vec![], libs: vec![],
cflags: vec![],
ldflags: vec![],
}; };
} }
}; };
@@ -73,12 +79,37 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
let mut include_dirs = Vec::new(); let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new(); let mut lib_dirs = Vec::new();
let mut libs = Vec::new(); let mut libs = Vec::new();
let mut cflags = Vec::new();
let mut ldflags = Vec::new();
for (name, value) in deps_table { for (name, value) in deps_table {
let tbl = match value.as_table() { let tbl = match value.as_table() {
Some(t) => t, Some(t) => t,
None => continue, None => continue,
}; };
let pkg_names = tbl
.get("pkg-config")
.or_else(|| tbl.get("pkg_config"))
.map(|raw| {
raw.as_str()
.map(|s| vec![s.to_string()])
.or_else(|| {
raw.as_array().map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
})
.unwrap_or_default()
})
.unwrap_or_default();
if !pkg_names.is_empty() {
let (flags, link_flags) =
crate::utils::build::resolve_pkg_config_flags_lossy(&pkg_names, &[], &[]);
cflags.extend(flags);
ldflags.extend(link_flags);
continue;
}
// skip system deps // skip system deps
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) { if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
continue; continue;
@@ -169,6 +200,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs, include_dirs,
lib_dirs, lib_dirs,
libs, libs,
cflags,
ldflags,
} }
} }
@@ -350,8 +383,10 @@ fn collect_project_info_inner(
}); });
let resolved = resolve_deps_for_gen(&config, profile, &root); let resolved = resolve_deps_for_gen(&config, profile, &root);
let (resolved_cflags, resolved_ldflags) = let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags); resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
// Build exclude/include pattern lists (same logic as cli::build) // Build exclude/include pattern lists (same logic as cli::build)
let mut combined_excludes: Vec<PathBuf> = Vec::new(); let mut combined_excludes: Vec<PathBuf> = Vec::new();

View File

@@ -23,7 +23,6 @@ use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
/// Initializes the current working directory as a new DCR project. /// Initializes the current working directory as a new DCR project.
/// ///
@@ -91,18 +90,8 @@ pub fn init(args: &[String]) -> i32 {
println!("Initializing the project in {cwd}"); println!("Initializing the project in {cwd}");
// Create and initialize default manifest (dcr.toml). // Create and initialize default manifest (dcr.toml).
let mut config = match Config::new("./dcr.toml") { if Config::create("./dcr.toml", Some(&project_name)).is_err() {
Ok(cfg) => cfg, error("Failed to create dcr.toml");
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.clone()))
.is_err()
{
error("Failed to write dcr.toml");
return 1; return 1;
} }
println!( println!(

View File

@@ -23,7 +23,6 @@ use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure. /// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
/// ///
@@ -110,18 +109,8 @@ pub fn new(args: &[String]) -> i32 {
// Initialize and write project configuration manifest (dcr.toml). // Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml"); let toml_path = format!("./{project_name}/dcr.toml");
let mut config = match Config::new(&toml_path) { if Config::create(&toml_path, Some(project_name)).is_err() {
Ok(cfg) => cfg, error("Failed to create dcr.toml");
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.to_string()))
.is_err()
{
error("Failed to write dcr.toml");
return 1; return 1;
} }
println!( println!(

View File

@@ -134,8 +134,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
cflags.extend(get_list_with_profile(&config, "cflags", profile)); cflags.extend(get_list_with_profile(&config, "cflags", profile));
let ldflags = get_list_with_profile(&config, "ldflags", profile); let ldflags = get_list_with_profile(&config, "ldflags", profile);
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile); let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?; let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let includes = test_include_dirs(&config, profile); let deps = crate::core::deps::resolve_deps(&config, profile, None, Path::new("."))?;
cflags.extend(deps.cflags);
ldflags.extend(deps.ldflags);
let mut includes = test_include_dirs(&config, profile);
includes.extend(deps.include_dirs);
let test_sources = collect_test_sources()?; let test_sources = collect_test_sources()?;
if test_sources.is_empty() { if test_sources.is_empty() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string()); return Err("tests/test.c not found; run 'dcr test --init' first".to_string());

View File

@@ -115,6 +115,19 @@ fn print_deps(
format!(" v{}", v) format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) { } else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git) format!(" ({})", git)
} else if let Some(pkg) = t.get("pkg-config").or_else(|| t.get("pkg_config")) {
if let Some(s) = pkg.as_str() {
format!(" (pkg-config: {})", s)
} else if let Some(arr) = pkg.as_array() {
let names = arr
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(", ");
format!(" (pkg-config: {})", names)
} else {
"".to_string()
}
} else { } else {
"".to_string() "".to_string()
} }

View File

@@ -35,7 +35,7 @@ use crate::core::build::steps::{
get_build_steps_with_profile, run_build_steps, verify_expectations, get_build_steps_with_profile, run_build_steps, verify_expectations,
}; };
use crate::core::build_config::Config; 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::core::workspace::parse_workspace;
use crate::utils::build::{ use crate::utils::build::{
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, 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>, workspace_root: Option<&Path>,
cancel: &Arc<AtomicBool>, cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter, 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> { ) -> Result<(), String> {
if cancel.load(Ordering::SeqCst) { if cancel.load(Ordering::SeqCst) {
return Err("Build interrupted".to_string()); return Err("Build interrupted".to_string());
@@ -641,66 +716,63 @@ 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)?; let mut resolved = resolve_deps(&config, profile, build_target, project_root)?;
resolved.include_dirs.extend(ws_include_dirs); resolved.include_dirs.extend(ws_include_dirs);
resolved.lib_dirs.extend(ws_lib_dirs); resolved.lib_dirs.extend(ws_lib_dirs);
resolved.libs.extend(ws_libs); resolved.libs.extend(ws_libs);
// Registry dependencies are cached under the DCR registry root. Build let mut built_roots = std::collections::HashSet::new();
// them as normal DCR projects before the current project is linked. let mut transitive_roots = std::collections::HashSet::new();
if let Some(deps) = deps_table { let direct_package_roots = resolved.package_roots.clone();
for (name, value) in deps { for dep_root in &direct_package_roots {
if register::is_registry_dep(value) { if !built_roots.insert(dep_root.clone()) {
let pkg_info = register::resolve_package_from_registry(name)?; continue;
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()
));
}
build_project_at(
&dep_root,
profile,
build_target,
has_explicit_target,
&[],
force,
verbose,
None,
cancel,
&mut *rep,
)?;
rep.on_event(BuildEvent::DepReady {
name,
version,
rebuilt: true,
});
} else {
rep.on_event(BuildEvent::DepReady {
name,
version,
rebuilt: false,
});
}
}
} }
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,
&[],
force,
verbose,
None,
cancel,
&mut *rep,
dependency_stack,
)?;
rep.on_event(BuildEvent::DepReady {
name: &name,
version: &version,
rebuilt: true,
});
extend_transitive_link_deps(
dep_root,
profile,
build_target,
&mut resolved,
&mut transitive_roots,
)?;
} }
let (resolved_cflags, resolved_ldflags) = let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_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(); let mut combined_excludes = Vec::new();
for dir in exclude_dirs { for dir in exclude_dirs {
combined_excludes.push(dir.clone()); combined_excludes.push(dir.clone());
@@ -919,6 +991,42 @@ fn build_project_at(
Ok(()) 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);
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),
profile,
target,
consumer_deps,
visited,
)?;
}
Ok(())
}
/// Converts relative paths in flags to absolute paths based on project root for include and link flags. /// 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 { fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
for prefix in prefixes { for prefix in prefixes {

View File

@@ -244,13 +244,19 @@ pub struct WorkspaceMemberConfig {
} }
impl Config { 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. /// 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> { 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 path = PathBuf::from(path);
let (data, doc) = if path.exists() { let (data, doc) = if path.exists() {
load_parts(&path)? load_parts(&path)?
} else { } else {
let content = default_toml_text(); let content = default_toml_text(package_name)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?; let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
fs::write(&path, doc.to_string())?; fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?; let data: Value = toml::from_str(&content)?;
@@ -830,14 +836,20 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
Ok((data, doc)) Ok((data, doc))
} }
fn default_toml_text() -> String { fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
let name = std::env::current_dir() let name = package_name
.ok() .map(|s| s.to_string())
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().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()); .unwrap_or_else(|| "project".to_string());
validate_non_empty_string(&name, "package.name")?;
validate_package_name(&name)?;
let dcr_version = env!("CARGO_PKG_VERSION"); let dcr_version = env!("CARGO_PKG_VERSION");
format!( Ok(format!(
"[package]\n\ "[package]\n\
name = \"{name}\"\n\ name = \"{name}\"\n\
version = \"{DEFAULT_VERSION}\"\n\ version = \"{DEFAULT_VERSION}\"\n\
@@ -854,7 +866,7 @@ fn default_toml_text() -> String {
kind = \"{DEFAULT_KIND}\"\n\ kind = \"{DEFAULT_KIND}\"\n\
\n\ \n\
[dependencies]\n" [dependencies]\n"
) ))
} }
fn dcr_version_warn_once(path_key: &str) -> bool { fn dcr_version_warn_once(path_key: &str) -> bool {
@@ -1285,6 +1297,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] #[test]
fn new_creates_default_config() { fn new_creates_default_config() {
let dir = temp_dir("new_default"); let dir = temp_dir("new_default");

View File

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

View File

@@ -23,6 +23,7 @@ pub mod register;
use crate::core::build_config::Config; use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps; use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock}; use crate::core::deps::lock::{DepLock, write_lock};
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
use std::path::Path; use std::path::Path;
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing. /// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
@@ -39,8 +40,8 @@ fn dep_version(path: &Path) -> String {
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file. /// 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 /// Registry, path, and git deps contribute to `ResolvedDeps`. Path and git sources support both
/// (no fetch or include/lib resolution). /// DCR packages (built from `dcr.toml`) and prebuilt libraries (described with include/lib/libs).
/// ///
/// # Parameters /// # Parameters
/// - `config`: Loaded `dcr.toml`. /// - `config`: Loaded `dcr.toml`.
@@ -73,7 +74,22 @@ pub fn resolve_deps(
if let Some(deps) = deps_table { if let Some(deps) = deps_table {
// Process each dependency declaration in the TOML table // Process each dependency declaration in the TOML table
for (name, value) in deps { 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 pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?; let dep_root = register::package_root_from_registry_info(&pkg_info)?;
@@ -88,6 +104,9 @@ pub fn resolve_deps(
.to_string(), .to_string(),
); );
resolved.libs.push(name.clone()); resolved.libs.push(name.clone());
resolved
.package_roots
.push(dep_root.to_string_lossy().to_string());
lock_packages.push(DepLock { lock_packages.push(DepLock {
name: name.clone(), name: name.clone(),
@@ -107,6 +126,8 @@ pub fn resolve_deps(
}); });
} else if let Some(path) = path_dep_path(value) { } else if let Some(path) = path_dep_path(value) {
let dep_root = project_root.join(path); 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(table) = value.as_table() {
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) { if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes { for inc in includes {
@@ -118,10 +139,7 @@ pub fn resolve_deps(
} }
} else { } else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include")); push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists( push_package_output_dirs(&mut resolved, &dep_root);
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
} }
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) { if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
@@ -143,16 +161,29 @@ pub fn resolve_deps(
} }
} }
} else { } else {
resolved.libs.push(name.clone()); resolved
.libs
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
} }
} else { } else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include")); push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists( push_package_output_dirs(&mut resolved, &dep_root);
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
push_default_lib_dirs(&mut resolved.lib_dirs, &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 { lock_packages.push(DepLock {
@@ -162,11 +193,46 @@ pub fn resolve_deps(
source: format!("path+{}", dep_root.display()), source: format!("path+{}", dep_root.display()),
}); });
} else if let Some(git_info) = git_dep(value) { } 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 { lock_packages.push(DepLock {
name: name.clone(), name: name.clone(),
version: git_info.version.unwrap_or_default(), version: git_info.version.unwrap_or(commit.clone()),
checksum: String::new(), checksum: commit.clone(),
source: format!("git+{}", git_info.url), source: format!("git+{}#{}", git_info.url, commit),
}); });
} }
} }
@@ -187,6 +253,9 @@ pub fn resolve_deps(
struct GitDep<'a> { struct GitDep<'a> {
url: &'a str, url: &'a str,
version: Option<String>, version: Option<String>,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
} }
/// Parses a git dependency from a TOML value table. /// Parses a git dependency from a TOML value table.
@@ -198,11 +267,71 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
.get("version") .get("version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.to_string()); .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 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. /// 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> { fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() { if let Some(table) = value.as_table() {
@@ -226,6 +355,117 @@ fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
push_if_exists(paths, &dep_root.join("target").join("lib")); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -254,6 +494,20 @@ mod tests {
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None); 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. /// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
#[test] #[test]
fn default_lib_dirs_include_packaged_library_output() { fn default_lib_dirs_include_packaged_library_output() {

View File

@@ -17,8 +17,10 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use toml::Value as TomlValue; use toml::Value as TomlValue;
/// Represents a registry configuration entry with its URL and priority. /// 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") 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`). /// 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 /// Configured registries are sorted by priority, but the lookup currently always reads the

View File

@@ -16,6 +16,15 @@ fn dcr_add_dependencies() {
"path dep not found in toml" "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 // Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir); let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed"); assert!(out.status.success(), "dcr add github should succeed");
@@ -43,6 +52,24 @@ fn dcr_add_dependencies() {
"git short dep not found in toml" "git short dep not found in toml"
); );
// Test pkg-config prefix.
let out = run_dcr(&["add", "fmt", "pkg-config:fmt"], &dir);
assert!(out.status.success(), "dcr add pkg-config should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("fmt = { pkg-config = \"fmt\" }"),
"pkg-config dep not found in toml"
);
// Test multiple pkg-config packages via pkg: prefix.
let out = run_dcr(&["add", "openssl", "pkg:openssl,libcrypto"], &dir);
assert!(out.status.success(), "dcr add multiple pkg should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("openssl = { pkg-config = [\"openssl\", \"libcrypto\"] }"),
"multi pkg-config dep not found in toml"
);
// Test flags (branch) // Test flags (branch)
let out = run_dcr( let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"], &["add", "branch_lib", "github:user/repo", "--branch", "dev"],
@@ -96,6 +123,419 @@ fn dcr_builds_lib_package() {
assert!(target_dir.join("lib").exists(), "lib directory missing"); 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 pkg_config_dependency_resolves_system_library() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping pkg-config dependency test");
return;
};
if !std::process::Command::new("pkg-config")
.args(["--exists", "zlib"])
.status()
.map(|status| status.success())
.unwrap_or(false)
{
eprintln!("pkg-config zlib unavailable; skipping pkg-config dependency test");
return;
}
let root = unique_sandbox_dir("pkg_config_dep");
let app = root.join("app");
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]\nzlib = { pkg-config = \"zlib\" }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include <zlib.h>\nint main(void) { return zlibVersion() == 0 ? 1 : 0; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"pkg-config dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
assert!(lock.contains("source = \"pkg-config+zlib\""));
// Check dcr tree displays pkg-config info
let tree_out = run_dcr(&["tree"], &app);
assert!(tree_out.status.success(), "dcr tree should succeed");
let tree_str = String::from_utf8_lossy(&tree_out.stdout);
assert!(
tree_str.contains("zlib (pkg-config: zlib)"),
"dcr tree should format pkg-config dependency, got: {}",
tree_str
);
}
#[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] #[test]
fn registry_dependency_is_built_from_cache() { fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else { let Some(compiler) = available_compiler() else {

View File

@@ -79,3 +79,24 @@ fn init_vcs_options_work() {
assert!(gitignore.contains(".dcr/")); assert!(gitignore.contains(".dcr/"));
} }
} }
#[test]
fn new_in_non_ascii_parent_directory() {
let parent_dir = unique_sandbox_dir("Проекты с пробелами");
let out = run_dcr(&["new", "my_app"], &parent_dir);
assert!(
out.status.success(),
"dcr new should succeed inside non-ascii parent dir: {}",
String::from_utf8_lossy(&out.stderr)
);
let project_dir = parent_dir.join("my_app");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
let toml_content = std::fs::read_to_string(project_dir.join("dcr.toml")).unwrap();
assert!(
toml_content.contains("name = \"my_app\""),
"package name should be my_app in dcr.toml"
);
}