@@ -25,6 +25,7 @@ import mcpp.build.backend;
2525import mcpp.build.ninja;
2626import mcpp.build.runtime_validation;
2727import mcpp.bmi_cache;
28+ import mcpp.bmi_cache.maintenance; // dir_size + human_bytes, for `clean --stale`
2829import mcpp.manifest;
2930import mcpp.source_kind;
3031import mcpp.modgraph.scanner;
@@ -813,7 +814,8 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
813814 auto newFp = ctx.outputDir .filename ().string ();
814815 if (e.fingerprint != newFp) {
815816 mcpp::ui::warning (std::format (
816- " fingerprint changed ({} → {}), full rebuild" ,
817+ " fingerprint changed ({} → {}), full rebuild; "
818+ " `mcpp clean --stale` drops the directories no build still uses" ,
817819 e.fingerprint , newFp));
818820 }
819821 break ;
@@ -2568,4 +2570,126 @@ export int clean_project(bool wipe_bmi) {
25682570 return 0 ;
25692571}
25702572
2573+ // `mcpp clean --stale` driver (#565).
2574+ //
2575+ // Every build lands in target/<triple>/<fingerprint>/, and a changed
2576+ // fingerprint opens a fresh directory while the old one is never touched
2577+ // again. "Current" is what target/.build_cache records: one entry per
2578+ // (target, profile) built recently, which is the set the fast paths and
2579+ // `mcpp run` still resolve to. Every other directory under a recorded
2580+ // target/<triple>/ is a leftover from a configuration that no longer exists
2581+ // and can go without forcing a rebuild of anything that does.
2582+ //
2583+ // Three deliberate limits, each erring toward deleting a directory that a
2584+ // rebuild can recreate rather than one that cannot:
2585+ // * Entries are matched by (triple directory, fingerprint), not by the
2586+ // absolute path the record stores, so a moved checkout is not read as
2587+ // "everything is stale".
2588+ // * Only triple directories named by the record are visited. Anything else
2589+ // under target/ (`dist/` from `mcpp pack`, whatever a later release adds)
2590+ // is not a fingerprint directory and is left alone — as is a triple whose
2591+ // entry has been evicted from the record; that one stays until it is
2592+ // built again.
2593+ // * The record keys on (target, profile) while the fingerprint also folds in
2594+ // features; a `--no-cache` build writes no entry at all; and `mcpp test`
2595+ // builds through run_tests, which never writes one. So an unrecorded
2596+ // directory is not proof of staleness, and recomputing fingerprints here
2597+ // is not an option (prepare_build resolves dependencies and may reach the
2598+ // network; a clean command must not). The guard that costs nothing and
2599+ // matches how `cache prune` already thinks: an unrecorded directory
2600+ // written within `--older-than` (default one day) is kept — that is the
2601+ // build somebody just ran. Older and unrecorded goes; the cost of being
2602+ // wrong there is one rebuild of a configuration nobody has touched since.
2603+ //
2604+ // With no record at all there is nothing to compare against, and the command
2605+ // refuses rather than guess.
2606+ export int clean_stale (bool dryRun, std::int64_t keepWithinSecs) {
2607+ namespace fs = std::filesystem;
2608+ auto root = mcpp::project::find_manifest_root (fs::current_path ());
2609+ if (!root) { std::println (stderr, " error: not in an mcpp package" ); return 2 ; }
2610+ const fs::path target = *root / " target" ;
2611+
2612+ std::set<std::pair<std::string, std::string>> current; // (triple dir, fingerprint)
2613+ std::set<std::string> currentTriples;
2614+ for (const auto & e : read_build_cache (*root)) {
2615+ const fs::path out (e.outputDir );
2616+ const std::string fp = e.fingerprint .empty () ? out.filename ().string () : e.fingerprint ;
2617+ const std::string triple = out.parent_path ().filename ().string ();
2618+ if (fp.empty () || triple.empty ()) continue ;
2619+ current.emplace (triple, fp);
2620+ currentTriples.insert (triple);
2621+ }
2622+ if (current.empty ()) {
2623+ std::println (stderr, " error: {} has no build record, so nothing is known to be current; "
2624+ " run `mcpp build` once, then retry" ,
2625+ (*root / kBuildCacheFile ).string ());
2626+ return 2 ;
2627+ }
2628+
2629+ std::uintmax_t bytes = 0 ;
2630+ std::size_t removed = 0 , failed = 0 ;
2631+ std::error_code ec;
2632+ for (fs::directory_iterator tripleIt (target, ec), end; tripleIt != end; tripleIt.increment (ec)) {
2633+ std::error_code tec;
2634+ const std::string triple = tripleIt->path ().filename ().string ();
2635+ if (!tripleIt->is_directory (tec) || tec || !currentTriples.contains (triple)) continue ;
2636+ std::error_code iec;
2637+ for (fs::directory_iterator fpIt (tripleIt->path (), iec), fend; fpIt != fend; fpIt.increment (iec)) {
2638+ std::error_code fec;
2639+ if (!fpIt->is_directory (fec) || fec) continue ;
2640+ const fs::path dir = fpIt->path ();
2641+ const std::string fp = dir.filename ().string ();
2642+ if (current.contains ({triple, fp})) continue ;
2643+ const std::string shown = std::format (" target/{}/{}" , triple, fp);
2644+ // build.ninja is rewritten by every build; the directory's own
2645+ // mtime only moves when an entry is added or removed.
2646+ const auto stamp = dir / " build.ninja" ;
2647+ const auto written = fs::last_write_time (fs::exists (stamp, fec) ? stamp : dir, fec);
2648+ if (!fec) {
2649+ const auto age = std::chrono::duration_cast<std::chrono::seconds>(
2650+ fs::file_time_type::clock::now () - written).count ();
2651+ if (age < keepWithinSecs) {
2652+ const std::string ago = age < 3600 ? std::format (" {}m" , std::max<std::int64_t >(age / 60 , 1 ))
2653+ : age < 86400 ? std::format (" {}h" , age / 3600 )
2654+ : std::format (" {}d" , age / 86400 );
2655+ std::println (" kept {} (not in the record, but written {} ago; see --older-than)" , shown, ago);
2656+ continue ;
2657+ }
2658+ }
2659+ const auto size = mcpp::bmi_cache::dir_size (dir);
2660+ if (dryRun) {
2661+ std::println (" would remove {} ({})" , shown, mcpp::bmi_cache::human_bytes (size));
2662+ } else {
2663+ std::error_code rec;
2664+ fs::remove_all (dir, rec);
2665+ if (rec) {
2666+ std::println (stderr, " error: cannot remove {}: {}" , shown, rec.message ());
2667+ ++failed;
2668+ continue ;
2669+ }
2670+ std::println (" removed {} ({})" , shown, mcpp::bmi_cache::human_bytes (size));
2671+ }
2672+ bytes += size;
2673+ ++removed;
2674+ }
2675+ if (iec) {
2676+ std::println (stderr, " error: cannot read {}: {}" , tripleIt->path ().string (), iec.message ());
2677+ ++failed;
2678+ }
2679+ }
2680+ if (ec) {
2681+ std::println (stderr, " error: cannot read {}: {}" , target.string (), ec.message ());
2682+ return 1 ;
2683+ }
2684+
2685+ if (removed == 0 && failed == 0 ) {
2686+ std::println (" Nothing stale under {}: every fingerprint directory is recorded as current" ,
2687+ target.string ());
2688+ } else {
2689+ std::println (" {} {} director{} ({})" , dryRun ? " Would remove" : " Removed" , removed,
2690+ removed == 1 ? " y" : " ies" , mcpp::bmi_cache::human_bytes (bytes));
2691+ }
2692+ return failed ? 1 : 0 ;
2693+ }
2694+
25712695} // namespace mcpp::build
0 commit comments