feat: update version to 0.6.9 and add packaging for Linux and BSD

- Bump version from 0.6.8 to 0.6.9 in Cargo.toml and Cargo.lock.
- Enhance GitHub Actions workflow to build and publish .deb, .rpm, and Snap packages.
- Implement AUR publishing for both dcr and dcr-bin packages.
- Add support for workspace-only projects in the build system.
- Introduce new `workspace` flag for build and run commands.
- Create snapcraft.yaml for Snap package configuration.
- Update build.rs and related files to handle workspace members and inheritance.
- Modify config handling to support optional package and build configurations.
This commit is contained in:
2026-05-27 23:14:49 +08:00
parent e3c35793ec
commit 8c9dd39503
11 changed files with 760 additions and 207 deletions

View File

@@ -1,138 +0,0 @@
name: AUR Publish
on:
workflow_dispatch:
inputs:
version:
description: "Tag version without v prefix (e.g. 0.2.2)"
required: true
push:
tags:
- "v*"
permissions:
contents: read
jobs:
publish-aur:
# Не публикуем dev/pre-release теги в AUR
if: "!contains(github.ref_name, '-')"
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive version and checksum
id: meta
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
PKGVER="${{ inputs.version }}"
else
PKGVER="${GITHUB_REF_NAME#v}"
fi
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/v${PKGVER}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
echo "pkgver=$PKGVER" >> "$GITHUB_OUTPUT"
echo "source_url=$SRC_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR metadata
shell: bash
run: |
set -euo pipefail
PKGVER='${{ steps.meta.outputs.pkgver }}'
SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
cat > packaging/aur/dcr/PKGBUILD <<PKG
pkgname=dcr
pkgver=${PKGVER}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('MIT')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/v\$pkgver.tar.gz")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-\$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-\$pkgver"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
}
PKG
cat > packaging/aur/dcr/.SRCINFO <<SRC
pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = ${PKGVER}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = MIT
makedepends = cargo
depends = gcc-libs
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-${PKGVER}.tar.gz::${SRC_URL}
sha256sums = ${SHA256}
pkgname = dcr
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr
shell: bash
run: |
set -euo pipefail
if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then
echo "AUR_SSH_PRIVATE_KEY secret is missing"
exit 1
fi
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packaging/aur/dcr/PKGBUILD aur-repo/PKGBUILD
cp packaging/aur/dcr/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ steps.meta.outputs.pkgver }}"
git push

View File

