Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
rustflags = ["-Zthreads=0", "-Zshare-generics=y"]
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ env:
# compiled crates are reused across runs (the cold-build win for CI).
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
# .cargo/config.toml carries nightly-only -Z build-speed flags for local dev. CI runs
# stable, which rejects -Z, so clear rustflags here. RUSTFLAGS overrides build.rustflags
# in cargo's precedence, and these flags only affect compile speed, never the output.
RUSTFLAGS: ""

jobs:
fmt-and-test:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ concurrency:

env:
CARGO_TERM_COLOR: always
# .cargo/config.toml carries nightly-only -Z build-speed flags for local dev. Release
# builds and `cargo publish` run on stable, which rejects -Z, so clear rustflags here.
# RUSTFLAGS overrides build.rustflags in cargo's precedence, and these flags only affect
# compile speed, never the output, so release binaries are unchanged.
RUSTFLAGS: ""

jobs:
changelog:
Expand Down
93 changes: 93 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ clap = { version = "4.6.1", features = ["derive"] }
cliclack = "0.5.4"
console = "0.16.3"
dirs = "6.0.0"
jwalk = "0.8.1"
similar = "3.1.1"
# Pinned to 0.36 on purpose: it has MSRV 1.75 (below our edition-2024 floor, so no cost),
# while 0.37+ raises the MSRV and 0.39 needs Rust 1.95. Only RAM and disk facts are used.
Expand All @@ -24,3 +25,15 @@ toml_edit = "0.25.12"
# frd recommends `strip` to the projects it tunes, so it ships its own binaries stripped too.
[profile.release]
strip = true

[profile.dev]
debug = "line-tables-only"
split-debuginfo = "unpacked"

[profile.dev.package."*"]
opt-level = 2

[profile.fast-build]
inherits = "dev"
debug = 0
strip = "debuginfo"
129 changes: 102 additions & 27 deletions src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
//! the live report so the user sees why an item matters on their machine.

use crate::suggestion::{
Action, InstallSpec, RunSpec, Scope, Suggestion, Tag, TomlChange, TomlOp, TomlValue,
Action, InstallSpec, PurgeSpec, Scope, Suggestion, SweepSpec, Tag, TomlChange, TomlOp,
TomlValue,
};
use crate::system::{SystemReport, have, human_bytes};
use std::path::PathBuf;
use crate::toml_ops;
use std::path::{Path, PathBuf};

