Skip to content

Commit 03632d2

Browse files
committed
cargo-rail: finally, no more Hakari!
1 parent f96f71f commit 03632d2

File tree

10 files changed

+1117
-14
lines changed

10 files changed

+1117
-14
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ AGENTS.md
2424

2525
# Cargo-Rail (Testing)
2626
*rail.toml
27+
.config/rail.toml.example
2728

2829
# Notes
2930
TODO.md

src/cargo/metadata.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,8 @@ impl WorkspaceMetadata {
2727

2828
/// Load workspace metadata with custom feature configuration
2929
///
30-
/// Useful for simulating different feature combinations to detect fragmentation
31-
///
32-
/// TODO: Future feature - will be used for:
33-
/// - Feature testing: `cargo rail test --all-features`
34-
/// - Feature simulation: detect fragmentation under different configs
35-
#[allow(dead_code)]
30+
/// Used by unify commands to gather metadata with --all-features for accurate
31+
/// feature union across the workspace. Can also be used for feature simulation.
3632
pub fn load_with_features(
3733
workspace_root: &Path,
3834
all_features: bool,

src/cargo/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
//! - Workspace metadata loading via cargo_metadata
55
//! - Cargo.toml manifest transformation (workspace flattening, path→version deps)
66
//! - Workspace dependency unification (eliminates workspace-hack crates)
7+
//! - Optional per-target validation with parallel execution
78
//!
89
//! # Architecture
910
//!
1011
//! - `metadata` - Comprehensive cargo_metadata wrapper with resolved feature analysis
1112
//! - `manifest` - Lossless Cargo.toml transformation
1213
//! - `unify` - Workspace dependency unification engine (replaces cargo-hakari)
14+
//! - `validate` - Optional per-target validation with Rayon parallelism
1315
1416
pub mod manifest;
1517
pub mod metadata;
1618
pub mod unify;
19+
pub mod validate;
1720

1821
pub use manifest::{CargoTransform, TransformContext};
1922
pub use metadata::WorkspaceMetadata;
2023
pub use unify::{UnifyConfig, UnifyStrategy, WorkspaceUnifier};
24+
pub use validate::validate_targets;

src/cargo/validate.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
//! Optional per-target validation for workspace dependency unification
2+
//!
3+
//! This module provides parallel validation against specific target triples to catch
4+
//! platform-specific issues that might not be visible in the default --all-features analysis.
5+
//!
6+
//! Validation is opt-in via rail.toml `[unify] validate_targets = [...]` or CLI flag.
7+
8+
use crate::error::{RailError, RailResult};
9+
use rayon::prelude::*;
10+
use std::path::Path;
11+
use std::sync::{Arc, Mutex};
12+
13+
/// Result of validating a single target
14+
#[derive(Debug, Clone)]
15+
pub struct TargetValidationResult {
16+
pub target: String,
17+
pub success: bool,
18+
pub error: Option<String>,
19+
pub warnings: Vec<String>,
20+
}
21+
22+
/// Summary of all target validations
23+
#[derive(Debug)]
24+
pub struct ValidationSummary {
25+
pub results: Vec<TargetValidationResult>,
26+
pub total_targets: usize,
27+
pub successful: usize,
28+
pub failed: usize,
29+
}
30+
31+
impl ValidationSummary {
32+
/// Check if all validations passed
33+
pub fn all_passed(&self) -> bool {
34+
self.failed == 0
35+
}
36+
37+
/// Get failed targets
38+
pub fn failed_targets(&self) -> Vec<&str> {
39+
self
40+
.results
41+
.iter()
42+
.filter(|r| !r.success)
43+
.map(|r| r.target.as_str())
44+
.collect()
45+
}
46+
}
47+
48+
/// Validate workspace unification against multiple target triples in parallel
49+
///
50+
/// This runs `cargo metadata --all-features --filter-platform=<triple>` for each target
51+
/// and validates that the workspace still resolves correctly.
52+
///
53+
/// Uses Rayon for parallel execution with configurable parallelism.
54+
pub fn validate_targets(
55+
workspace_root: &Path,
56+
targets: &[String],
57+
max_parallel_jobs: usize,
58+
) -> RailResult<ValidationSummary> {
59+
if targets.is_empty() {
60+
return Ok(ValidationSummary {
61+
results: vec![],
62+
total_targets: 0,
63+
successful: 0,
64+
failed: 0,
65+
});
66+
}
67+
68+
// Configure Rayon thread pool
69+
let pool = rayon::ThreadPoolBuilder::new()
70+
.num_threads(max_parallel_jobs)
71+
.build()
72+
.map_err(|e| RailError::message(format!("Failed to build thread pool: {}", e)))?;
73+
74+
// Shared result collector (thread-safe)
75+
let results = Arc::new(Mutex::new(Vec::new()));
76+
77+
// Run validations in parallel
78+
pool.install(|| {
79+
targets.par_iter().for_each(|target| {
80+
let result = validate_single_target(workspace_root, target);
81+
results.lock().unwrap().push(result);
82+
});
83+
});
84+
85+
// Collect results
86+
let results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
87+
let successful = results.iter().filter(|r| r.success).count();
88+
let failed = results.iter().filter(|r| !r.success).count();
89+
90+
Ok(ValidationSummary {
91+
total_targets: targets.len(),
92+
successful,
93+
failed,
94+
results,
95+
})
96+
}
97+
98+
/// Validate a single target triple
99+
fn validate_single_target(workspace_root: &Path, target: &str) -> TargetValidationResult {
100+
// Try to load metadata with --all-features and --filter-platform
101+
let result = std::process::Command::new("cargo")
102+
.arg("metadata")
103+
.arg("--all-features")
104+
.arg("--filter-platform")
105+
.arg(target)
106+
.arg("--format-version=1")
107+
.current_dir(workspace_root)
108+
.output();
109+
110+
match result {
111+
Ok(output) => {
112+
// Collect warnings from stderr (even on success)
113+
let warnings = extract_warnings_from_stderr(&output.stderr);
114+
115+
if output.status.success() {
116+
// Try to parse the metadata to ensure it's valid
117+
match serde_json::from_slice::<serde_json::Value>(&output.stdout) {
118+
Ok(_) => {
119+
// Success - workspace resolves for this target
120+
TargetValidationResult {
121+
target: target.to_string(),
122+
success: true,
123+
error: None,
124+
warnings,
125+
}
126+
}
127+
Err(e) => {
128+
// Failed to parse metadata JSON
129+
TargetValidationResult {
130+
target: target.to_string(),
131+
success: false,
132+
error: Some(format!("Failed to parse metadata: {}", e)),
133+
warnings,
134+
}
135+
}
136+
}
137+
} else {
138+
// cargo metadata failed
139+
let stderr = String::from_utf8_lossy(&output.stderr);
140+
TargetValidationResult {
141+
target: target.to_string(),
142+
success: false,
143+
error: Some(format!("cargo metadata failed: {}", stderr)),
144+
warnings,
145+
}
146+
}
147+
}
148+
Err(e) => {
149+
// Failed to execute cargo metadata
150+
TargetValidationResult {
151+
target: target.to_string(),
152+
success: false,
153+
error: Some(format!("Failed to execute cargo: {}", e)),
154+
warnings: vec![],
155+
}
156+
}
157+
}
158+
}
159+
160+
/// Extract warning messages from cargo metadata stderr
161+
///
162+
/// Parses cargo's diagnostic output to extract warnings about:
163+
/// - Platform-specific dependencies
164+
/// - Unavailable features
165+
/// - Target-specific issues
166+
fn extract_warnings_from_stderr(stderr: &[u8]) -> Vec<String> {
167+
let stderr_str = String::from_utf8_lossy(stderr);
168+
let mut warnings = Vec::new();
169+
170+
for line in stderr_str.lines() {
171+
let line = line.trim();
172+
173+
// Collect lines that look like warnings
174+
let is_warning = line.starts_with("warning:")
175+
|| line.contains("only available on")
176+
|| line.contains("not available on")
177+
|| (line.contains("feature") && (line.contains("unavailable") || line.contains("unsupported")))
178+
|| (line.contains("-specific") && line.contains("crate"));
179+
180+
if is_warning {
181+
warnings.push(line.to_string());
182+
}
183+
}
184+
185+
warnings
186+
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::*;
191+
192+
#[test]
193+
fn test_validate_single_target_host() {
194+
// Use current directory as test workspace
195+
let current_dir = std::env::current_dir().unwrap();
196+
197+
// Validate against host target (should always work)
198+
let host_target = std::env::consts::ARCH.to_string() + "-" + std::env::consts::OS;
199+
200+
// Map common OS names to target triple format
201+
let host_target = match std::env::consts::OS {
202+
"macos" => format!("{}-apple-darwin", std::env::consts::ARCH),
203+
"linux" => format!("{}-unknown-linux-gnu", std::env::consts::ARCH),
204+
"windows" => format!("{}-pc-windows-msvc", std::env::consts::ARCH),
205+
_ => host_target,
206+
};
207+
208+
let result = validate_single_target(&current_dir, &host_target);
209+
assert!(result.success, "Host target validation should succeed");
210+
assert!(result.error.is_none(), "Should have no error");
211+
}
212+
213+
#[test]
214+
fn test_validate_single_target_invalid() {
215+
let current_dir = std::env::current_dir().unwrap();
216+
217+
// Use an invalid target triple
218+
let result = validate_single_target(&current_dir, "invalid-target-triple");
219+
assert!(!result.success, "Invalid target should fail");
220+
assert!(result.error.is_some(), "Should have an error message");
221+
}
222+
223+
#[test]
224+
fn test_validation_summary_all_passed() {
225+
let summary = ValidationSummary {
226+
results: vec![
227+
TargetValidationResult {
228+
target: "target1".to_string(),
229+
success: true,
230+
error: None,
231+
warnings: vec![],
232+
},
233+
TargetValidationResult {
234+
target: "target2".to_string(),
235+
success: true,
236+
error: None,
237+
warnings: vec![],
238+
},
239+
],
240+
total_targets: 2,
241+
successful: 2,
242+
failed: 0,
243+
};
244+
245+
assert!(summary.all_passed());
246+
assert!(summary.failed_targets().is_empty());
247+
}
248+
249+
#[test]
250+
fn test_validation_summary_some_failed() {
251+
let summary = ValidationSummary {
252+
results: vec![
253+
TargetValidationResult {
254+
target: "target1".to_string(),
255+
success: true,
256+
error: None,
257+
warnings: vec![],
258+
},
259+
TargetValidationResult {
260+
target: "target2".to_string(),
261+
success: false,
262+
error: Some("Failed".to_string()),
263+
warnings: vec![],
264+
},
265+
],
266+
total_targets: 2,
267+
successful: 1,
268+
failed: 1,
269+
};
270+
271+
assert!(!summary.all_passed());
272+
assert_eq!(summary.failed_targets(), vec!["target2"]);
273+
}
274+
}

0 commit comments

Comments
 (0)