diff --git a/.github/workflows/aur.yml b/.github/workflows/aur.yml deleted file mode 100644 index 5d5244b..0000000 --- a/.github/workflows/aur.yml +++ /dev/null @@ -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 < packaging/aur/dcr/.SRCINFO < ~/.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 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3733033..cb2a538 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,6 +148,82 @@ jobs: body_path: .github/release-notes.md 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 ────────────────────────────────────────────────────────────────── build-macos: if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }} @@ -194,7 +270,7 @@ jobs: body_path: .github/release-notes.md prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} -# ── BSD ──────────────────────────────────────────────────────────────────── + # ── BSD Systems ──────────────────────────────────────────────────────────── build-bsd: if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }} needs: [setup, notes] @@ -210,6 +286,7 @@ jobs: name: release-notes path: .github + # ── FreeBSD ── - name: Install cargo-zigbuild run: | pip install cargo-zigbuild @@ -229,7 +306,8 @@ jobs: cp target/x86_64-unknown-freebsd/release/dcr "$ASSET" echo "FREEBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" - - name: Build in OpenBSD VM + # ── OpenBSD ── + - name: Build in OpenBSD VM uses: vmactions/openbsd-vm@v1 with: usesh: true @@ -243,6 +321,7 @@ jobs: cp target/release/dcr "$ASSET" echo "OPENBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" + # ── NetBSD ── - name: Build in NetBSD VM uses: vmactions/netbsd-vm@v1 with: @@ -257,6 +336,7 @@ jobs: cp target/release/dcr "$ASSET" echo "NETBSD_ASSET=${ASSET}" >> "$GITHUB_ENV" + # ── Assets Upload ── - name: Upload Release Assets uses: softprops/action-gh-release@v2 with: @@ -314,6 +394,218 @@ jobs: body_path: .github/release-notes.md 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 < packages/aur/dcr/.SRCINFO < ~/.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 < packages/aur/dcr-bin/.SRCINFO < ~/.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 ────────────────────────────────────────────────────────────── publish-crates: if: | @@ -321,6 +613,9 @@ jobs: (github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true') needs: [setup, build-linux, build-macos, build-windows, build-bsd] runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 with: @@ -329,7 +624,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Publish to crates.io - run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish # ── Homebrew tap ─────────────────────────────────────────────────────────── publish-brew: diff --git a/Cargo.lock b/Cargo.lock index ea4e6af..fc4c9b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.6.8" +version = "0.6.9" dependencies = [ "ctrlc", "git2", diff --git a/Cargo.toml b/Cargo.toml index b938825..1bb5481 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,46 @@ [package] name = "dcr" -version = "0.6.8" +version = "0.6.9" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." license = "GPL-3.0-or-later" 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) " +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) " +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] glob = "0.3" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } diff --git a/packaging/aur/dcr-git b/packaging/aur/dcr-git new file mode 160000 index 0000000..12038a6 --- /dev/null +++ b/packaging/aur/dcr-git @@ -0,0 +1 @@ +Subproject commit 12038a613e9c314c545cbb912590dc6b698b1dd0 diff --git a/packaging/snap/snapcraft.yaml b/packaging/snap/snapcraft.yaml new file mode 100644 index 0000000..5ea941a --- /dev/null +++ b/packaging/snap/snapcraft.yaml @@ -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/ \ No newline at end of file diff --git a/src/cli/build.rs b/src/cli/build.rs index b7476a7..312c6f3 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -69,7 +69,9 @@ pub fn build(args: &[String]) -> i32 { if flags.target.is_none() { 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"); if !bt.is_empty() { flags.target = Some(bt); @@ -100,6 +102,7 @@ pub fn build(args: &[String]) -> i32 { flags.target.as_deref(), flags.force, flags.verbose, + flags.workspace.as_deref(), ) }) { Ok(()) => 0, @@ -372,8 +375,13 @@ fn get_build_steps_with_profile( get_build_steps(config, &format!("build.{field}")) } -fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option) { - if !items.contains(&"target".to_string()) { +fn ensure_target_dirs( + items: &[String], + profile: &str, + target_dir: Option, + skip_local_target: bool, +) { + if !skip_local_target && !items.contains(&"target".to_string()) { let _ = fs::create_dir("./target"); } if let Some(dir) = &target_dir { @@ -414,8 +422,38 @@ fn build_from_root( target: Option<&str>, force: bool, verbose: bool, + workspace: Option<&str>, ) -> Result<(), 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_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)? { - build_workspace(&workspace, profile, build_target.as_deref(), force, verbose)?; - let excludes: Vec = - workspace.members.iter().map(|m| m.path.clone()).collect(); + if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? { + if let Some(filter_name) = workspace { + for member in ws.members.iter().filter(|m| m.name == filter_name) { + 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 = + 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( root, profile, build_target.as_deref(), - &excludes, + &[], force, 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; @@ -485,9 +553,18 @@ fn build_workspace( target: Option<&str>, force: bool, verbose: bool, + workspace_root: Option<&Path>, ) -> Result<(), String> { 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(()) } @@ -499,6 +576,7 @@ fn build_project_at( exclude_dirs: &[std::path::PathBuf], force: bool, verbose: bool, + workspace_root: Option<&Path>, ) -> Result<(), String> { with_dir(project_root, || { check_interrupted()?; @@ -506,7 +584,22 @@ fn build_project_at( if !items.contains(&"dcr.toml".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_version = get_config_str(&config, "package.version"); 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_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); - let target_dir = build_target.map(|t| format!("target/{t}/{profile}")); - ensure_target_dirs(&items, profile, target_dir); + let target_dir_binding = match workspace_root { + 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 resolved = resolve_deps(&config, profile, build_target, project_root)?; @@ -635,7 +753,15 @@ fn build_project_at( 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!( "\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 substitute = |s: &str| -> String { s.replace("{profile}", profile) @@ -749,7 +874,11 @@ fn build_project_at( standard: &build_standard, cxx_standard: &build_cxx_standard, 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), platform: normalize_platform(&build_platform), linker: resolved_linker.as_deref(), diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 16ead01..0ecb458 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -21,6 +21,7 @@ use crate::utils::log::warn; pub struct BuildRunFlags { pub profile: String, pub target: Option, + pub workspace: Option, pub force: bool, pub clean: bool, pub verbose: bool, @@ -29,6 +30,7 @@ pub struct BuildRunFlags { pub fn parse_build_run_flags(args: &[String]) -> Result { let mut profile = PROFILE.to_string(); let mut target = None; + let mut workspace = None; let mut force = false; let mut clean = false; let mut verbose = false; @@ -52,6 +54,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { verbose = true; 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 let Some(t) = iter.next() { target = Some(t.clone()); @@ -76,6 +87,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { Ok(BuildRunFlags { profile, target, + workspace, force, clean, verbose, diff --git a/src/cli/run.rs b/src/cli/run.rs index 8725354..a7ca498 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -24,6 +24,7 @@ use crate::utils::fs::find_project_root; use crate::utils::fs::with_dir; use crate::utils::log::error; use crate::utils::text::{BOLD_GREEN, colored}; +use std::path::Path; use std::process::Command; fn get_run_cmd( @@ -82,18 +83,82 @@ pub fn run(args: &[String]) -> i32 { 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 { + let config = + Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?; + let project_name: &str = config .get("package.name") .and_then(|v| v.as_str()) .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 flags.target.is_none() { + let mut target = flags.target.clone(); + if target.is_none() { let default_target = if cfg!(target_os = "linux") { "x86_64-unknown-linux-gnu" } else if cfg!(target_os = "macos") { @@ -103,7 +168,7 @@ pub fn run(args: &[String]) -> i32 { } else { "unknown" }; - flags.target = Some(default_target.to_string()); + target = Some(default_target.to_string()); } 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())) .unwrap_or(""); - let normalized_target_dir = flags - .target - .as_ref() - .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)); + let normalized_target_dir = match workspace_root { + Some(wr) => target + .as_ref() + .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 .get("package.version") .and_then(|v| v.as_str()) .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(); if run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf") { - error("Cannot run library build"); - return 1; + return Err("Cannot run library build".to_string()); } - let build_status = build(args); + + let build_status = build(&args_for_build(flags)); let bin_path = crate::platform::bin_path( &flags.profile, project_name, @@ -140,15 +210,15 @@ pub fn run(args: &[String]) -> i32 { if let Some(cmd) = run_cmd { println!("\n {} {}", colored("Running", BOLD_GREEN), cmd); println!("--------------------------------"); - return run_shell(&cmd); + return Ok(run_shell(&cmd)); } println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("--------------------------------"); - return run_binary( + return Ok(run_binary( project_name, &flags.profile, normalized_target_dir.as_deref(), - ); + )); } let fallback_code = if let Some(cmd) = run_cmd { @@ -161,11 +231,27 @@ pub fn run(args: &[String]) -> i32 { ) }; if fallback_code != 1 { - return fallback_code; + return Ok(fallback_code); } - error("Fix errors in the code to run the project"); - 1 + Err("Fix errors in the code to run the project".to_string()) +} + +fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec { + 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 { diff --git a/src/core/config.rs b/src/core/config.rs index c4bdcc8..4d76099 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -77,8 +77,10 @@ pub struct Config { #[allow(dead_code)] #[derive(Debug, Clone, Deserialize)] pub struct DcrConfig { - pub package: PackageConfig, - pub build: BuildConfig, + #[serde(default)] + pub package: Option, + #[serde(default)] + pub build: Option, #[serde(default)] pub dependencies: BTreeMap, #[serde(default)] @@ -123,6 +125,8 @@ pub struct BuildConfig { pub clean: Vec, #[serde(default)] pub src_disable: Option, + #[serde(default)] + pub workspace_only: bool, } #[derive(Debug, Clone, Deserialize)] @@ -201,13 +205,100 @@ impl Config { } #[allow(dead_code)] - pub fn package(&self) -> &PackageConfig { - &self.typed.package + pub fn package(&self) -> Option<&PackageConfig> { + self.typed.package.as_ref() } #[allow(dead_code)] - pub fn build_config(&self) -> &BuildConfig { - &self.typed.build + pub fn build_config(&self) -> Option<&BuildConfig> { + 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> { @@ -229,15 +320,29 @@ impl Config { } 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())); } - if self.typed.package.version.trim().is_empty() { + if pkg.version.trim().is_empty() { 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(); 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")?; - if self.typed.build.standard.trim().is_empty() { + validate_language_config(&build.language, "build.language")?; + if build.standard.trim().is_empty() { 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())); } - if let Some(platform) = &self.typed.build.platform + if let Some(platform) = &build.platform && platform.trim().is_empty() { return Err(ConfigError::Invalid("build.platform is empty".into())); } 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(); if !kind.is_empty() && !VALID_KINDS.contains(&kind) { return Err(ConfigError::Invalid("build.kind is invalid".into())); } } - validate_string_list(&self.typed.build.exclude, "build.exclude")?; - validate_string_list(&self.typed.build.include, "build.include")?; - validate_string_list(&self.typed.build.roots, "build.roots")?; - validate_string_list(&self.typed.build.clean, "build.clean")?; - validate_string_list(&self.typed.build.cflags, "build.cflags")?; - validate_string_list(&self.typed.build.ldflags, "build.ldflags")?; - if let Some(target) = &self.typed.build.target { + validate_string_list(&build.exclude, "build.exclude")?; + validate_string_list(&build.include, "build.include")?; + validate_string_list(&build.roots, "build.roots")?; + validate_string_list(&build.clean, "build.clean")?; + validate_string_list(&build.cflags, "build.cflags")?; + validate_string_list(&build.ldflags, "build.ldflags")?; + if let Some(target) = &build.target { validate_non_empty_string(target, "build.target")?; } 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", ); let config = Config::open(&path.to_string_lossy()).unwrap(); - assert_eq!(config.package().name, "typed"); - assert_eq!(config.typed().package.version, "1.2.3"); - assert_eq!(config.build_config().compiler, "clang"); - assert_eq!(config.build_config().cflags, ["-Wall"]); + assert_eq!(config.package().unwrap().name, "typed"); + assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3"); + assert_eq!(config.build_config().unwrap().compiler, "clang"); + assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]); assert!(config.typed().dependencies.contains_key("foo")); } diff --git a/src/core/workspace.rs b/src/core/workspace.rs index beac290..440c7b4 100644 --- a/src/core/workspace.rs +++ b/src/core/workspace.rs @@ -26,6 +26,7 @@ pub struct WorkspaceMember { pub name: String, pub path: PathBuf, pub deps: Vec, + pub main: bool, } #[derive(Debug, Clone)] @@ -33,6 +34,15 @@ pub struct Workspace { pub members: Vec, } +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( config: &Config, profile: &str, @@ -85,6 +95,7 @@ pub fn parse_workspace( name: name.to_string(), path, deps, + main: tbl.get("main").and_then(|v| v.as_bool()).unwrap_or(false), }); }