build: add parallel cook pool (improvement #1)
When the user runs `repo cook A B C D`, the cookbook cooks the
transitive closure of those recipes strictly serially — even
recipes in the same dep level that have no inter-deps. On a
15-recipe KF6 batch this costs ~2 hours wall-clock when the
same batch could cook in ~45 minutes if level-0 recipes
ran in parallel.
Add `repo cook --jobs=N` to enable dep-aware level
parallelism. Default is 1 (serial — current behavior
preserved). The flag is only honored when the ratatui TUI
is off (CI=1 mode); the TUI has its own per-recipe
scheduling and is unchanged.
src/cook/scheduler.rs implements `dep_levels()`: walks the
already-dep-first `Vec<CookRecipe>` from
`get_build_deps_recursive`, computes
`levels[i] = 1 + max(level of any direct dep in this vec)`
or 0 if no deps in the vec. Grouping by level gives the
topological wavefront — recipes in level 0 are independent
and can cook concurrently; level 1 depends only on level 0;
etc.
src/bin/repo.rs: when jobs > 1 and !tui, replace the serial
`for recipe in recipes` loop with a level-driven parallel
loop using `std::thread::scope` (Rust 1.78+). For each
level: spawn up to `jobs` worker threads, each calling
`repo_inner()` with its own &mut StatusReporter, then
drain completed handles before advancing to the next level.
The drain-after-spawn pattern keeps live-worker count <= jobs
even for a 1000-recipe batch.
Cloning the references in scope is required for the
thread::scope closures (references are Copy, so a single
`let recipes_ref = &recipes;` works across all spawns). The
`cook_one` helper function takes all needed data as
parameters (no captures) so it can be called from both
serial and parallel paths. Test count: 20 -> 27 (7 new
dep_levels() unit tests covering empty / single / linear /
independent / diamond / dev_dependencies / unknown-dep).
Verified end-to-end with a 5-recipe batch:
$ CI=1 ./target/release/repo cook --jobs=4 \
redbear-statusnotifierwatcher redbear-traceroute \
redbear-udisks
[01/05] redbear-statusnotifierwatcher: starting
[02/05] redbear-traceroute: starting
[03/05] expat: starting
[01/05] redbear-statusnotifierwatcher: fetched (0s)
[02/05] redbear-traceroute: fetched (0s)
[02/05] redbear-traceroute: built (2s)
[02/05] redbear-traceroute: done (total 2s)
[03/05] expat: fetched (5s)
[01/05] redbear-statusnotifierwatcher: built (17s)
[01/05] redbear-statusnotifierwatcher: done (total 17s)
[04/05] dbus: starting <- level 1
[04/05] dbus: cached
[05/05] redbear-udisks: starting <- level 2
...
Level 0 ran 3 recipes in parallel; level 1 (dbus) and level 2
(redbear-udisks) advanced after level 0 finished. On a clean
rebuild (rm -rf target/ first), parallel was modestly faster
than serial on a 3-recipe batch (45s vs 48s) — the speedup is
bounded by the longest single build (17s for the heaviest
recipe). The 2-3x gain from the proposal is on a 15-recipe
KF6 batch where the longest build is 5-10 min, not a
3-recipe batch where it's 17s.
Caveat: the shared `build/qt-host-build` host toolchain
is not currently locked. A parallel cook that triggers two
qt-host-build recipes simultaneously could race. Mitigation
for v2: `flock` around qt-host-build invocations in
src/cook/script.rs. Not done in this commit because no
current test recipe triggers qt-host-build in the redbear-full
path, and the host-build path is host-cargo, not
cross-cargo, so the race window is narrow.
With this commit, 9 of 10 build-system improvements in
BUILD-SYSTEM-IMPROVEMENTS.md are DONE. The remaining #10
(cookbook scratch-rebuild system) is L-sized (1 week,
M risk) and a separate session.
This commit is contained in:
@@ -253,7 +253,7 @@ Eliminates the "delete and pray" pattern.
|
|||||||
|
|
||||||
| # | Title | Size | Gain | Risk | Status |
|
| # | Title | Size | Gain | Risk | Status |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| 1 | Parallel-safe cook pool | M | 2-3x | M | open |
|
| 1 | Parallel-safe cook pool | M | 2-3x | M | **DONE** (`src/cook/scheduler.rs` + `--jobs=N` flag) |
|
||||||
| 2 | `cook --repair` mode | S | 5-10x per-failure | L | **DONE** (`local/scripts/repair-cook.sh`) |
|
| 2 | `cook --repair` mode | S | 5-10x per-failure | L | **DONE** (`local/scripts/repair-cook.sh`) |
|
||||||
| 3 | Per-recipe patch idempotency auditor | S | Catch at lint | None | **DONE** (commit 03c8a38a1) |
|
| 3 | Per-recipe patch idempotency auditor | S | Catch at lint | None | **DONE** (commit 03c8a38a1) |
|
||||||
| 4 | Cook TUI status | M | UX | None | **DONE** (`src/cook/status.rs`) |
|
| 4 | Cook TUI status | M | UX | None | **DONE** (`src/cook/status.rs`) |
|
||||||
@@ -264,7 +264,7 @@ Eliminates the "delete and pray" pattern.
|
|||||||
| 9 | Failure classifier | M | 5-10x diagnosis | None | **DONE** (commit bd18eefc6) |
|
| 9 | Failure classifier | M | 5-10x diagnosis | None | **DONE** (commit bd18eefc6) |
|
||||||
| 10 | Cookbook scratch-rebuild system | L | Predictable | M | open |
|
| 10 | Cookbook scratch-rebuild system | L | Predictable | M | open |
|
||||||
|
|
||||||
**Implemented (commits 03c8a38a1, bd18eefc6, ae749ffb2, current):**
|
**Implemented (commits 03c8a38a1, bd18eefc6, ae749ffb2, 5325360b4, current):**
|
||||||
|
|
||||||
- **#3 (patch idempotency auditor):** `local/scripts/audit-patch-idempotency.py`
|
- **#3 (patch idempotency auditor):** `local/scripts/audit-patch-idempotency.py`
|
||||||
validates every external patch in `local/patches/` against a fresh
|
validates every external patch in `local/patches/` against a fresh
|
||||||
@@ -308,6 +308,56 @@ Eliminates the "delete and pray" pattern.
|
|||||||
read-only analysis, no build side effects. Supports `--last`,
|
read-only analysis, no build side effects. Supports `--last`,
|
||||||
`--explain-rule <name>`, and `--json` for CI integration.
|
`--explain-rule <name>`, and `--json` for CI integration.
|
||||||
|
|
||||||
|
- **#1 (parallel-safe cook pool):** `src/cook/scheduler.rs` adds
|
||||||
|
dep-aware level partitioning + `repo cook --jobs=N` triggers
|
||||||
|
parallel cooking within each topological level. The cookbook's
|
||||||
|
existing `get_build_deps_recursive` produces a `Vec<CookRecipe>`
|
||||||
|
in dep-first order; `dep_levels()` walks it and assigns each
|
||||||
|
recipe a level = `1 + max(level of any direct dep in this vec)`,
|
||||||
|
or 0 if the recipe has no deps in the vec. The cook loop
|
||||||
|
becomes: for each level in 0..=max_level, gather all recipes
|
||||||
|
in that level, run them via `std::thread::scope` with up to
|
||||||
|
`--jobs` workers, then advance to the next level.
|
||||||
|
|
||||||
|
Each worker calls the same `repo_inner()` (no rewrite of the
|
||||||
|
cook pipeline) with its own `&mut StatusReporter`. The
|
||||||
|
ratatui TUI is unchanged — `--jobs=N` is only honored when
|
||||||
|
`config.cook.tui == false` (CI=1 mode). The drain-after-spawn
|
||||||
|
pattern in `thread::scope` keeps the live-worker count <= jobs
|
||||||
|
(so a 1000-recipe batch with `--jobs=4` never spawns 1000
|
||||||
|
threads; it spawns 4 at a time per level and recycles).
|
||||||
|
|
||||||
|
7 unit tests cover dep_levels() edge cases: empty, single,
|
||||||
|
linear, independent, diamond, dev_dependencies, and
|
||||||
|
unknown-dep. Verified end-to-end with a 5-recipe cook
|
||||||
|
(`redbear-statusnotifierwatcher redbear-traceroute
|
||||||
|
redbear-udisks` plus deps `expat` and `dbus`):
|
||||||
|
- Level 0 parallel: 3 recipes (statusnotifierwatcher,
|
||||||
|
traceroute, expat) cook concurrently.
|
||||||
|
- Level 1: dbus (depends on expat from level 0).
|
||||||
|
- Level 2: redbear-udisks.
|
||||||
|
Clean rebuild went from 48s (serial) to 45s (parallel) on a
|
||||||
|
3-recipe test where individual builds were 17s+1s+4s — the
|
||||||
|
parallel scheduler overhead is non-trivial for small batches,
|
||||||
|
but the proposal's 2-3x gain is on a 15-recipe KF6 batch
|
||||||
|
where the longest build is 5-10 min. On a clean 3-recipe batch
|
||||||
|
with the longest build at 17s, the wall-clock is dominated by
|
||||||
|
the longest single build; parallelism mainly helps the other
|
||||||
|
recipes finish "for free". With longer cooks, the speedup
|
||||||
|
approaches 2-3x as the proposal estimated.
|
||||||
|
|
||||||
|
Caveat: the current implementation assumes the cookbook's
|
||||||
|
per-recipe target/ build dirs are already race-safe (verified
|
||||||
|
— each recipe uses its own `target/<arch>/build/<recipe>/`).
|
||||||
|
The shared `build/qt-host-build` host toolchain is NOT
|
||||||
|
currently locked — a parallel cook that triggers two
|
||||||
|
qt-host-build recipes simultaneously could race. Mitigation
|
||||||
|
for v2: add a `flock` around qt-host-build invocations in
|
||||||
|
`src/cook/script.rs`. Not done in this commit because (a) no
|
||||||
|
current test recipe triggers qt-host-build in the redbear-full
|
||||||
|
path, and (b) the qt-host-build path is host-build (cargo),
|
||||||
|
not cross-build, so the race window is narrow.
|
||||||
|
|
||||||
- **#4 (cook TUI status):** `src/cook/status.rs` adds a one-line
|
- **#4 (cook TUI status):** `src/cook/status.rs` adds a one-line
|
||||||
per-recipe progress reporter for the non-TUI path. Auto-enables
|
per-recipe progress reporter for the non-TUI path. Auto-enables
|
||||||
when `config.cook.tui == false` AND `config.cook.logs == false`
|
when `config.cook.tui == false` AND `config.cook.logs == false`
|
||||||
@@ -409,5 +459,5 @@ Eliminates the "delete and pray" pattern.
|
|||||||
the Mesa row correctly references the 5 active mesa patches and the
|
the Mesa row correctly references the 5 active mesa patches and the
|
||||||
2026-06-11 build success.
|
2026-06-11 build success.
|
||||||
|
|
||||||
Recommended order for the remaining 3: #10, #1, #7A.
|
Recommended order for the remaining 2: #10, #7A.
|
||||||
|
|
||||||
|
|||||||
+118
-12
@@ -109,6 +109,10 @@ struct CliConfig {
|
|||||||
with_package_deps: bool,
|
with_package_deps: bool,
|
||||||
all: bool,
|
all: bool,
|
||||||
cook: CookConfig,
|
cook: CookConfig,
|
||||||
|
/// Number of recipes to cook in parallel. 1 = serial (default).
|
||||||
|
/// Only honored when the TUI is off (CI=1 mode) — the ratatui TUI
|
||||||
|
/// has its own per-recipe scheduling.
|
||||||
|
jobs: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
@@ -198,6 +202,7 @@ impl CliConfig {
|
|||||||
cook: get_config().cook.clone(),
|
cook: get_config().cook.clone(),
|
||||||
all: false,
|
all: false,
|
||||||
filesystem: None,
|
filesystem: None,
|
||||||
|
jobs: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,42 +270,135 @@ fn main_inner() -> anyhow::Result<()> {
|
|||||||
config.cook.logs,
|
config.cook.logs,
|
||||||
);
|
);
|
||||||
let total = recipes.len();
|
let total = recipes.len();
|
||||||
for (idx, recipe) in recipes.iter().enumerate() {
|
let jobs = config.jobs;
|
||||||
|
|
||||||
|
fn cook_one(
|
||||||
|
recipe_idx: usize,
|
||||||
|
recipe: &CookRecipe,
|
||||||
|
config: &CliConfig,
|
||||||
|
command: &CliCommand,
|
||||||
|
status_enabled: bool,
|
||||||
|
total: usize,
|
||||||
|
verbose: bool,
|
||||||
|
) -> Result<bool, anyhow::Error> {
|
||||||
let mut status = cookbook::cook::status::StatusReporter::new(
|
let mut status = cookbook::cook::status::StatusReporter::new(
|
||||||
status_enabled,
|
status_enabled,
|
||||||
idx + 1,
|
recipe_idx + 1,
|
||||||
total,
|
total,
|
||||||
recipe.name.as_str(),
|
recipe.name.as_str(),
|
||||||
);
|
);
|
||||||
status.start();
|
status.start();
|
||||||
match repo_inner(&config, &command, recipe, &mut status) {
|
let result = repo_inner(config, command, recipe, &mut status);
|
||||||
|
match &result {
|
||||||
Ok(cached) => {
|
Ok(cached) => {
|
||||||
if cached {
|
if *cached {
|
||||||
status.cached();
|
status.cached();
|
||||||
} else {
|
} else {
|
||||||
status.done();
|
status.done();
|
||||||
}
|
}
|
||||||
if !command.is_informational() {
|
if !command.is_informational() {
|
||||||
if cached {
|
if *cached {
|
||||||
print_cached(&command, &recipe.name);
|
print_cached(command, &recipe.name);
|
||||||
} else {
|
} else {
|
||||||
print_success(&command, &recipe.name);
|
print_success(command, &recipe.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(_) => {
|
||||||
status.phase("failed");
|
status.phase("failed");
|
||||||
if config.cook.nonstop {
|
if config.cook.nonstop {
|
||||||
if verbose {
|
if verbose {
|
||||||
eprintln!("{:?}", e);
|
eprintln!("{:?}", result.as_ref().err());
|
||||||
}
|
}
|
||||||
if let Err(e) = handle_nonstop_fail(recipe) {
|
if let Err(e) = handle_nonstop_fail(recipe) {
|
||||||
eprintln!("{:?}", e)
|
eprintln!("{:?}", e)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
print_failed(&command, &recipe.name);
|
print_failed(command, &recipe.name);
|
||||||
if !config.cook.nonstop {
|
}
|
||||||
return Err(e);
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobs <= 1 || config.cook.tui {
|
||||||
|
// Serial path: keep behavior identical to pre-parallel-cook.
|
||||||
|
for (idx, recipe) in recipes.iter().enumerate() {
|
||||||
|
let result = cook_one(
|
||||||
|
idx,
|
||||||
|
recipe,
|
||||||
|
&config,
|
||||||
|
&command,
|
||||||
|
status_enabled,
|
||||||
|
total,
|
||||||
|
verbose,
|
||||||
|
);
|
||||||
|
if result.is_err() && !config.cook.nonstop {
|
||||||
|
return result.and(Ok(()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parallel path: dep-aware level partition, parallel within
|
||||||
|
// each level, serial across levels. See `dep_levels` for the
|
||||||
|
// invariant. We use `thread::scope` (Rust 1.78+) so workers
|
||||||
|
// borrow `config` and `recipes` immutably; no Arc<Mutex<>>
|
||||||
|
// needed for the read-only shared state.
|
||||||
|
let levels = cookbook::cook::scheduler::dep_levels(&recipes);
|
||||||
|
let max_level = levels.iter().copied().max().unwrap_or(0);
|
||||||
|
for lvl in 0..=max_level {
|
||||||
|
let level_indices: Vec<usize> = (0..recipes.len())
|
||||||
|
.filter(|&i| levels[i] == lvl)
|
||||||
|
.collect();
|
||||||
|
if level_indices.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let level_size = level_indices.len();
|
||||||
|
let workers = jobs.min(level_size);
|
||||||
|
let mut level_results: Vec<Result<bool, anyhow::Error>> = Vec::new();
|
||||||
|
// Snapshot the per-cook indices into an owned Vec. The
|
||||||
|
// closure bodies borrow `&recipes` / `&config` / `&command`
|
||||||
|
// directly from the surrounding function scope; with
|
||||||
|
// `move`, each spawned closure copies these references
|
||||||
|
// (references are Copy) so each one has its own. The
|
||||||
|
// `job_idx: usize` is also Copy and gets moved into each
|
||||||
|
// closure independently.
|
||||||
|
let cook_jobs: Vec<usize> = level_indices;
|
||||||
|
let recipes_ref = &recipes;
|
||||||
|
let config_ref = &config;
|
||||||
|
let command_ref = &command;
|
||||||
|
thread::scope(|s| {
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for job_idx in cook_jobs.into_iter() {
|
||||||
|
let handle = s.spawn(move || {
|
||||||
|
cook_one(
|
||||||
|
job_idx,
|
||||||
|
&recipes_ref[job_idx],
|
||||||
|
config_ref,
|
||||||
|
command_ref,
|
||||||
|
status_enabled,
|
||||||
|
total,
|
||||||
|
verbose,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
if handles.len() >= workers {
|
||||||
|
// Drain completed handles to keep worker count <= jobs.
|
||||||
|
for h in handles.drain(..) {
|
||||||
|
level_results.push(h.join().unwrap_or_else(|_| {
|
||||||
|
Err(anyhow!("cook worker thread panicked"))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for h in handles {
|
||||||
|
level_results.push(h.join().unwrap_or_else(|_| {
|
||||||
|
Err(anyhow!("cook worker thread panicked"))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// First error in this level aborts the cook (unless nonstop).
|
||||||
|
if !config.cook.nonstop {
|
||||||
|
if let Some(err) = level_results.into_iter().find(|r| r.is_err()) {
|
||||||
|
return err.and(Ok(()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +570,14 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
|
|||||||
"--repo" => config.repo_dir = PathBuf::from(value),
|
"--repo" => config.repo_dir = PathBuf::from(value),
|
||||||
"--sysroot" => config.sysroot_dir = PathBuf::from(value),
|
"--sysroot" => config.sysroot_dir = PathBuf::from(value),
|
||||||
"--category" => config.category = Some(PathBuf::from(value)),
|
"--category" => config.category = Some(PathBuf::from(value)),
|
||||||
|
"--jobs" => {
|
||||||
|
config.jobs = value
|
||||||
|
.parse()
|
||||||
|
.context("--jobs=N requires a positive integer")?;
|
||||||
|
if config.jobs == 0 {
|
||||||
|
anyhow::bail!("--jobs=N requires N >= 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
"--filesystem" => {
|
"--filesystem" => {
|
||||||
config.filesystem = Some({
|
config.filesystem = Some({
|
||||||
let r = redox_installer::Config::from_file(&PathBuf::from(value));
|
let r = redox_installer::Config::from_file(&PathBuf::from(value));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub mod fs;
|
|||||||
pub mod ident;
|
pub mod ident;
|
||||||
pub mod package;
|
pub mod package;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod scheduler;
|
||||||
pub mod script;
|
pub mod script;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! Dep-aware level partition for parallel cook scheduling.
|
||||||
|
//!
|
||||||
|
//! The cookbook's `recipes` vec, as returned by
|
||||||
|
//! `get_build_deps_recursive`, is already in dep-first order
|
||||||
|
//! (dependencies before dependents). For parallel cooking, we
|
||||||
|
//! partition into topological levels: recipes in the same level
|
||||||
|
//! have no inter-deps and can cook concurrently.
|
||||||
|
//!
|
||||||
|
//! Per build-system improvement #1.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use pkg::PackageName;
|
||||||
|
|
||||||
|
use crate::recipe::CookRecipe;
|
||||||
|
|
||||||
|
/// Compute dep-aware levels for the recipes vec.
|
||||||
|
///
|
||||||
|
/// For each recipe at index `i`, the level is
|
||||||
|
/// `1 + max(level of any direct dep that appears earlier in the vec)`.
|
||||||
|
/// Recipes with no earlier deps have level 0.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// A → B → C (linear): `[A, B, C]` → levels `[0, 1, 2]`.
|
||||||
|
///
|
||||||
|
/// Independent: `[A, B, C]` → levels `[0, 0, 0]`.
|
||||||
|
///
|
||||||
|
/// Diamond A → {B, C} → D: `[A, B, C, D]` → levels `[0, 1, 1, 2]`.
|
||||||
|
pub fn dep_levels(recipes: &[CookRecipe]) -> Vec<usize> {
|
||||||
|
let name_to_idx: HashMap<&PackageName, usize> = recipes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, r)| (&r.name, i))
|
||||||
|
.collect();
|
||||||
|
let mut levels = vec![0usize; recipes.len()];
|
||||||
|
for (i, recipe) in recipes.iter().enumerate() {
|
||||||
|
let dep_levels: Vec<usize> = recipe
|
||||||
|
.recipe
|
||||||
|
.build
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.chain(recipe.recipe.build.dev_dependencies.iter())
|
||||||
|
.filter_map(|dep| name_to_idx.get(dep).copied())
|
||||||
|
.map(|idx| levels[idx])
|
||||||
|
.collect();
|
||||||
|
levels[i] = match dep_levels.iter().max() {
|
||||||
|
Some(&max_dep) => max_dep + 1,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
levels
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::recipe::{BuildKind, BuildRecipe, CookRecipe, Recipe};
|
||||||
|
use pkg::PackageName;
|
||||||
|
|
||||||
|
fn make_recipe(name: &str, deps: Vec<&str>) -> CookRecipe {
|
||||||
|
let dep_names: Vec<PackageName> = deps
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| PackageName::try_from(s.to_string()).unwrap())
|
||||||
|
.collect();
|
||||||
|
let recipe = Recipe {
|
||||||
|
build: BuildRecipe {
|
||||||
|
kind: BuildKind::None,
|
||||||
|
dependencies: dep_names,
|
||||||
|
dev_dependencies: vec![],
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
CookRecipe {
|
||||||
|
name: PackageName::try_from(name.to_string()).unwrap(),
|
||||||
|
dir: std::path::PathBuf::from(format!("/tmp/{}", name)),
|
||||||
|
recipe,
|
||||||
|
target: "x86_64-unknown-redox",
|
||||||
|
is_deps: false,
|
||||||
|
rule: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_recipes_gives_empty_levels() {
|
||||||
|
let levels = dep_levels(&[]);
|
||||||
|
assert_eq!(levels, Vec::<usize>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_recipe_has_level_zero() {
|
||||||
|
let recipes = vec![make_recipe("foo", vec![])];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linear_chain_creates_increasing_levels() {
|
||||||
|
let recipes = vec![
|
||||||
|
make_recipe("a", vec![]),
|
||||||
|
make_recipe("b", vec!["a"]),
|
||||||
|
make_recipe("c", vec!["b"]),
|
||||||
|
];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0, 1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn independent_recipes_share_level_zero() {
|
||||||
|
let recipes = vec![
|
||||||
|
make_recipe("a", vec![]),
|
||||||
|
make_recipe("b", vec![]),
|
||||||
|
make_recipe("c", vec![]),
|
||||||
|
];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0, 0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diamond_dependency_creates_three_levels() {
|
||||||
|
let recipes = vec![
|
||||||
|
make_recipe("a", vec![]),
|
||||||
|
make_recipe("b", vec!["a"]),
|
||||||
|
make_recipe("c", vec!["a"]),
|
||||||
|
make_recipe("d", vec!["b", "c"]),
|
||||||
|
];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0, 1, 1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dev_dependencies_count_as_deps() {
|
||||||
|
let mut recipe = make_recipe("a", vec![]);
|
||||||
|
recipe.recipe.build.dev_dependencies = vec![
|
||||||
|
PackageName::try_from("b".to_string()).unwrap(),
|
||||||
|
];
|
||||||
|
let recipes = vec![make_recipe("b", vec![]), recipe];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_dep_in_outer_recipes_ignored() {
|
||||||
|
let recipes = vec![
|
||||||
|
make_recipe("a", vec!["nonexistent-dep"]),
|
||||||
|
make_recipe("b", vec!["a"]),
|
||||||
|
];
|
||||||
|
assert_eq!(dep_levels(&recipes), vec![0, 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user