@@ -148,6 +148,82 @@ jobs:
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Linux Packages (.deb & .rpm) ───────────────────────────────────────────
build-deb-rpm:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Tools
run: |
cargo install cargo-deb
cargo install cargo-generate-rpm
- name: Build Native Release
run: cargo build --release
- name: Generate .deb Package
run: cargo deb --no-build
- name: Generate .rpm Package
run: cargo generate-rpm
- name: Upload Packages to Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: |
target/debian/*.deb
target/generate-rpm/*.rpm
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Snapcraft ──────────────────────────────────────────────────────────────
build-snap:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build Release Binary
run: |
cargo build --release
# Создаем папки для контекста snapcraft, если внутри yaml завязан относительный путь
mkdir -p dist
cp target/release/dcr dist/
- name: Build Snap Package
uses: snapcore/action-build@v1
id: snapcraft
with:
path: packages/snap
- name: Upload Snap to Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: packages/snap/*.snap
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── macOS ────────────────────────────────────────────────────────────────── # ── macOS ──────────────────────────────────────────────────────────────────
build-macos: build-macos:
if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }}
@@ -194,7 +270,7 @@ jobs:
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── BSD ──────────────────────────────────────────────────────────────────── # ── BSD Systems ────────────────────────────────────────────────────────────
build-bsd: build-bsd:
if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }}
needs: [setup, notes] needs: [setup, notes]
@@ -210,6 +286,7 @@ jobs:
name: release-notes name: release-notes
path: .github path: .github
# ── FreeBSD ──
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
run: | run: |
pip install cargo-zigbuild pip install cargo-zigbuild
@@ -229,7 +306,8 @@ jobs:
cp target/x86_64-unknown-freebsd/release/dcr "$ASSET" cp target/x86_64-unknown-freebsd/release/dcr "$ASSET"
echo "FREEBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" echo "FREEBSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
- name: Build in OpenBSD VM # ── OpenBSD ──
- name: Build in OpenBSD VM
uses: vmactions/openbsd-vm@v1 uses: vmactions/openbsd-vm@v1
with: with:
usesh: true usesh: true
@@ -243,6 +321,7 @@ jobs:
cp target/release/dcr "$ASSET" cp target/release/dcr "$ASSET"
echo "OPENBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" echo "OPENBSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# ── NetBSD ──
- name: Build in NetBSD VM - name: Build in NetBSD VM
uses: vmactions/netbsd-vm@v1 uses: vmactions/netbsd-vm@v1
with: with:
@@ -257,6 +336,7 @@ jobs:
cp target/release/dcr "$ASSET" cp target/release/dcr "$ASSET"
echo "NETBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" echo "NETBSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# ── Assets Upload ──
- name: Upload Release Assets - name: Upload Release Assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
@@ -314,6 +394,218 @@ jobs:
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Arch User Repository (dcr) ───────────────────────────────────
publish-aur:
if: "!contains(github.ref_name, '-')"
needs: [setup]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${{ needs.setup.outputs.tag }}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
echo "source_url=$SRC_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR metadata
shell: bash
run: |
set -euo pipefail
PKGVER='${{ needs.setup.outputs.version }}'
SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr
cat > packages/aur/dcr/PKGBUILD <<PKG
pkgname=dcr
pkgver=${PKGVER}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/v\$pkgver.tar.gz")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-\$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-\$pkgver"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
}
PKG
cat > packages/aur/dcr/.SRCINFO <<SRC
pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = ${PKGVER}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = GPL-3.0-or-later
makedepends = cargo
depends = gcc-libs
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-${PKGVER}.tar.gz::${SRC_URL}
sha256sums = ${SHA256}
pkgname = dcr
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr
shell: bash
run: |
set -euo pipefail
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packages/aur/dcr/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push
# ── Arch User Repository (dcr-bin) ────────────────────────────────
publish-aur-bin:
if: "!contains(github.ref_name, '-')"
# Ждем сборку линукса, чтобы бинарник гарантированно лежал в релизах
needs: [setup, build-linux]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
BIN_URL="https://github.com/dexoron/dcr/releases/download/${{ needs.setup.outputs.tag }}/dcr-x86_64-unknown-linux-musl-${{ needs.setup.outputs.version }}"
SHA256="$(curl -fsSL "$BIN_URL" | sha256sum | awk '{print $1}')"
echo "bin_url=$BIN_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR Metadata
shell: bash
run: |
set -euo pipefail
PKGVER='${{ needs.setup.outputs.version }}'
BIN_URL='${{ steps.meta.outputs.bin_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr-bin
cat > packages/aur/dcr-bin/PKGBUILD <<PKG
pkgname=dcr-bin
pkgver=${PKGVER}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)"
arch=('x86_64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
provides=('dcr')
conflicts=('dcr')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source_x86_64=("dcr-\$pkgver::${BIN_URL}")
sha256sums_x86_64=('${SHA256}')
package() {
install -Dm755 "\$srcdir/dcr-\$pkgver" "\$pkgdir/usr/bin/dcr"
}
PKG
cat > packages/aur/dcr-bin/.SRCINFO <<SRC
pkgbase = dcr-bin
pkgdesc = Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)
pkgver = ${PKGVER}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
license = GPL-3.0-or-later
provides = dcr
conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
source_x86_64 = dcr-${PKGVER}::${BIN_URL}
sha256sums_x86_64 = ${SHA256}
pkgname = dcr-bin
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr-bin
shell: bash
run: |
set -euo pipefail
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packages/aur/dcr-bin/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr-bin/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr-bin ${{ needs.setup.outputs.version }}"
git push
# ── crates.io ────────────────────────────────────────────────────────────── # ── crates.io ──────────────────────────────────────────────────────────────
publish-crates: publish-crates:
if: | if: |
@@ -321,6 +613,9 @@ jobs:
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true')
needs: [setup, build-linux, build-macos, build-windows, build-bsd] needs: [setup, build-linux, build-macos, build-windows, build-bsd]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -329,7 +624,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io - name: Publish to crates.io
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish
# ── Homebrew tap ─────────────────────────────────────────────────────────── # ── Homebrew tap ───────────────────────────────────────────────────────────
publish-brew: publish-brew:

2
Cargo.lock generated
View File

@@ -112,7 +112,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"git2", "git2",

View File

@@ -1,12 +1,46 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.6.8" version = "0.6.9"
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."
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/dexoron/dcr" repository = "https://github.com/dexoron/dcr"
[package.metadata.binstall]
pkg-fmt = "bin"
pkg-url = "{ repo }/releases/download/v{ version }/dcr-{ target }-{ version }{ bin-suffix }"
[package.metadata.deb]
maintainer = "Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>"
copyright = "2026, Dexoron"
license-file = ["LICENSE"]
extended-description = """\
Cargo-like utility to manage C/C++ projects.\
"""
section = "devel"
priority = "optional"
assets = [
["target/release/dcr", "usr/bin/dcr", "755"],
# man-page:
# ["assets/dcr.1", "usr/share/man/man1/dcr.1", "644"],
]
[package.metadata.rpm]
maintainer = "Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>"
copyright = "2026, Dexoron"
license-file = ["LICENSE"]
extended-description = """\
Cargo-like utility to manage C/C++ projects.\
"""
section = "devel"
priority = "optional"
assets = [
["target/release/dcr", "usr/bin/dcr", "755"],
# man-page:
# ["assets/dcr.1", "usr/share/man/man1/dcr.1", "644"],
]
[dependencies] [dependencies]
glob = "0.3" glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }

1
packaging/aur/dcr-git Submodule

Submodule packaging/aur/dcr-git added at 12038a613e

View File

@@ -0,0 +1,18 @@
name: dcr
base: core22
version: '0.6.9'
summary: Cargo-like utility to manage C/C++ projects
description: |
A build tool and package manager for C/C++ projects inspired by Rust's Cargo.
grade: stable
confinement: classic
apps:
dcr:
command: bin/dcr
parts:
dcr:
plugin: dump
source: ../../dist/

View File

@@ -69,7 +69,9 @@ pub fn build(args: &[String]) -> i32 {
if flags.target.is_none() { if flags.target.is_none() {
let config_path = root.join("dcr.toml"); let config_path = root.join("dcr.toml");
if let Ok(config) = Config::open(config_path.to_str().unwrap()) { if let Ok(config) = Config::open(config_path.to_str().unwrap())
&& !config.is_workspace_only()
{
let bt = get_build_string_with_profile(&config, "target", "debug"); let bt = get_build_string_with_profile(&config, "target", "debug");
if !bt.is_empty() { if !bt.is_empty() {
flags.target = Some(bt); flags.target = Some(bt);
@@ -100,6 +102,7 @@ pub fn build(args: &[String]) -> i32 {
flags.target.as_deref(), flags.target.as_deref(),
flags.force, flags.force,
flags.verbose, flags.verbose,
flags.workspace.as_deref(),
) )
}) { }) {
Ok(()) => 0, Ok(()) => 0,
@@ -372,8 +375,13 @@ fn get_build_steps_with_profile(
get_build_steps(config, &format!("build.{field}")) get_build_steps(config, &format!("build.{field}"))
} }
fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<String>) { fn ensure_target_dirs(
if !items.contains(&"target".to_string()) { items: &[String],
profile: &str,
target_dir: Option<String>,
skip_local_target: bool,
) {
if !skip_local_target && !items.contains(&"target".to_string()) {
let _ = fs::create_dir("./target"); let _ = fs::create_dir("./target");
} }
if let Some(dir) = &target_dir { if let Some(dir) = &target_dir {
@@ -414,8 +422,38 @@ fn build_from_root(
target: Option<&str>, target: Option<&str>,
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace: Option<&str>,
) -> Result<(), String> { ) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
if config.is_workspace_only() {
let ws = parse_workspace(&config, profile, target, root)?
.ok_or_else(|| "Workspace root has no members defined".to_string())?;
let members: Vec<_> = if let Some(filter_name) = workspace {
ws.members
.into_iter()
.filter(|m| m.name == filter_name)
.collect()
} else {
ws.members
};
if members.is_empty() {
return Err("No matching workspace members to build".to_string());
}
for member in &members {
build_project_at(
&member.path,
profile,
target,
&[],
force,
verbose,
Some(root),
)?;
}
return Ok(());
}
let project_name = get_config_str(&config, "package.name"); let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version"); let project_version = get_config_str(&config, "package.version");
@@ -454,20 +492,50 @@ fn build_from_root(
) )
); );
} }
if let Some(workspace) = parse_workspace(&config, profile, build_target.as_deref(), root)? { if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? {
build_workspace(&workspace, profile, build_target.as_deref(), force, verbose)?; if let Some(filter_name) = workspace {
let excludes: Vec<std::path::PathBuf> = for member in ws.members.iter().filter(|m| m.name == filter_name) {
workspace.members.iter().map(|m| m.path.clone()).collect(); build_project_at(
&member.path,
profile,
build_target.as_deref(),
&[],
force,
verbose,
Some(root),
)?;
}
} else {
build_workspace(
&ws,
profile,
build_target.as_deref(),
force,
verbose,
Some(root),
)?;
let excludes: Vec<std::path::PathBuf> =
ws.members.iter().map(|m| m.path.clone()).collect();
build_project_at(
root,
profile,
build_target.as_deref(),
&excludes,
force,
verbose,
None,
)?;
}
} else {
build_project_at( build_project_at(
root, root,
profile, profile,
build_target.as_deref(), build_target.as_deref(),
&excludes, &[],
force, force,
verbose, verbose,
None,
)?; )?;
} else {
build_project_at(root, profile, build_target.as_deref(), &[], force, verbose)?;
} }
} }
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -485,9 +553,18 @@ fn build_workspace(
target: Option<&str>, target: Option<&str>,
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace_root: Option<&Path>,
) -> Result<(), String> { ) -> Result<(), String> {
for member in &workspace.members { for member in &workspace.members {
build_project_at(&member.path, profile, target, &[], force, verbose)?; build_project_at(
&member.path,
profile,
target,
&[],
force,
verbose,
workspace_root,
)?;
} }
Ok(()) Ok(())
} }
@@ -499,6 +576,7 @@ fn build_project_at(
exclude_dirs: &[std::path::PathBuf], exclude_dirs: &[std::path::PathBuf],
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace_root: Option<&Path>,
) -> Result<(), String> { ) -> Result<(), String> {
with_dir(project_root, || { with_dir(project_root, || {
check_interrupted()?; check_interrupted()?;
@@ -506,7 +584,22 @@ fn build_project_at(
if !items.contains(&"dcr.toml".to_string()) { if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string()); return Err("dcr.toml file not found".to_string());
} }
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let mut config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
// Inherit build fields from workspace root if configured
if let Some(root) = workspace_root {
let has_inherit = config
.get("build.inherit")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if has_inherit {
let root_path = root.join("dcr.toml");
if root_path.is_file() {
let parent_config =
Config::open(&root_path.to_string_lossy()).map_err(|e| e.to_string())?;
config.merge_parent(&parent_config);
}
}
}
let project_name = get_config_str(&config, "package.name"); let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version"); let project_version = get_config_str(&config, "package.version");
let build_target_config = get_build_string_with_profile(&config, "target", profile); let build_target_config = get_build_string_with_profile(&config, "target", profile);
@@ -598,8 +691,33 @@ fn build_project_at(
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
let target_dir = build_target.map(|t| format!("target/{t}/{profile}")); let target_dir_binding = match workspace_root {
ensure_target_dirs(&items, profile, target_dir); Some(root) => {
let target_str = build_target
.map(normalize_target_os)
.filter(|t| !t.is_empty());
let rel: PathBuf = match target_str {
Some(ref t) => Path::new("target").join(t).join(profile),
None => {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else {
profile.to_string()
};
Path::new("target").join(&default_dir)
}
};
root.join(&rel).to_string_lossy().to_string()
}
None => normalize_target(build_target.unwrap_or(""), profile).unwrap_or_default(),
};
let target_dir = if target_dir_binding.is_empty() {
None
} else {
Some(target_dir_binding.clone())
};
ensure_target_dirs(&items, profile, target_dir, workspace_root.is_some());
let deps_table = config.get("dependencies").and_then(|v| v.as_table()); let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let resolved = resolve_deps(&config, profile, build_target, project_root)?; let resolved = resolve_deps(&config, profile, build_target, project_root)?;
@@ -635,7 +753,15 @@ fn build_project_at(
dep_root.display() dep_root.display()
)); ));
} }
build_project_at(&dep_root, profile, build_target, &[], force, verbose)?; build_project_at(
&dep_root,
profile,
build_target,
&[],
force,
verbose,
None,
)?;
print!( print!(
"\r{:100}\r {} {} v{}", "\r{:100}\r {} {} v{}",
"", "",
@@ -724,7 +850,6 @@ fn build_project_at(
} }
} }
let target_dir_binding = normalize_target(build_target.unwrap_or(""), profile);
let version_info = parse_version_info(&project_version); let version_info = parse_version_info(&project_version);
let substitute = |s: &str| -> String { let substitute = |s: &str| -> String {
s.replace("{profile}", profile) s.replace("{profile}", profile)
@@ -749,7 +874,11 @@ fn build_project_at(
standard: &build_standard, standard: &build_standard,
cxx_standard: &build_cxx_standard, cxx_standard: &build_cxx_standard,
target: build_target, target: build_target,
target_dir: target_dir_binding.as_deref(), target_dir: if target_dir_binding.is_empty() {
None
} else {
Some(target_dir_binding.as_str())
},
kind: normalize_kind(&build_kind), kind: normalize_kind(&build_kind),
platform: normalize_platform(&build_platform), platform: normalize_platform(&build_platform),
linker: resolved_linker.as_deref(), linker: resolved_linker.as_deref(),

View File

@@ -21,6 +21,7 @@ use crate::utils::log::warn;
pub struct BuildRunFlags { pub struct BuildRunFlags {
pub profile: String, pub profile: String,
pub target: Option<String>, pub target: Option<String>,
pub workspace: Option<String>,
pub force: bool, pub force: bool,
pub clean: bool, pub clean: bool,
pub verbose: bool, pub verbose: bool,
@@ -29,6 +30,7 @@ pub struct BuildRunFlags {
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> { pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
let mut profile = PROFILE.to_string(); let mut profile = PROFILE.to_string();
let mut target = None; let mut target = None;
let mut workspace = None;
let mut force = false; let mut force = false;
let mut clean = false; let mut clean = false;
let mut verbose = false; let mut verbose = false;
@@ -52,6 +54,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
verbose = true; verbose = true;
continue; continue;
} }
if candidate == "workspace" {
if let Some(w) = iter.next() {
workspace = Some(w.clone());
} else {
warn("--workspace requires a value");
return Err(1);
}
continue;
}
if candidate == "target" { if candidate == "target" {
if let Some(t) = iter.next() { if let Some(t) = iter.next() {
target = Some(t.clone()); target = Some(t.clone());
@@ -76,6 +87,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
Ok(BuildRunFlags { Ok(BuildRunFlags {
profile, profile,
target, target,
workspace,
force, force,
clean, clean,
verbose, verbose,

View File

@@ -24,6 +24,7 @@ use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir; use crate::utils::fs::with_dir;
use crate::utils::log::error; use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use std::path::Path;
use std::process::Command; use std::process::Command;
fn get_run_cmd( fn get_run_cmd(
@@ -82,18 +83,82 @@ pub fn run(args: &[String]) -> i32 {
return 1; return 1;
} }
}; };
let flags = match parse_build_run_flags(args) {
Ok(v) => v,
Err(_) => return 1,
};
// Handle workspace-only root: delegate to workspace member
if config.is_workspace_only() {
let ws = match crate::core::workspace::parse_workspace(
&config,
&flags.profile,
flags.target.as_deref(),
&root,
) {
Ok(Some(ws)) => ws,
Ok(None) => {
error("Workspace root has no members defined");
return 1;
}
Err(e) => {
error(&e);
return 1;
}
};
let member = match &flags.workspace {
Some(name) => ws.members.iter().find(|m| m.name == *name),
None => ws.main_member(),
};
let member = match member {
Some(m) => m,
None => {
if let Some(name) = &flags.workspace {
error(&format!("Workspace member '{name}' not found"));
} else {
error("No workspace member to run (set `main = true` on one member)");
}
return 1;
}
};
// Build and run from the member's directory
return match with_dir(&member.path, || {
run_project(&member.path, &flags, Some(root.as_path()))
}) {
Ok(code) => code,
Err(e) => {
error(&e);
1
}
};
}
match run_project(&root, &flags, None) {
Ok(code) => code,
Err(e) => {
error(&e);
1
}
}
}
fn run_project(
root: &Path,
flags: &crate::cli::flags::BuildRunFlags,
workspace_root: Option<&Path>,
) -> Result<i32, String> {
let config =
Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?;
let project_name: &str = config let project_name: &str = config
.get("package.name") .get("package.name")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let mut flags = match parse_build_run_flags(args) {
Ok(v) => v,
Err(_) => return 1,
};
// If no target specified, use default host target for target-specific config // If no target specified, use default host target for target-specific config
if flags.target.is_none() { let mut target = flags.target.clone();
if target.is_none() {
let default_target = if cfg!(target_os = "linux") { let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu" "x86_64-unknown-linux-gnu"
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
@@ -103,7 +168,7 @@ pub fn run(args: &[String]) -> i32 {
} else { } else {
"unknown" "unknown"
}; };
flags.target = Some(default_target.to_string()); target = Some(default_target.to_string());
} }
let build_kind = config let build_kind = config
@@ -112,25 +177,30 @@ pub fn run(args: &[String]) -> i32 {
.or_else(|| config.get("build.kind").and_then(|v| v.as_str())) .or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
.unwrap_or(""); .unwrap_or("");
let normalized_target_dir = flags let normalized_target_dir = match workspace_root {
.target Some(wr) => target
.as_ref() .as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)); .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile))
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
None => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
};
let version = config let version = config
.get("package.version") .get("package.version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version); let run_cmd = get_run_cmd(&config, &flags.profile, target.as_deref(), version);
let kind = build_kind.trim(); let kind = build_kind.trim();
if run_cmd.is_none() if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf") && (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
{ {
error("Cannot run library build"); return Err("Cannot run library build".to_string());
return 1;
} }
let build_status = build(args);
let build_status = build(&args_for_build(flags));
let bin_path = crate::platform::bin_path( let bin_path = crate::platform::bin_path(
&flags.profile, &flags.profile,
project_name, project_name,
@@ -140,15 +210,15 @@ pub fn run(args: &[String]) -> i32 {
if let Some(cmd) = run_cmd { if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd); println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------"); println!("--------------------------------");
return run_shell(&cmd); return Ok(run_shell(&cmd));
} }
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------"); println!("--------------------------------");
return run_binary( return Ok(run_binary(
project_name, project_name,
&flags.profile, &flags.profile,
normalized_target_dir.as_deref(), normalized_target_dir.as_deref(),
); ));
} }
let fallback_code = if let Some(cmd) = run_cmd { let fallback_code = if let Some(cmd) = run_cmd {
@@ -161,11 +231,27 @@ pub fn run(args: &[String]) -> i32 {
) )
}; };
if fallback_code != 1 { if fallback_code != 1 {
return fallback_code; return Ok(fallback_code);
} }
error("Fix errors in the code to run the project"); Err("Fix errors in the code to run the project".to_string())
1 }
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
if let Some(ref target) = flags.target {
args.push("--target".to_string());
args.push(target.clone());
}
if let Some(ref name) = flags.workspace {
args.push("--workspace".to_string());
args.push(name.clone());
}
if flags.verbose {
args.push("--verbose".to_string());
}
args
} }
fn run_shell(cmd: &str) -> i32 { fn run_shell(cmd: &str) -> i32 {

View File

@@ -77,8 +77,10 @@ pub struct Config {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig { pub struct DcrConfig {
pub package: PackageConfig, #[serde(default)]
pub build: BuildConfig, pub package: Option<PackageConfig>,
#[serde(default)]
pub build: Option<BuildConfig>,
#[serde(default)] #[serde(default)]
pub dependencies: BTreeMap<String, DependencyConfig>, pub dependencies: BTreeMap<String, DependencyConfig>,
#[serde(default)] #[serde(default)]
@@ -123,6 +125,8 @@ pub struct BuildConfig {
pub clean: Vec<String>, pub clean: Vec<String>,
#[serde(default)] #[serde(default)]
pub src_disable: Option<bool>, pub src_disable: Option<bool>,
#[serde(default)]
pub workspace_only: bool,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@@ -201,13 +205,100 @@ impl Config {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn package(&self) -> &PackageConfig { pub fn package(&self) -> Option<&PackageConfig> {
&self.typed.package self.typed.package.as_ref()
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn build_config(&self) -> &BuildConfig { pub fn build_config(&self) -> Option<&BuildConfig> {
&self.typed.build self.typed.build.as_ref()
}
pub fn is_workspace_only(&self) -> bool {
self.typed
.build
.as_ref()
.map(|b| b.workspace_only)
.unwrap_or(false)
&& !self.typed.workspace.is_empty()
}
/// Merge build fields from parent config into self where self has empty/default values.
/// Used for workspace member inheritance (build.inherit = true).
pub fn merge_parent(&mut self, parent: &Config) {
let fields: &[&str] = &[
"language",
"standard",
"cxx_standard",
"compiler",
"kind",
"target",
"platform",
"cflags",
"ldflags",
"exclude",
"include",
"roots",
"clean",
"src_disable",
];
let parent_build = match parent.data.get("build").and_then(|v| v.as_table()) {
Some(t) => t.clone(),
None => return,
};
let self_build = self
.data
.get("build")
.and_then(|v| v.as_table())
.cloned()
.unwrap_or_default();
let mut changes: Vec<(String, toml::Value)> = Vec::new();
for field in fields {
let val = match parent_build.get(*field) {
Some(v) => v,
None => continue,
};
let self_val = self_build.get(*field);
let should_inherit = match val {
toml::Value::Array(_) => self_val.is_none(),
toml::Value::String(_) => self_val
.and_then(|v| v.as_str())
.map(|s| s.trim().is_empty())
.unwrap_or(true),
_ => self_val.is_none(),
};
if should_inherit {
changes.push((field.to_string(), val.clone()));
}
}
if changes.is_empty() {
return;
}
if !self.data.is_table() {
let mut m = Map::new();
m.insert("build".to_string(), toml::Value::Table(Map::new()));
self.data = toml::Value::Table(m);
}
let tbl = self.data.as_table_mut().unwrap();
let build_tbl = tbl
.entry("build".to_string())
.or_insert_with(|| toml::Value::Table(Map::new()));
if let Some(build_tbl) = build_tbl.as_table_mut() {
for (key, val) in changes {
build_tbl.entry(key).or_insert(val);
}
}
if let Ok(typed) = parse_typed_config(&self.data) {
self.typed = typed;
}
} }
pub fn get(&self, key: &str) -> Option<&Value> { pub fn get(&self, key: &str) -> Option<&Value> {
@@ -229,15 +320,29 @@ impl Config {
} }
pub fn validate(&self) -> Result<(), ConfigError> { pub fn validate(&self) -> Result<(), ConfigError> {
if self.typed.package.name.trim().is_empty() { if self.is_workspace_only() {
return self.validate_workspace();
}
let pkg =
self.typed.package.as_ref().ok_or_else(|| {
ConfigError::Invalid("missing [package] section (required)".into())
})?;
let build = self
.typed
.build
.as_ref()
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
if pkg.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into())); return Err(ConfigError::Invalid("package.name is empty".into()));
} }
if self.typed.package.version.trim().is_empty() { if pkg.version.trim().is_empty() {
return Err(ConfigError::Invalid("package.version is empty".into())); return Err(ConfigError::Invalid("package.version is empty".into()));
} }
validate_package_name(&self.typed.package.name)?; validate_package_name(&pkg.name)?;
if let Some(pkg_type) = &self.typed.package.pkg_type { if let Some(pkg_type) = &pkg.pkg_type {
let pkg_type = pkg_type.trim(); let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none" if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{ {
@@ -247,32 +352,32 @@ impl Config {
} }
} }
validate_language_config(&self.typed.build.language, "build.language")?; validate_language_config(&build.language, "build.language")?;
if self.typed.build.standard.trim().is_empty() { if build.standard.trim().is_empty() {
return Err(ConfigError::Invalid("build.standard is empty".into())); return Err(ConfigError::Invalid("build.standard is empty".into()));
} }
if self.typed.build.compiler.trim().is_empty() { if build.compiler.trim().is_empty() {
return Err(ConfigError::Invalid("build.compiler is empty".into())); return Err(ConfigError::Invalid("build.compiler is empty".into()));
} }
if let Some(platform) = &self.typed.build.platform if let Some(platform) = &build.platform
&& platform.trim().is_empty() && platform.trim().is_empty()
{ {
return Err(ConfigError::Invalid("build.platform is empty".into())); return Err(ConfigError::Invalid("build.platform is empty".into()));
} }
validate_toolchain(self.typed.toolchain.as_ref())?; validate_toolchain(self.typed.toolchain.as_ref())?;
if let Some(kind) = &self.typed.build.kind { if let Some(kind) = &build.kind {
let kind = kind.trim(); let kind = kind.trim();
if !kind.is_empty() && !VALID_KINDS.contains(&kind) { if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }
validate_string_list(&self.typed.build.exclude, "build.exclude")?; validate_string_list(&build.exclude, "build.exclude")?;
validate_string_list(&self.typed.build.include, "build.include")?; validate_string_list(&build.include, "build.include")?;
validate_string_list(&self.typed.build.roots, "build.roots")?; validate_string_list(&build.roots, "build.roots")?;
validate_string_list(&self.typed.build.clean, "build.clean")?; validate_string_list(&build.clean, "build.clean")?;
validate_string_list(&self.typed.build.cflags, "build.cflags")?; validate_string_list(&build.cflags, "build.cflags")?;
validate_string_list(&self.typed.build.ldflags, "build.ldflags")?; validate_string_list(&build.ldflags, "build.ldflags")?;
if let Some(target) = &self.typed.build.target { if let Some(target) = &build.target {
validate_non_empty_string(target, "build.target")?; validate_non_empty_string(target, "build.target")?;
} }
for profile in ["release", "debug"] { for profile in ["release", "debug"] {
@@ -824,10 +929,10 @@ mod tests {
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n", "[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
); );
let config = Config::open(&path.to_string_lossy()).unwrap(); let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(config.package().name, "typed"); assert_eq!(config.package().unwrap().name, "typed");
assert_eq!(config.typed().package.version, "1.2.3"); assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
assert_eq!(config.build_config().compiler, "clang"); assert_eq!(config.build_config().unwrap().compiler, "clang");
assert_eq!(config.build_config().cflags, ["-Wall"]); assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
assert!(config.typed().dependencies.contains_key("foo")); assert!(config.typed().dependencies.contains_key("foo"));
} }

View File

@@ -26,6 +26,7 @@ pub struct WorkspaceMember {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,
pub deps: Vec<String>, pub deps: Vec<String>,
pub main: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -33,6 +34,15 @@ pub struct Workspace {
pub members: Vec<WorkspaceMember>, pub members: Vec<WorkspaceMember>,
} }
impl Workspace {
pub fn main_member(&self) -> Option<&WorkspaceMember> {
self.members
.iter()
.find(|m| m.main)
.or_else(|| self.members.first())
}
}
pub fn parse_workspace( pub fn parse_workspace(
config: &Config, config: &Config,
profile: &str, profile: &str,
@@ -85,6 +95,7 @@ pub fn parse_workspace(
name: name.to_string(), name: name.to_string(),
path, path,
deps, deps,
main: tbl.get("main").and_then(|v| v.as_bool()).unwrap_or(false),
}); });
} }