Skip to content

Commit 09155f4

Browse files
committed
cargo-rail: fixed some basic perf stuff; added docs/architecture.md
1 parent c8c636a commit 09155f4

File tree

5 files changed

+108
-81
lines changed

5 files changed

+108
-81
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ docs/src
2626
rules.md
2727
cmd_cfg.md
2828
v1_precheck.md
29+
ARCHITECTURE.md
2930

3031
# Cargo-Rail (Testing)
3132
*rail.toml

README.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ cargo rail test --explain
5757
**After:**
5858
5959
```yaml
60-
- run: cargo rail test --nextest # Tests only affected crates (1 minute)
60+
- run: cargo rail test # Tests only affected crates (1 minute)
6161
```
6262
6363
### 2. Dependency Unification (replaces cargo-hakari)
@@ -213,7 +213,7 @@ Run `cargo rail init --check` to preview generated config.
213213
cargo rail affected # Show affected crates
214214
cargo rail affected --since main # Compare against branch
215215
cargo rail test # Test affected crates
216-
cargo rail test --nextest --watch # Watch mode with nextest
216+
cargo rail test --explain # Explain why each crate is tested
217217
```
218218

219219
### Dependency Unification
@@ -228,7 +228,6 @@ cargo rail unify # Apply unification
228228
```bash
229229
cargo rail split my-crate # Split to standalone repo
230230
cargo rail sync my-crate # Bidirectional sync
231-
cargo rail status # Show sync status
232231
```
233232

234233
### Release Automation (replaces cargo-release + git-cliff)
@@ -249,18 +248,6 @@ cargo rail release --all --bump patch --execute
249248
- GitHub release creation via `gh` CLI (optional)
250249
- GPG/SSH tag signing support
251250

252-
### Watch Mode (bacon/cargo-watch integration)
253-
254-
```bash
255-
# Auto-detect best watcher and run smart tests
256-
cargo rail test --watch
257-
258-
# Explicit watcher selection
259-
cargo rail test --watch --watch-mode bacon
260-
```
261-
262-
---
263-
264251
## Comparison
265252

266253
### vs Dependency Management
@@ -303,7 +290,6 @@ cargo rail test --watch --watch-mode bacon
303290
| Change detection | ✅ Graph-aware | ⚠️ Path-based | ✅ Graph-aware |
304291
| Rust-native || ⚠️ Shell | ❌ JS ecosystem |
305292
| Nextest integration | ✅ Auto-detect | Manual ||
306-
| Watch mode | ✅ bacon/cargo-watch | Manual ||
307293
| Dependency resolution | ✅ Cargo's resolver |||
308294

309295
---

src/cargo/cargo_transform.rs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
use crate::cargo::manifest_ops;
88
use crate::error::{RailError, RailResult};
99
use cargo_metadata::Metadata;
10+
use std::cell::RefCell;
1011
use std::path::PathBuf;
11-
use toml_edit::{DocumentMut, Item};
12+
use toml_edit::{DocumentMut, Item, Table};
1213

1314
/// Context for Cargo.toml transformations
1415
pub struct TransformContext {
@@ -19,26 +20,71 @@ pub struct TransformContext {
1920
}
2021

2122
/// Cargo.toml transformer for split/sync operations
23+
///
24+
/// Caches the workspace document to avoid repeated I/O when transforming multiple manifests.
25+
/// Uses interior mutability (`RefCell`) so the public API remains `&self`.
2226
pub struct CargoTransform {
2327
metadata: Metadata,
28+
/// Cached workspace document (loaded lazily via RefCell for interior mutability)
29+
cached_workspace_doc: RefCell<Option<DocumentMut>>,
30+
/// Workspace root for lazy loading
31+
workspace_root: PathBuf,
2432
}
2533

2634
impl CargoTransform {
2735
/// Create a new transformer with workspace metadata
2836
pub fn new(metadata: Metadata) -> Self {
29-
Self { metadata }
37+
let workspace_root = metadata.workspace_root.as_std_path().to_path_buf();
38+
Self {
39+
metadata,
40+
cached_workspace_doc: RefCell::new(None),
41+
workspace_root,
42+
}
43+
}
44+
45+
/// Get workspace.package table, loading and caching the workspace doc if needed
46+
///
47+
/// Uses `RefCell` for interior mutability - loads once, caches for reuse.
48+
fn get_workspace_package(&self) -> RailResult<Option<Table>> {
49+
// Check if already cached
50+
{
51+
let cache = self.cached_workspace_doc.borrow();
52+
if let Some(ref doc) = *cache {
53+
return Ok(
54+
doc
55+
.get("workspace")
56+
.and_then(|w| w.as_table())
57+
.and_then(|w| w.get("package"))
58+
.and_then(|p| p.as_table())
59+
.cloned(),
60+
);
61+
}
62+
}
63+
64+
// Load and cache
65+
let workspace_toml_path = self.workspace_root.join("Cargo.toml");
66+
let doc = manifest_ops::read_toml_file(&workspace_toml_path)?;
67+
let result = doc
68+
.get("workspace")
69+
.and_then(|w| w.as_table())
70+
.and_then(|w| w.get("package"))
71+
.and_then(|p| p.as_table())
72+
.cloned();
73+
74+
*self.cached_workspace_doc.borrow_mut() = Some(doc);
75+
Ok(result)
3076
}
3177

3278
/// Transform a Cargo.toml from workspace format to split (standalone) format
3379
///
3480
/// This replaces workspace dependency references with concrete version requirements.
35-
pub fn transform_to_split(&self, content: &str, context: &TransformContext) -> RailResult<String> {
81+
pub fn transform_to_split(&self, content: &str, _context: &TransformContext) -> RailResult<String> {
3682
let mut doc: DocumentMut = content
3783
.parse()
3884
.map_err(|e| RailError::message(format!("Failed to parse Cargo.toml: {}", e)))?;
3985

4086
// Remove workspace inheritance markers and resolve to actual values
41-
self.resolve_workspace_inheritance(&mut doc, &context.workspace_root)?;
87+
self.resolve_workspace_inheritance(&mut doc)?;
4288

4389
// Transform workspace dependencies to standalone format
4490
self.transform_dependencies_to_standalone(&mut doc)?;
@@ -57,21 +103,12 @@ impl CargoTransform {
57103
}
58104

59105
/// Resolve workspace inheritance (workspace = true fields) to actual values
60-
fn resolve_workspace_inheritance(&self, doc: &mut DocumentMut, workspace_root: &std::path::Path) -> RailResult<()> {
61-
// Load workspace Cargo.toml to get [workspace.package] values
62-
let workspace_toml_path = workspace_root.join("Cargo.toml");
63-
let workspace_doc = manifest_ops::read_toml_file(&workspace_toml_path)?;
64-
65-
// Get workspace.package table if it exists
66-
let workspace_package = workspace_doc
67-
.get("workspace")
68-
.and_then(|w| w.as_table())
69-
.and_then(|w| w.get("package"))
70-
.and_then(|p| p.as_table());
71-
72-
// Use manifest_ops to resolve package inheritance
73-
if let Some(workspace_pkg) = workspace_package {
74-
manifest_ops::resolve_package_workspace_inheritance(doc, workspace_pkg)?;
106+
///
107+
/// Uses cached workspace document to avoid repeated I/O.
108+
fn resolve_workspace_inheritance(&self, doc: &mut DocumentMut) -> RailResult<()> {
109+
// Get workspace.package table from cache (loads workspace doc if needed)
110+
if let Some(workspace_pkg) = self.get_workspace_package()? {
111+
manifest_ops::resolve_package_workspace_inheritance(doc, &workspace_pkg)?;
75112
}
76113

77114
Ok(())

src/cargo/manifest_analyzer.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ pub struct ManifestAnalyzer {
177177
usage_index: HashMap<DepKey, Vec<DepUsage>>,
178178
/// Pre-computed usage counts for O(1) lookup
179179
usage_counts: HashMap<DepKey, usize>,
180+
/// Package name index for O(1) lookup of dep keys by package name
181+
/// Maps package_name -> list of DepKeys that refer to that package
182+
package_index: HashMap<String, Vec<DepKey>>,
180183
}
181184

182185
impl ManifestAnalyzer {
@@ -218,10 +221,20 @@ impl ManifestAnalyzer {
218221
})
219222
.collect();
220223

224+
// Build package name index for O(1) lookup of dep keys by package name
225+
let mut package_index: HashMap<String, Vec<DepKey>> = HashMap::new();
226+
for dep_key in usage_index.keys() {
227+
package_index
228+
.entry(dep_key.name.clone())
229+
.or_default()
230+
.push(dep_key.clone());
231+
}
232+
221233
Ok(Self {
222234
members: parsed_members,
223235
usage_index,
224236
usage_counts,
237+
package_index,
225238
})
226239
}
227240

@@ -622,11 +635,15 @@ impl ManifestAnalyzer {
622635
/// Count how many crates use a package (aggregated across renamed and non-renamed deps)
623636
///
624637
/// Issue #6: When include_renamed = true, count all usages of the package
638+
/// Uses package_index for O(1) key lookup instead of O(n) scan.
625639
pub fn package_usage_count(&self, package_name: &str) -> usize {
626-
let mut unique_users: HashSet<&String> = HashSet::new();
640+
let Some(dep_keys) = self.package_index.get(package_name) else {
641+
return 0;
642+
};
627643

628-
for (dep_key, usages) in &self.usage_index {
629-
if dep_key.name == package_name {
644+
let mut unique_users: HashSet<&String> = HashSet::new();
645+
for dep_key in dep_keys {
646+
if let Some(usages) = self.usage_index.get(dep_key) {
630647
for usage in usages {
631648
unique_users.insert(&usage.used_by);
632649
}
@@ -639,18 +656,27 @@ impl ManifestAnalyzer {
639656
/// Get all dep keys that refer to a specific package (including renamed)
640657
///
641658
/// Issue #6: Used to find all renamed variants of a package
659+
/// Uses package_index for O(1) lookup instead of O(n) scan.
642660
pub fn dep_keys_for_package(&self, package_name: &str) -> Vec<&DepKey> {
643-
self.usage_index.keys().filter(|k| k.name == package_name).collect()
661+
self
662+
.package_index
663+
.get(package_name)
664+
.map(|keys| keys.iter().collect())
665+
.unwrap_or_default()
644666
}
645667

646668
/// Get aggregated usage sites for a package (all renamed and non-renamed usages)
647669
///
648670
/// Issue #6: Used when include_renamed = true to merge features across all usages
671+
/// Uses package_index for O(1) key lookup instead of O(n) scan.
649672
pub fn get_package_usage_sites(&self, package_name: &str) -> Vec<&DepUsage> {
650-
let mut all_usages = Vec::new();
673+
let Some(dep_keys) = self.package_index.get(package_name) else {
674+
return Vec::new();
675+
};
651676

652-
for (dep_key, usages) in &self.usage_index {
653-
if dep_key.name == package_name {
677+
let mut all_usages = Vec::new();
678+
for dep_key in dep_keys {
679+
if let Some(usages) = self.usage_index.get(dep_key) {
654680
all_usages.extend(usages.iter());
655681
}
656682
}

src/cargo/manifest_writer.rs

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,24 @@ impl ManifestWriter {
3939
// Ensure [workspace] section exists
4040
manifest_ops::ensure_section(&mut doc, "workspace").context("Failed to create [workspace] section")?;
4141

42-
// Get or create [workspace.dependencies] - DO NOT CLEAR IT
43-
// We merge new deps with existing ones, not replace them
44-
let deps_table = manifest_ops::get_or_create_table(&mut doc, "workspace.dependencies")
45-
.context("Failed to create [workspace.dependencies]")?;
46-
// NOTE: Removed deps_table.clear() - BUG FIX: preserve existing deps
47-
48-
// Group dependencies by target
49-
let (regular_deps, target_deps) = self.group_dependencies(deps);
50-
51-
// Write regular dependencies
52-
for dep in regular_deps {
53-
let entry = manifest_ops::build_dep_entry(&dep);
54-
manifest_ops::insert_dependency(deps_table, &dep.name, entry).context("Failed to insert regular dependency")?;
42+
// First pass: write regular dependencies (no target)
43+
// This scope ensures deps_table reference is released before we handle target deps
44+
{
45+
let deps_table = manifest_ops::get_or_create_table(&mut doc, "workspace.dependencies")
46+
.context("Failed to create [workspace.dependencies]")?;
47+
48+
for dep in deps.iter().filter(|d| d.target.is_none()) {
49+
let entry = manifest_ops::build_dep_entry(dep);
50+
manifest_ops::insert_dependency(deps_table, &dep.name, entry).context("Failed to insert regular dependency")?;
51+
}
5552
}
5653

57-
// Write target-specific dependencies
58-
for (target, deps) in target_deps {
59-
for dep in deps {
60-
let entry = manifest_ops::build_dep_entry(&dep);
61-
manifest_ops::insert_target_dependency(&mut doc, &target, "dependencies", &dep.name, entry)
62-
.context("Failed to insert target dependency")?;
63-
}
54+
// Second pass: write target-specific dependencies
55+
for dep in deps.iter().filter(|d| d.target.is_some()) {
56+
let entry = manifest_ops::build_dep_entry(dep);
57+
let target = dep.target.as_ref().unwrap();
58+
manifest_ops::insert_target_dependency(&mut doc, target, "dependencies", &dep.name, entry)
59+
.context("Failed to insert target dependency")?;
6460
}
6561

6662
// Format and write
@@ -70,25 +66,6 @@ impl ManifestWriter {
7066
Ok(())
7167
}
7268

73-
/// Group dependencies into regular and target-specific
74-
fn group_dependencies(
75-
&self,
76-
deps: &[UnifiedDep],
77-
) -> (Vec<UnifiedDep>, std::collections::HashMap<String, Vec<UnifiedDep>>) {
78-
let mut regular_deps = Vec::new();
79-
let mut target_deps: std::collections::HashMap<String, Vec<UnifiedDep>> = std::collections::HashMap::new();
80-
81-
for dep in deps {
82-
if let Some(ref target) = dep.target {
83-
target_deps.entry(target.clone()).or_default().push(dep.clone());
84-
} else {
85-
regular_deps.push(dep.clone());
86-
}
87-
}
88-
89-
(regular_deps, target_deps)
90-
}
91-
9269
/// Update a member's Cargo.toml to use workspace inheritance
9370
///
9471
/// # Arguments

0 commit comments

Comments
 (0)