Cross-Tool Synthesis & the Dub Delta
The capstone of the monorepo-tooling survey. Part 1 distils the 44 deep-dives and the concepts vocabulary into a head-to-head comparison across the five axes that decide a monorepo tool's character: workspace declaration, dependency isolation, task DAG & change detection, caching & remote execution, and CLI/filter ergonomics. From that synthesis it extracts The Consensus Standard (the baseline shared across almost every modern ecosystem), maps the Architectural Trade-offs between the tool families, and closes with The Dub Delta — an explicit capability-by-capability gap analysis of dub against the consensus, the bridge into the milestoned proposal.
Scope. This is the synthesis leaf of the survey. It assumes the per-tool mechanics (deep-dives) and the shared definitions (concepts) as given and cross-links rather than re-derives them. For
dub's current architecture read directly from thedlang/dubsource tree, see the baseline; for the concrete enhancement plan, see the proposal. The monorepo-tooling analogue of async-io's comparison.
Last reviewed: June 5, 2026
Part 1 — Cross-tool synthesis
1.1 The 44 tools at a glance
The master catalog, grouped by category. DAG = does the tool build a multi-member task/target graph (vs. a single package's compile DAG). Cache summarises the deepest tier reached: dl (download/metadata only), local (local task-output), remote (shared/HTTP/gRPC cache), REAPI (Remote Execution API). XRef = the local cross-reference mechanism. The link cell points at each deep-dive.
Package managers (resolve & isolate; mostly no task DAG)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
npm | JS/TS | Root-package (workspaces) | no | dl | semver + symlink |
yarn-berry | JS/TS | Root-package (workspaces) | pkg (foreach) | dl | workspace: |
pnpm | JS/TS | Virtual (pnpm-workspace) | pkg (-r) | dl | workspace: + catalog: |
bun | JS/TS | Root/virtual (workspaces) | pkg (--filter) | dl | workspace: + catalog: |
uv | Python | Cargo-style (dual-mode) | no | local | { workspace = true } |
poetry | Python | None native | no | dl | path + develop=true |
hatch | Python | Env-scoped workspace.members | no | local | editable members + path |
cargo | Rust | [workspace] (dual-mode) | action | local | path + .workspace = true |
go-work | Go | Virtual (go.work) | no | local | implicit (MVS main modules) |
gradle | JVM | Multi-project + composite | task | remote | project(":path") + substitution |
maven | JVM | Aggregator (<modules>) | task (reactor) | dl | GAV + ReactorReader |
sbt | Scala/JVM | Multi-project (Scala DSL) | task | REAPI (2.x) | dependsOn(proj) |
mill | Scala/JVM | Module tree (Scala DSL) | action | local | moduleDeps |
composer | PHP | None native | no | dl | path repos + replace |
JS/TS task orchestrators (overlay a DAG + cache on a package manager's workspace)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
nx | JS/TS | Inherited from PM | task | remote | delegated to PM |
turborepo | JS/TS | Inherited from PM | task | remote | delegated to PM |
lerna | JS/TS | Inherited from PM | task (Nx) | remote (Nx) | delegated to PM |
rush | JS/TS | Explicit projects[] | task | remote | workspace: (via pnpm) |
lage | JS/TS | Inherited from PM | task | remote | delegated to PM |
wireit | JS/TS | Inherited from PM | task | remote (GH) | relative path + script |
Polyglot build orchestrators (own the action graph, often down to REAPI)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
bazel | Polyglot | FS auto-detect (BUILD) | action | REAPI | //path:target label |
buck2 | Polyglot | FS auto-detect (BUCK) | action | REAPI | cell//path:target |
pants | Polyglot | FS auto-detect (BUILD) | action | REAPI | inferred from imports |
please | Polyglot | FS auto-detect (BUILD) | action | REAPI | //path:target label |
moon | Polyglot | Root-anchored (.moon/) | action | remote | host PM + dependsOn |
gn | Polyglot | FS auto-detect (.gn) | action | none | //path:target label |
Container / CI-oriented (data-flow DAG over containerised steps)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
dagger | Container | Module graph | data-flow | remote | dagger install ./mod |
earthly | Container | Emergent +target graph | data-flow | remote | +target (cross-repo) |
garden | Container | Stack Graph (scanned) | data-flow | remote | action-to-action |
Generic task runners (a deps DAG; no package model)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
task | Polyglot | includes namespaces | task | local (skip) | task edges |
just | Polyglot | mod namespace tree | task | none | task edges |
mise | Polyglot | config_roots (experimental) | task | local (skip) | task edges (//path:task) |
make | Polyglot | None (recursive $(MAKE)) | task | local (mtime) | file edges |
Native build systems (one global target DAG; toolchain owns linkage)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
meson | Native | subprojects/ + .wrap | target | local (mtime) | .wrap [provide] |
cmake | Native | None (add_subdirectory) | target | none | target name |
scons | Native | SConscript tree | action | remote (CacheDir) | graph edges |
waf | Native | recurse() tree | action | remote (wafcache) | use='A B' |
ninja | Native | Generated build.ninja | action | local (mtime) | file edges |
Remote-execution backends (REAPI servers; no client-side workspace)
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
buildbuddy | REAPI | none (server) | no (consumes) | REAPI | n/a |
buildbarn | REAPI | none (server) | no (consumes) | REAPI | n/a |
nativelink | REAPI | none (server) | no (consumes) | REAPI | n/a |
Minimalist / research & polyglot glue
| Tool | Ecosystem | Workspace model | DAG | Cache | Local cross-refs |
|---|---|---|---|---|---|
redo | Research | .do tree + .redo db | action (dynamic) | local | redo-ifchange ../x |
tup | Research | .tup SQLite db | action (file) | none | group/bin edges |
nix-flakes | Polyglot | Flake graph (flake.lock) | derivation | remote (binary cache) | relative path: |
NOTE
The seven category bands above are the same grouping the umbrella index carries in its sidebar. dub is omitted from these tables because it is the baseline, not a surveyed peer — its row is the subject of The Dub Delta.
The single most useful lens on the catalog: the five axes are largely orthogonal, and a tool's category predicts where it sits on each. Package managers max out isolation and lockfiles but stop before the task DAG; orchestrators inherit the workspace and add a DAG + cache but own no dependency resolution; the polyglot engines fuse all five into one content-addressed action graph at the cost of adopting a foreign build language. The sections below walk each axis.
1.2 Workspace declaration & topology
How are members discovered, and is the root itself a buildable package? Three discovery mechanisms and two root models (full taxonomy in concepts §1):
| Discovery | Root model | Exemplars |
|---|---|---|
| Glob array | root-package or virtual | cargo, uv, pnpm, npm, bun, yarn-berry |
| Explicit array | aggregator/registry | maven (<modules>), go-work (use), rush (projects[]), gradle (include()) |
| Filesystem auto-detect | implicit (tree) | bazel, buck2, pants, please, gn, nix-flakes |
| Inherited from PM | none of its own | nx, turborepo, lerna, lage, wireit, moon |
The reference design is cargo's dual-mode root: a [workspace] table either sits inside a buildable crate (root-package form) or stands alone with no [package] (virtual form), and members are glob-expanded path arrays plus the transitive closure of path dependencies. uv ports this verbatim ([tool.uv.workspace] with members/exclude, root-package or virtual), and bun supports both. pnpm is purely virtual (a dedicated pnpm-workspace.yaml), and go-work is virtual and developer-local — its go.work use-lists modules and is deliberately kept out of VCS.
At the opposite pole, the polyglot engines need no member array at all: every directory with a marker file is a package. bazel addresses each as a //-rooted label and discovers topology from deps edges; pants goes furthest, inferring even the cross-references from import statements. The JS/TS orchestrators are parasitic — lage, turborepo, and nx declare no members, reading the host package manager's workspaces globs and overlaying only a task layer. Finally, several mainstream tools have no native workspace (poetry, composer, cmake): multi-package trees are improvised from per-project manifests glued by relative paths — exactly where dub's subPackages array sits today.
NOTE
A workspace boundary is anchored by a marker file the CLI walks up to find: Cargo.toml with [workspace], pnpm-workspace.yaml, MODULE.bazel, .buckconfig, pants.toml, .plzconfig, a .gn dotfile, or a .tup/.redo database. dub has no such workspace marker — only a root recipe enumerating sub-packages by path.
1.3 Dependency isolation & local cross-references
Two coupled questions: where do third-party deps land on disk, and how does a member reference a sibling member's source (concepts §2–3).
Isolation. Four models, least to most isolated: flat hoisting (one root node_modules, npm; trades on phantom deps), isolated symlinks (pnpm's node_modules/.pnpm virtual store, rush), content-addressed store (yarn-berry Plug'n'Play .pnp.cjs, uv LinkMode::Clone reflink, bun), and per-project vendoring (composer vendor/, poetry/hatch per-project .venv). The REAPI engines (bazel, buck2) and nix-flakes generalise the content-addressed store to all inputs and outputs — the same CAS underpins both their dependency model and their cache (§1.5). The native runners (make, ninja, just, task) have no isolation layer at all: package placement is the native toolchain's job — the model dub's shared $DUB_HOME/packages/ cache already follows.
Local cross-references. The defining monorepo capability — depending on a sibling's source without publishing. Four mechanisms recur:
| Mechanism | Edge is… | Exemplars |
|---|---|---|
workspace: protocol | local-first selector, rewritten on publish | yarn-berry, pnpm, bun, rush |
| Workspace-source flag | { workspace = true } / dep.workspace | uv, cargo |
Relative path= dep | a directory path to the sibling | cargo, composer, poetry, dub today |
| Typed project / label | a project value or graph label | sbt (dependsOn), mill (moduleDeps), bazel (//path:target) |
yarn-berry is the reference: a workspace:*/^/~ selector links to the member's source with LinkType.SOFT (symlinked, never fetched, not persisted to the lockfile), then the beforeWorkspacePacking hook rewrites it to a real registry range at publish. go-work makes the same idea implicit — every used module is a co-equal main module, so a cross-member import resolves to on-disk source through MVS with no replace, version, or publish step. maven has a unique variant: a sibling is an ordinary <dependency> GAV, and the reactor's ReactorReader intercepts resolution within the build to serve the freshly-built target/.
A second layer is version unification: a central [workspace.dependencies] (cargo) or catalog: (pnpm, bun, yarn-berry) registry at the root, so every member writes react.workspace = true / "react": "catalog:" and a shared upstream is pinned once. cargo couples this with field inheritance (version.workspace = true, authors.workspace = true). This is precisely the fragmentation dub suffers: each Sparkles member pins expected/silly independently, with nothing reconciling them — the Milestone-2 target of the proposal.
1.4 Task DAG & change detection
Dependency placement determines what builds against what; the task graph determines what runs in what order. The pivotal distinction is graph granularity (concepts §4):
| Graph kind | Nodes are… | Exemplars |
|---|---|---|
| Package graph | whole members | pnpm -r, yarn-berry foreach -t, bun --filter |
| Task graph | (package × task) pairs | turborepo, nx, lage, wireit, rush, maven reactor |
| Action graph | single tool invocations | cargo units, bazel (Skyframe), buck2 (DICE), ninja file edges, mill |
| Implicit data-flow | API/command calls | dagger (BuildKit LLB), earthly, garden (Stack Graph) |
A package graph topo-sorts whole members under a concurrency cap — pnpm chunks the project DAG via graphSequencer and runs it under pLimit-bounded --workspace-concurrency; yarn-berry's yarn workspaces foreach -t is the direct inspiration for the dub proposal's loop. A task graph crosses that package graph with a per-task pipeline: lage's dep-specs ^build (dependencies' build), ^^transpile (transitive), pkg#task (specific node), and bare build (same package) are the canonical vocabulary, shared by turborepo (dependsOn) and nx. An action graph dissolves packages entirely: bazel's action graph is the task DAG, so a Skyframe rebuild touches only the reverse-transitive closure of changed inputs.
A crucial gap: most package managers stop before any task DAG. npm run --workspaces runs scripts sequentially in declared order — no topology, no parallelism — which is the entire reason turborepo, nx, lage, and wireit exist as overlays. uv, poetry, composer, and dub own no member-level task graph at all.
Change detection bounds work to what changed, in three increasingly precise families:
| Mechanism | Compares… | Exemplars |
|---|---|---|
| mtime / timestamp | file mtimes vs. outputs | make, ninja, cmake/meson (delegated) |
| Input hashing | content hash vs. stored fingerprint | turborepo, nx (xxh3_64), bazel, gradle, cargo fingerprints |
Affected / --since | a VCS diff → changed members + dependents | lerna, nx affected, moon, please (plz query changes --since) |
These compose: nx layers --since affected detection over its xxh3_64 computation hash, and moon pairs per-task content hashing with a Git-aware tracker. mtime detection is fragile on ephemeral CI — make's incrementality "vanishes on a fresh checkout," the structural reason CI-heavy monorepos migrate to input-hashing tools. dub's build cache is content-addressed (a genuine strength, §1.5) but per-package, with no --since slicing — the Milestone-4 target of the proposal.
1.5 Caching & remote execution
The caching ladder, deepest tier reached varying by where and what (concepts §5):
| Tier | Reuses… | Exemplars |
|---|---|---|
| Download / metadata | fetched package archives only | npm, composer, maven (~/.m2), poetry, go-work, dub ($DUB_HOME) |
| Local task-output | build/test outputs on this machine | turborepo, nx, gradle, mill, cargo target/, bazel --disk_cache |
| Remote / shared | outputs across machines & CI | turborepo (HttpCache), nx (Nx Cloud), gradle (HttpBuildCache), scons, waf, sbt 2.x |
| Remote execution (REAPI) | runs the action on a remote worker | bazel, buck2, pants, please, buildbuddy, buildbarn, nativelink |
The crucial cliff is between remote cache and remote execution. Almost every modern orchestrator now offers a remote cache that replays a prior result (terminal logs + output files) keyed by a content hash — turborepo, nx, lage, rush, wireit, gradle, moon. Far fewer run the action remotely: remote execution ships an action's inputs to a worker fleet and runs the compiler there, which requires the action to be hermetic (fully described by its declared inputs). Only the polyglot engines and the dedicated REAPI servers — buildbuddy, buildbarn, nativelink — implement the Remote Execution API (REAPI v2): ContentAddressableStorage + ByteStream + ActionCache + (optionally) Execution. These backends own no workspace; they only ever see the hashed, post-analysis action graph a client emits. sbt 2.x is the rare language package manager with native REAPI-compatible task caching; nix-flakes reaches the deepest hermeticity in the catalog (a content-addressed store keyed by the full build closure) but over Nix's own protocol, orthogonal to REAPI.
This is the largest single capability gap. The language package managers (cargo, uv, go-work, npm, pnpm, composer) and dub cache only downloads and local incremental build state; none has a shared task-output cache. dub does have a real, content-addressed local build cache (its computeBuildID hashes the full build inputs) — but it is local-only, per-package, and has no remote tier.
1.6 CLI & filter ergonomics
The developer command boundary — how to target one member, several, or a subgraph:
| Ergonomic | Vocabulary | Exemplars |
|---|---|---|
| Per-package selector | -p <pkg> / :pkg / colon-label | cargo (-p), bazel (//path:target), dub (:subpkg) |
| Filter grammar | --filter <pattern> (name/path/glob) | pnpm, turborepo, bun, nx, lerna (--scope) |
| Topological broadcast | foreach -t / -r over all members | yarn-berry, pnpm (-r), maven reactor, cargo (--workspace) |
| Subgraph traversal | --from / dependents / ...pkg | pnpm (...pkg), turborepo (...), nx (affected) |
| Change-based slicing | --since <ref> | lerna, nx, moon, please |
| Concurrency controls | -j/--jobs, --parallel, --concurrency | cargo, ninja, turborepo (10), nx (3) |
pnpm's --filter is the richest grammar — name globs, path globs, the ...pkg dependents/dependencies expansion, and [<ref>] git-range selectors all compose. cargo offers the cleanest binary pair: -p <member> for one, --workspace for all. The orchestrators add change-aware selection on top — nx affected and lerna --since compute the changed members plus their impacted dependents from a git diff.
dub's CLI surface is the minimal end of this axis. It offers the :subpkg selector and --root <path>, but no filter/selection vocabulary for multiple members: no --filter, no repeatable -p, no --recursive/--from/--since, no "all members" broadcast. The one fan-out flag, upgrade -s/--sub-packages, is a one-off that re-enters dub's single-root machinery N times. Multi-package work is "loop it yourself" — exactly what the Sparkles apps/ci helper does.
The Consensus Standard
Cutting across the 44 tools, a baseline feature set has emerged that almost every modern monorepo ecosystem now ships (or that teams bolt on if the native tool lacks it). A tool is "consensus-grade" when it offers all of:
- A declared workspace with glob member discovery — a root manifest naming members by glob (
members = ["libs/*", "apps/*"]), with a virtual (non-buildable) root option. Shipped natively bycargo,uv,pnpm,bun,npm/yarn-berry. The polyglot engines substitute filesystem auto-detection (bazel). - A
workspace:-style local-first cross-reference — depend on a sibling by name, resolved to its source in dev, rewritten to a real range on publish. Theyarn-berry/pnpmworkspace:protocol,cargo'sdep.workspace = true,go-work's implicit MVS. - A unified root lockfile — one lock resolving all members together so a transitive dependency is pinned once, monorepo-wide.
cargo(Cargo.lock),pnpm,uv,bun,yarn-berry. - Central version unification — a
[workspace.dependencies]table orcatalog:protocol eliminating per-member version drift.cargo,pnpm,bun,yarn-berry. - A topological task DAG with bounded concurrency — build/test a member only after its workspace dependencies, running independent legs in parallel under a
-j/--concurrencycap.yarn-berryforeach -t,pnpm-r,turborepo,nx,cargo,gradle,maven. - A filter/selection vocabulary — target one member, a glob of members, or a change-bounded subgraph (
--filter,-p,--since).pnpm,turborepo,nx,cargo,lerna. - Content-addressed task-output caching — a cache key over source contents + command + env + upstream output hashes, replaying a prior result instead of recomputing.
turborepo,nx,gradle,mill,bazel— increasingly with a remote tier.
Items 1–6 are now table stakes for a serious monorepo; item 7's local form is common, its remote form is the current frontier, and REAPI remote execution (the eighth, aspirational tier) remains the exclusive province of the heavy polyglot engines. The consensus tool lands in the middle-to-right of every axis in the concepts grid: dual-mode root, glob discovery, a workspace: protocol, a unified lockfile, a topological DAG with input-hash change detection, a filter grammar, and at least a local content-addressed cache.
Architectural Trade-offs
The catalog's tools cluster into seven families, each making a different bet about what to own and what to delegate. The central tension is power vs. ceremony: the more of the five axes a tool owns natively, the more it imposes its own build language and the less it cooperates with the host ecosystem.
| Family | Owns | Delegates | The bet |
|---|---|---|---|
| Heavy polyglot engines | all five axes; action graph; REAPI | the host language's idioms | hermeticity + scale are worth a foreign build language |
| Language-native workspaces | workspace, lockfile, package DAG | task pipelines, remote cache | the ecosystem's own model is enough; stay idiomatic |
| JS task orchestrators | task DAG, cache, change detection | workspace + deps (to the package manager) | overlay a graph on the PM you already use |
| Minimalist task runners | a deps DAG, namespacing | everything else (packages, cache, isolation) | be a thin, universal command front-end |
| Container / CI engines | a data-flow DAG, layer cache, hermetic steps | the package model | the container is the unit of reproducibility |
| Remote-execution backends | the CAS + scheduler + worker fleet | the entire workspace (to the client) | be the server half of REAPI; own nothing above the action |
| Polyglot glue | a content-hashed dependency/derivation graph | task pipelines | pin and wire what no single ecosystem tool can see whole |
Heavy polyglot action-graph engines (bazel, buck2, pants, please) own everything down to the leaf compiler invocation. They dissolve packages into a fine-grained action DAG, content-address every action, and reach REAPI remote execution — bazel's Skyframe rebuilds only the reverse-transitive closure of changed inputs; buck2's DICE graph gives near-instant no-op rebuilds; pants infers the graph from imports so there is nothing to hand-wire. The price is a foreign build language (Starlark BUILD files), a steep hermeticity discipline, and abandoning the host ecosystem's idioms. This is the right bet at Google/Meta scale and the wrong one for a five-package library.
Language-native workspaces (cargo, uv, pnpm, yarn-berry, go-work) own the workspace, the unified lockfile, and a package-level DAG, but stop before user-defined task pipelines and remote caching. They stay maximally idiomatic — a Rust dev needs no new tool, just a [workspace] table — at the cost of a coarse graph (whole-member, not per-action) and no shared cache. cargo is the canonical example and the most direct precedent for the proposed dub workspace feature: one root manifest, one Cargo.lock, one shared target/, a topologically-scheduled fingerprint-cached build DAG, dual-mode root, and [workspace.dependencies] inheritance. This is the family dub should join.
JS task orchestrators (nx, turborepo, lage, wireit, rush) are the overlay answer: they own the task DAG, content-addressed cache, and change detection, but delegate the workspace and dependency resolution to the package manager underneath. This exists precisely because the JS package managers stopped at item 5 — npm run --workspaces has no topology, so an entire tool category sprang up to add one. turborepo is the lean extreme (one turbo.json, reads the PM's lockfile, hashes every task); nx the maximal (project-graph inference, affected, Nx Cloud); wireit the most minimal (a wireit block inside the existing package.json, no new binary). The bet — "overlay a graph on the PM you already use" — is itself a lesson for dub: the proposal can add a topological loop without rebuilding resolution, because dub already owns the resolver.
Minimalist task runners (just, task, make, mise, redo, tup) own a deps DAG and namespacing and deliberately delegate everything else. They are universal polyglot front doors — make's 1976 mtime engine, task's declarative YAML, just's parameterised recipes — with no package model, no isolation, and (mostly) no content cache. redo and tup are the research-grade outliers: tup discovers the dependency graph by intercepting real syscalls and updates only the change-reachable slice. These are what dub users reach for today to glue members together (the Sparkles apps/ci helper is this pattern in hand-written D).
Container / CI engines (dagger, earthly, garden) make the container the unit of reproducibility: every step runs in a BuildKit container, the graph is an implicit data-flow DAG that caches and parallelises automatically, and earthly's +target references reach across files and even git repositories. The bet trades fine-grained incrementality for environmental hermeticity — and is orthogonal to dub (it would wrap a dub build, not replace it). Note earthly is now frozen/unmaintained (mid-2025).
Remote-execution backends (buildbuddy, buildbarn, nativelink) own only the server half — a CAS + ActionCache + scheduler + worker fleet speaking REAPI v2 — and nothing above the action. The workspace, members, and dependency DAG live entirely on the client (bazel/buck2); the backend only ever sees hashed, post-analysis actions. nativelink's Nix-pinned Local Remote Execution makes local and remote toolchains bit-for-bit identical. These are irrelevant to dub until it has a content-addressed action graph to feed them — a post-proposal horizon.
Polyglot glue (nix-flakes) pins and wires what no single ecosystem tool can see whole: each flake.nix is a content-hash-locked node, relative path: inputs reference sibling flakes, and flake.lock pins the whole transitive DAG. Sparkles already uses this (the Nix flake, nix/dub-lock.json) as the layer above dub — the glue that does what dub cannot. The proposal's goal is to move workspace concerns intodub so the Nix layer is reproducibility, not orchestration.
The Dub Delta
Measuring dub — read directly from the dlang/dub source at v1.42.0-beta.1 — against The Consensus Standard yields a concrete, addressable gap. dub already owns the two hard primitives a workspace needs — a working dependency resolver and a solid content-addressed local build cache — but it has no organising concept above the single package. A grep of the entire dub source and docs trees for "workspace" or "monorepo" returns zero matches.
| Capability | Consensus (exemplar) | Dub today (baseline) | The gap |
|---|---|---|---|
| Workspace declaration | [workspace] members = ["libs/*"], virtual root (cargo) | subPackages array; root is always a package; no globbing | No [workspace] block, no member globbing, no virtual (non-buildable) root |
| Local cross-references | workspace: protocol; resolve-by-name (yarn-berry, go-work) | manual path="../.." + :subpkg, depth-sensitive | No local-first protocol; hand-maintained relative paths that drift vs. published |
| Unified lockfile | one root lock for all members (cargo, pnpm) | one dub.selections.json per member; no root lock | Fragmented per-member lockfiles; version drift structurally possible |
| Config / version inheritance | version.workspace = true, [workspace.dependencies] (cargo) | none (only the obscure inheritable selections flag) | No metadata or dependency-version inheritance from a root |
| Cross-member task DAG | topological foreach -t (yarn-berry), dependsOn (turborepo) | none; per-package verbs + bespoke apps/ci scripts | No dub build --workspace; orchestration is uncoordinated external scripts |
| Filter / slicing | --filter, -p, --recursive, --since (pnpm, nx) | :subpkg, --root, one-off upgrade -s | No multi-member selection or change-based slicing |
| Change detection | input-hash + --since affected (nx, moon) | content-addressed per-package build cache, no --since | No workspace-aware affected slicing |
| Local caching | content-addressed task-output cache (turborepo) | content-addressed computeBuildID package cache (real, good) | Per-package, not workspace-aware; configs produce distinct ids → redundant recompiles |
| Remote cache / execution | remote cache (turborepo) → REAPI (bazel) | none — local $DUB_HOME only | No remote cache, no REAPI; CI cannot pull a teammate's artifacts |
The deficits cluster into four addressable themes, which are exactly the milestone structure of the proposal:
- Structural workspace layout — a
[workspace]block (root-package and virtual modes) with glob member discovery and a unified rootdub.selections.json, borrowing thecargomodel. (Proposal Milestone 1.) - Config & dependency inheritance — a
workspace:-style local cross-reference protocol and a central[workspace.dependencies]registry, eliminating the depth-sensitivepath="../.."wiring and version drift. (Milestone 2, modelled onyarn-berry+cargo.) - Topological multi-member task routing — a
dub build --workspace/dub test -p <member>loop usingdub's existing resolver topology, with filter ergonomics and-j/--parallelconcurrency, inspired byyarn workspaces foreach. (Milestone 3.) - Change tracking & remote caching —
--since <ref>git-diff slicing and a workspace constraints engine, thenx/lernaaffected model. (Milestone 4.)
The honest summary: every multi-package capability in Sparkles today — looping tests across the five members, keeping expected/silly aligned, building dependents after their local libraries — is bolted on outside dub (in apps/ci, in the Nix flake, in hand-maintained path= strings and per-member lockfiles). dub has the resolver and the cache; it lacks the workspace. The cross-tool evidence above shows the consensus shape that workspace should take, and the proposal is the milestoned plan to build it.
Sources
- Cross-cutting concept doc: concepts (workspace topology, isolation, task DAG, caching, lockfiles — the layered vocabulary this synthesis re-cuts by axis).
- The system under improvement:
dubbaseline (read from thedlang/dubsource atv1.42.0-beta.1) and the proposal it bridges into. - Per-tool primary sources are cited in each deep-dive; this page synthesises the 44-tool catalog.
- Structural model: async-io's comparison (the synthesis-doc pattern this imitates).