A workspace allows multiple related packages to share dependency resolution records and build management entry points. It is suitable for projects where core libraries, command-line programs, and servers evolve together, but it does not automatically eliminate duplicate dependency versions, nor does it require all members to be released together.
When designing splits, prioritize interfaces and responsibilities: should a change in one crate affect another? Can it be tested independently? Does it need to be published externally? Too many small crates also increase maintenance and compilation boundary costs.
A Complete Two-Member Project
Root Cargo.toml:
[workspace]
members = ["crates/calc-core", "crates/demo-cli"]
default-members = ["crates/demo-cli"]
resolver = "3"
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
calc-core = { path = "crates/calc-core", version = "0.1.0" }
The root only has [workspace], making it a virtual manifest. resolver=3 was introduced alongside Rust 2024 and requires Rust/Cargo 1.85 or higher; virtual workspaces have no root package edition to infer from, so it should be explicitly selected. resolver=3 applies appropriate fallback strategies for dependencies incompatible with rust-version, but this does not replace testing with the minimum toolchain. Dependency Resolver
crates/calc-core/Cargo.toml:
[package]
name = "calc-core"
version.workspace = true
edition.workspace = true
cargo run -p demo-clicargo check --workspacecargo test --workspacecargo metadata --no-deps --format-version 1
Here, checked_mul explicitly handles overflow, avoiding the difference in default arithmetic checks between release and debug modes from leaking into the business interface.
Observing Member Selection with Real Library Tests
Append the following test to calc-core/src/lib.rs. The boundaries explicitly use u32::MAX / 2 to verify the maximum doubleable value and the immediately following overflow value, without relying on the default overflow check switch.
When running cargo test at the workspace root, default-members selects only demo-cli. calc-core will be compiled as a dependency, but its library unit tests will not run as a result. Use cargo test -p calc-core or cargo test --workspace to see the boundary test execute. This explains why "the application build depends on the core library" and "the core library's tests are covered" cannot substitute for each other.
A dependency built; did its unit tests run?
The dependency relationship stays unchanged; commands select different packages’ test targets. Default testing uses calc-core through the CLI without executing calc-core’s #[test] functions. Switch to -p calc-core or --workspace to see that coverage change.
Preparing the visual
A dependency built; did its unit tests run?
With default-members=[demo-cli], default tests select the CLI; building calc-core as a dependency does not run its library unit tests.
{"id":"rust-workspace-test-selection","title":"A dependency built; did its unit tests run?","summary":"With default-members=[demo-cli], default tests select the CLI; building calc-core as a dependency does not run its library unit tests.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"default\" data-i18n=\"default\"></button><button data-mode=\"core\" data-i18n=\"core\"></button><button data-mode=\"all\" data-i18n=\"all\"></button></div><svg id=\"map\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><section class=\"panel\"><h3 data-i18n=\"packages\"></h3><div id=\"packages\"></div></section><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"explain\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n.table .tr>*{min-height:4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));let mode='default';function draw(){pressed(mode);let c=mode!=='default',a=mode!=='core';graph('map',[{name:'CLI',x:90,y:100,cls:a?'on':''},{name:'core',x:270,y:100,cls:c?'on':'gold'}],[[0,1,a]]);table('packages',[['', 'demo-cli','calc-core'],[t('selected'),a?'✓':'—',c?'✓':'—'],[t('built'),'—',a?'✓':'—'],[t('unit'),a?'✓':'—',c?'✓':'—']]);set('explanation',t('explain'));}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"all":"test --workspace","built":"Normal dependency build","core":"test -p calc-core","default":"cargo test","explain":"Building a normal library and building/running its test target are separate decisions.","heading":"A dependency built; did its unit tests run?","intro":"Choose a command at a virtual workspace root and compare selected packages with built dependencies.","mode":"Command","packages":"Build and test targets","reset":"Reset","scope":"Two article members with CLI depending on core; shows lib/bin unit-test selection, not every doctest or integration target.","scopeLabel":"Model scope and assumptions","selected":"Selected","unit":"Unit tests run"}}
Inheritance is an Explicit Choice, Not Automatic Root Configuration Override
Members must explicitly select respective inheritance items via workspace=true. The path in workspace.dependencies is resolved relative to the workspace root; a path written directly in a member's dependencies is resolved relative to the member's manifest. When version requirements and path coexist, the local package must also satisfy the requirements, and release semantics involve registry versions.
Members can add features when inheriting dependencies; features enabled in the root cannot be arbitrarily removed by members. Joint declarations reduce duplicate configuration but cannot prevent other transitive dependencies from introducing another incompatible version. Workspaces
Root profiles and patch configurations affect the workspace build; expecting profiles placed in member files to take effect independently usually results in an ignored warning. Lints, package metadata, etc., should also be explicitly selected according to the corresponding inheritance syntax.
Lint Inheritance Also Requires Member Integration
Add to the root manifest:
[workspace.lints.rust]
unsafe_code = "forbid"
Each member that needs to comply with this constraint should add:
[lints]
workspace = true
This is suitable for jointly forbidding unsafe in pure business crates. It does not impose a global policy on dependency source code: external dependencies still have their own compilation contexts, and native libraries are not proven safe by this lint. If a dedicated FFI member indeed bears the unsafe boundary, it should be given explicit checking methods and configuration, rather than mistakenly assuming the root table will unconditionally override all members. Workspace Inherited Configuration
Shared Lock File Does Not Mean Only One Copy of All Dependencies
Cargo.lock can simultaneously record foo 1.x and foo 2.x. Types from different package versions have different identities; even if named struct fields are similar, they may not be interchangeable. cargo tree -d can help discover these situations, and cargo tree -i can continue to find the sources of references.
The target directory is only a shared artifact location. Differences in target architecture, profile, feature sets, host build dependencies, and compilation options can all produce different compilation units. You cannot deduce that all dependencies are compiled only once from the concept of a "shared cache."
Command Scope Must Be Clearly Stated
When not specifying -p at the virtual workspace root, default-members controls the default member selection; --workspace explicitly selects all members. For specific packages, use -p; if a package has multiple binaries, use --bin.
The test targets, doc tests, and regular builds for cargo test --workspace are not exactly the same configuration. Release checks should also be performed on the specific packages prepared for release; a successful workspace build does not mean that each package packaged independently contains the correct files and resolvable dependencies.
Command (at workspace root)
Actively Selects What
Cannot Infer What From This
cargo check
Default member demo-cli
All targets of other members have been checked
cargo test -p calc-core --lib
Core library unit test target
CLI, integration tests, and doc tests have run
cargo test --workspace
Default test sets of each member
Every feature and target platform has passed
cargo check --workspace --all-targets
Members' lib, bin, test, example, and benchmark targets
Doc tests have executed or binaries have finished linking
cargo test --workspace --exclude demo-cli
Excludes CLI when selecting members
Code depended on by other packages will never be built
Here, Cargo targets (lib/bin/test, etc.) and the platform triple specified by --target are two different dimensions. When troubleshooting "why is this package still being compiled," first check if it appears as a dependency; when troubleshooting "why is this test missing," then check if the package and test target were selected. Test Target Selection
patch is Adding Candidate Packages to a Source
Assume the codec package in the registry needs a local fix; the root manifest can write:
This requires vendor/codec to truly contain a package named codec whose version satisfies the dependency constraints. patch does not unconditionally override all versions with the same name; the lock file and candidate version selection may cause it not to be adopted. Check Cargo prompts, cargo tree, and the lock file, and update the resolution for that package if necessary.
It is also not the case that "all dependency requirements must be exactly the same" to use it: multiple compatible ranges can resolve to the same version, and incompatible versions may continue to coexist. Replacing another major version requires providing suitable candidates separately, rather than just changing the version number in the manifest without maintaining the API.
Patches declared by the library itself do not automatically become the root configuration for downstream applications. If it is only for local verification of the fix, record the source of use and removal conditions; if publishing, ensure that downstream parties can obtain the required version from the official dependency source. Overriding Dependencies
Reduce Build Coupling with Boundaries
The core library should expose stable types and clear errors; the CLI is responsible for arguments and output; do not bring the network runtime and build generator into all members just to share a constant. Public dependency re-exports, feature unions, and build.rs outputs all expand coupling and should be evaluated together when modifying interfaces.
Responsibilities for generated code and native builds are seen in Cargo.toml, and verification of conditional branches is seen in cfg.
Independent Packaging Exposes Dependencies Invisible Within the Workspace
First, practice local packaging with calc-core without external dependencies:
The first command checks which files will be included; the second generates the .crate and performs unpacking build verification by default. When checking the list, focus on templates, header files, and configuration examples required for runtime or build, rather than just looking at whether src exists. If build.rs depends on adjacent directories outside the workspace, the build within the repository may succeed, but after independent packaging, those inputs will be missing.
When packaging, Cargo normalizes the manifest, processes inheritance fields, and removes root configurations like workspace and patch. For demo-cli, the path of calc-core only resolves the local development location; version describes the registry dependency; formal distribution must ensure that the required version is available at the corresponding source. Placing two packages in the same repository does not allow the released CLI to automatically access that relative path. cargo package Processing Steps
If these members are only for internal joint building, you can set publish=false in the respective packages to prevent accidental publishing; this does not affect normal local path dependencies. If publishing separately, determine compatible versions around the core library's public API and update the requirements of the consumers. Unified inheritance of version is just unifying the filled value; it does not automatically analyze semantic compatibility or arrange release order.
When Onboarding New Members, Check Along the Actual Usage Chain
After adding a service package, first confirm whether workspace_members and workspace_default_members in cargo metadata match expectations; then check whether its normal dependencies on the core library, test-only dependencies, and build dependencies are placed in the correct tables. Finally, run an end-to-end use case from the user's actual entry point, for example, converting CLI arguments to business values, calling doubled, and converting None to a understandable error.
Directory splitting only creates value corresponding to interface boundaries. The core library returning Option is the complete contract for this small example; if you need to distinguish illegal input, overflow, and resource errors in the future, you should upgrade to explicit error types and let each entry point be responsible for display. Avoid having the core layer directly print or exit the process, otherwise, web services and tests, even if they reuse the function, cannot freely decide on error response methods.