# nested_workspace
Run Cargo commands on workspaces in workspaces
Nested Workspace supports the following Cargo subcommands directly:
- `cargo build`
- `cargo check`
- `cargo test`
Additional Cargo subcommands are supported via the `nested` subcommand, installed with the following command:
```sh
cargo install cargo-nested
```
For example, the follow command runs `cargo clean` on the current package or workspace and each nested workspace:
```sh
cargo nested clean
```
**Note:** `cargo nested build` and `cargo nested test` should also work. However, they may result in extra calls to `cargo build` and `cargo test` (respectively) if direct support for these commands is configured (as describe next).
## Usage
Nested Workspace requires that each nested workspace appear under a _containing package_ as follows ([example]):
```
containing package
├─ nested workspace A
└─ nested workspace B
```
Furthermore, the following steps are required:
1. In the containing package's Cargo.toml file, create a `nest_workspace` metadata table. The table should contain a `roots` array with the name of each nested workspace. Example:
```toml
[package.metadata.nested_workspace]
roots = [
"nested_workspace_a",
"nested_workspace_b",
...
]
```
2. To enable direct support for `cargo build` and `cargo check`, add `nested_workspace` as `build-dependency` to the containing package's Cargo.toml:
```toml
[build-dependencies]
nested_workspace = "*"
```
And create a build script (`build.rs`) with the following contents:
```rs
fn main() {
nested_workspace::build().unwrap();
}
```
3. To enable direct support for `cargo test`, add `nested_workspace` as `dev-dependency` to the containing package's Cargo.toml:
```toml
[dev-dependencies]
nested_workspace = "*"
```
And create a test like the following:
```rs
#[test]
fn nested_workspace() {
nested_workspace::test().unwrap();
}
```
## Argument handling
### `cargo build` and `cargo check`
All arguments are filtered out; no arguments are forwarded. However, the commands are called with `-vv`, `--offline`, and `--workspace`:
- `-vv` aids in debugging.
- `--offline` avoids potential deadlocks (see [Known problem] below).
- `--workspace` ensures all packages in a nested workspace are built/checked, even if a nested workspace contains a root package.
### `cargo test`
The following modifications are made:
- `-p <containing-package>` and `--package <containing-package>` are filtered out.
- All arguments besides those covered by the previous bullet are forwarded.
- `--workspace` is added to the arguments so that all packages in a nested workspace are tested, even if a nested workspace contains a root package.
### `cargo nested <subcommand>`
All arguments are forwarded; no arguments are filtered out or added.
A primary reason for this policy is that the arguments accepted by an arbitrary subcommand cannot be predicted. For example, a subcommand might not accept `--workspace`, or it might consider `-p` to mean something other than "package".
## Known problem: potential deadlocks
Nested Workspace has safeguards to avoid potential deadlocks.
A build script holds a lock on the build directory while running. Furthermore, `cargo check` tries to obtain a lock on the package cache unless `--offline` is passed. Thus, the following scenario could occur:
- Thread A runs `cargo check`, which locks the package cache, locks the build directory, and then releases the lock on the package cache.
- Thread B runs `cargo check`, which locks the package cache and tries to lock the build directory, but blocks because thread A holds the lock.
- Thread A runs the build script, which runs `cargo check` and tries to lock the package cache, but blocks because thread B holds the lock.
To avoid this scenario, Nested Workspace checks whether `--offline` was passed to the parent command (i.e., the Cargo command that caused the build script to be run). If not, Nested Workspace exits with a warning like the following:
```
Refusing to check as `--offline` was not passed to parent command
```
Thus, in the scenario above, thread A would not hold a lock on the package cache, thereby avoiding the deadlock.
## Git dependencies
Using `cargo check --offline` with Git dependencies can result in errors like the following:
```
error: failed to get `clippy_utils` as a dependency of ...
...
Caused by:
can't checkout from 'https://github.com/rust-lang/rust-clippy': you are in the offline mode (--offline)
```
To avoid such errors, we recommend running `cargo nested fetch` beforehand, e.g.:
```sh
cargo nested fetch && cargo check --offline
```
## Why would one need multiple workspaces?
- **Multiple toolchains:** Cargo builds all targets in workspace [with the same toolchain]. If a project needs multiple toolchains, then multiple workspaces are needed. ([Dylint] is an example of such a project.)
- **Conflicting features:** Cargo performs [feature unification] across the packages in a workspace. Features are meant to be additive, but some packages have conflicting features ([`gix-transport`] is an example). Multiple workspaces can be used to build targets with features that conflict.
## Why aren't more subcommands supported directly?
Nested Workspace needs a _trigger_ to run a subcommand:
- For `cargo build` and `cargo check`, the trigger is a build script containing `nested_workspace::build()`.
- For `cargo test`, the trigger is a test containing `nested_workspace::test()`.
For other subcommands, there is no obvious trigger. Hence, other subcommands must be run with `cargo nested <subcommand>`.
[Dylint]: https://github.com/trailofbits/dylint
[Known problem]: #known-problem-potential-deadlocks
[`gix-transport`]: https://github.com/GitoxideLabs/gitoxide/blob/8c353ea00c805604113a567d2f5157be94cc9f28/gix-transport/src/client/blocking_io/http/mod.rs#L25-L26
[example]: ./example
[feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification
[with the same toolchain]: https://github.com/rust-lang/rustup/issues/1399#issuecomment-383376082