pub fn build(r: &SystemReport) -> Vec<Suggestion> {
let mut out = Vec::new();
Expand Down Expand Up @@ -169,27 +171,50 @@ pub fn build(r: &SystemReport) -> Vec<Suggestion> {
}

// --- tools ---
if !have("cargo-sweep") {
// Gate the sweep on cargo-sweep being on PATH: offering to run a binary that is
// not installed only produces a confusing failed step. When it is missing we offer
// the install instead, and a re-run of frd then surfaces the sweep (the same
// install-then-rerun flow sccache uses above).
if have("cargo-sweep") {
out.push(sweep_sug(
"Sweep stale build artifacts",
Tag::Disk,
"Removes artifacts untouched for more than 7 days while keeping warm ones. \
Choose how wide to sweep: just this project, or any parent up to your home dir."
.into(),
SweepSpec {
candidates: sweep_candidates(&p.root),
time_days: 7,
},
));
} else {
out.push(install_sug(
"Install cargo-sweep (disk reclaim)",
Tag::Disk,
"Garbage-collects stale build artifacts that Cargo never removes, by age, instead \
of the all-or-nothing cargo clean."
of the all-or-nothing cargo clean. Re-run frd afterward to sweep."
.into(),
"cargo-sweep",
"cargo-sweep",
));
}
out.push(run_sug(
"Sweep stale artifacts in this project",
Tag::Disk,
"Removes artifacts untouched for more than 15 days while keeping warm ones. Re-run \
with --recursive ~/dev to sweep every repo. Needs cargo-sweep."
.into(),
"cargo",
vec!["sweep".into(), "--time".into(), "15".into()],
Some(p.root.clone()),
));

// Offer the leftover-target purge only once builds are centralized: with
// build.target-dir set, every per-project target/ from before the switch is dead
// weight that nothing will reuse. Until then deleting them just forces a cold rebuild
// into the same scattered place. The card here covers the already-centralized case;
// main offers the same purge in the run the user accepts centralization.
if let Some(spec) = purge_spec(r) {
out.push(purge_sug(
"Reclaim leftover per-project target dirs",
Tag::Disk,
"build.target-dir is set, so new builds share one dir and the old per-project \
target/ dirs are now dead weight. Delete them to reclaim that space. Choose how \
wide: just this project, or any parent up to your home dir."
.into(),
spec,
));
}

if !have("cargo-machete") {
out.push(install_sug(
Expand All @@ -205,6 +230,18 @@ pub fn build(r: &SystemReport) -> Vec<Suggestion> {
out
}

/// The leftover-target purge for this machine, present only once `build.target-dir` is
/// configured globally. Returned to main so it can offer the purge in the same run the
/// user accepts centralization, not just on the next one. The configured central dir is
/// carried in `protected` so the purge never deletes the dir builds were just pointed at.
pub fn purge_spec(r: &SystemReport) -> Option<PurgeSpec> {
let central = toml_ops::global_target_dir_value(r)?;
Some(PurgeSpec {
candidates: sweep_candidates(&r.project.root),
protected: Some(central),
})
}

fn shared_target_dir() -> String {
dirs::home_dir()
.unwrap_or_default()
Expand Down Expand Up @@ -235,22 +272,60 @@ fn install_sug(title: &str, tag: Tag, why: String, crate_name: &str, bin: &str)
}
}

fn run_sug(
title: &str,
tag: Tag,
why: String,
program: &str,
args: Vec<String>,
cwd: Option<PathBuf>,
) -> Suggestion {
fn sweep_sug(title: &str, tag: Tag, why: String, spec: SweepSpec) -> Suggestion {
Suggestion {
title: title.into(),
tag,
why,
action: Action::Run(RunSpec {
program: program.into(),
args,
cwd,
}),
action: Action::Sweep(spec),
}
}

fn purge_sug(title: &str, tag: Tag, why: String, spec: PurgeSpec) -> Suggestion {
Suggestion {
title: title.into(),
tag,
why,
action: Action::Purge(spec),
}
}

fn sweep_candidates(root: &Path) -> Vec<PathBuf> {
candidates_up_to(root, dirs::home_dir().as_deref())
}

/// The project dir then each parent up to and including `home`, narrow to wide. If
/// the project is not under `home` the chain would climb to the filesystem root, so
/// in that case we offer only the project dir and never sweep toward `/`.
fn candidates_up_to(root: &Path, home: Option<&Path>) -> Vec<PathBuf> {
let mut dirs = Vec::new();
for ancestor in root.ancestors() {
dirs.push(ancestor.to_path_buf());
if Some(ancestor) == home {
return dirs;
}
}
vec![root.to_path_buf()]
}

#[cfg(test)]
mod tests {
use super::candidates_up_to;
use std::path::{Path, PathBuf};

#[test]
fn candidates_run_from_project_up_to_home() {
let got = candidates_up_to(Path::new("/Users/x/dev/proj"), Some(Path::new("/Users/x")));
let want: Vec<PathBuf> = ["/Users/x/dev/proj", "/Users/x/dev", "/Users/x"]
.iter()
.map(PathBuf::from)
.collect();
assert_eq!(got, want);
}

#[test]
fn project_outside_home_offers_only_itself() {
let got = candidates_up_to(Path::new("/tmp/proj"), Some(Path::new("/Users/x")));
assert_eq!(got, vec![PathBuf::from("/tmp/proj")]);
}
}
19 changes: 11 additions & 8 deletions src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub fn run(report: &SystemReport) -> bool {
// and tool-based items, which status_of can classify as applied or pending.
let (maintenance, checkable): (Vec<Suggestion>, Vec<Suggestion>) = catalog::build(report)
.into_iter()
.partition(|s| matches!(s.action, Action::Run(_)));
.partition(|s| matches!(s.action, Action::Sweep(_) | Action::Purge(_)));

println!("{}", style("Doctor: optimization checkup").bold().cyan());
let mut pending = 0usize;
Expand All @@ -32,13 +32,16 @@ pub fn run(report: &SystemReport) -> bool {
println!();
println!("{}", style("Maintenance (re-run anytime):").dim());
for s in &maintenance {
if let Action::Run(spec) = &s.action {
println!(
" {} {}",
style(format!("{}:", s.title)).dim(),
style(spec.display()).dim()
);
}
let detail = match &s.action {
Action::Sweep(spec) => spec.display(),
Action::Purge(spec) => spec.display(),
_ => continue,
};
println!(
" {} {}",
style(format!("{}:", s.title)).dim(),
style(detail).dim()
);
}
}

Expand Down
Loading
Loading