1pub mod adapter;
15pub mod build_spec;
16pub mod convert;
17pub mod diagnostics;
18pub mod ecosystem_impl;
19pub mod error;
20pub mod features;
21pub mod fleet_commit;
22pub mod fleet_migrate;
23pub mod fleet_verify;
24pub mod fleet_sweep;
25pub mod gen_build_job;
26pub mod gen_delta;
27pub mod git_prefetcher;
28pub mod invariants;
29pub mod lock_lifecycle;
30pub mod path_resolver;
31pub mod platform_features;
32pub mod quirks;
33pub mod raw;
34
35pub use adapter::{ctx_for, CargoAdapter};
36pub use error::{CargoError, Result};
37
38use std::path::{Path, PathBuf};
39
40use gen_types::Manifest;
41
42pub fn parse(root: &Path) -> Result<Manifest> {
51 let root_manifest_path = root.join("Cargo.toml");
52 let root_raw = read_cargo_toml(&root_manifest_path)?;
53
54 let mut packages = Vec::new();
55
56 if let Some(_pkg) = &root_raw.package {
57 let pkg = convert::convert_package(
59 &root_raw,
60 &root_manifest_path,
61 workspace_pkg(&root_raw),
62 workspace_deps(&root_raw),
63 )?;
64 packages.push(pkg);
65 }
66
67 if let Some(ws) = &root_raw.workspace {
68 for member in &ws.members {
69 if member.contains('*') {
74 if let Some(expanded) = expand_glob(root, member) {
75 for mpath in expanded {
76 packages.push(parse_member(root, &mpath, &root_raw)?);
77 }
78 continue;
79 }
80 }
81 let mpath = PathBuf::from(member);
82 packages.push(parse_member(root, &mpath, &root_raw)?);
83 }
84 }
85
86 let lockfile = match read_optional(&root.join("Cargo.lock"))? {
87 Some(text) => {
88 let parsed: raw::CargoLock =
89 toml::from_str(&text).map_err(|source| CargoError::Toml {
90 path: root.join("Cargo.lock"),
91 source,
92 })?;
93 Some(convert::convert_lockfile(&parsed, &root.join("Cargo.lock"))?)
94 }
95 None => None,
96 };
97
98 Ok(convert::build_manifest(root, &root_raw, packages, lockfile))
99}
100
101fn parse_member(
102 root: &Path,
103 member_rel: &Path,
104 workspace_root_raw: &raw::CargoToml,
105) -> Result<gen_types::Package> {
106 let path = root.join(member_rel).join("Cargo.toml");
107 let raw = read_cargo_toml(&path).map_err(|e| match e {
108 CargoError::Io { .. } => CargoError::MissingWorkspaceMember {
109 root: root.to_path_buf(),
110 member: member_rel.display().to_string(),
111 },
112 other => other,
113 })?;
114 convert::convert_package(
115 &raw,
116 &path,
117 workspace_pkg(workspace_root_raw),
118 workspace_deps(workspace_root_raw),
119 )
120}
121
122fn workspace_pkg(raw: &raw::CargoToml) -> Option<&raw::RawPackage> {
123 raw.workspace.as_ref().and_then(|w| w.package.as_ref())
124}
125
126fn workspace_deps(raw: &raw::CargoToml) -> &indexmap::IndexMap<String, raw::RawDep> {
127 static EMPTY: std::sync::OnceLock<indexmap::IndexMap<String, raw::RawDep>> =
128 std::sync::OnceLock::new();
129 raw.workspace
130 .as_ref()
131 .map(|w| &w.dependencies)
132 .unwrap_or_else(|| EMPTY.get_or_init(indexmap::IndexMap::new))
133}
134
135fn read_cargo_toml(path: &Path) -> Result<raw::CargoToml> {
136 let text = std::fs::read_to_string(path).map_err(|source| CargoError::Io {
137 path: path.to_path_buf(),
138 source,
139 })?;
140 toml::from_str(&text).map_err(|source| CargoError::Toml {
141 path: path.to_path_buf(),
142 source,
143 })
144}
145
146fn read_optional(path: &Path) -> Result<Option<String>> {
147 match std::fs::read_to_string(path) {
148 Ok(s) => Ok(Some(s)),
149 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
150 Err(source) => Err(CargoError::Io {
151 path: path.to_path_buf(),
152 source,
153 }),
154 }
155}
156
157fn expand_glob(root: &Path, pattern: &str) -> Option<Vec<PathBuf>> {
162 let (prefix, rest) = pattern.split_once('*')?;
163 if !rest.is_empty() && rest != "/" {
164 return None; }
166 let (search_dir, name_prefix) =
171 if prefix.is_empty() || prefix.ends_with('/') {
172 (root.join(prefix.trim_end_matches('/')), String::new())
173 } else if let Some(slash) = prefix.rfind('/') {
174 (root.join(&prefix[..slash]), prefix[slash + 1..].to_string())
175 } else {
176 (root.to_path_buf(), prefix.to_string())
177 };
178 let mut out = Vec::new();
179 let entries = std::fs::read_dir(&search_dir).ok()?;
180 for e in entries.flatten() {
181 let path = e.path();
182 if !path.is_dir() || !path.join("Cargo.toml").exists() {
183 continue;
184 }
185 let fname = path.file_name()?.to_string_lossy().into_owned();
186 if !name_prefix.is_empty() && !fname.starts_with(&name_prefix) {
187 continue;
188 }
189 if let Ok(rel) = path.strip_prefix(root) {
190 out.push(rel.to_path_buf());
191 }
192 }
193 out.sort();
194 Some(out)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use std::fs;
201
202 fn write(path: &Path, body: &str) {
203 if let Some(p) = path.parent() {
204 fs::create_dir_all(p).unwrap();
205 }
206 fs::write(path, body).unwrap();
207 }
208
209 #[test]
210 fn parses_single_crate_repo() {
211 let dir = tempfile_dir();
212 write(
213 &dir.join("Cargo.toml"),
214 r#"
215[package]
216name = "demo"
217version = "0.2.1"
218edition = "2024"
219license = "MIT"
220description = "demo crate"
221
222[dependencies]
223serde = "1.0"
224serde_json = { version = "1", features = ["preserve_order"] }
225indexmap = { version = "2", default-features = false }
226"#,
227 );
228 let m = parse(&dir).unwrap();
229 assert_eq!(m.package_count(), 1);
230 let p = m.find_package("demo").unwrap();
231 assert_eq!(p.version, gen_types::Version::new(0, 2, 1));
232 assert_eq!(p.license.as_deref(), Some("MIT"));
233 assert_eq!(p.dependencies.len(), 3);
234 let s = p.dependencies.iter().find(|d| d.name == "serde").unwrap();
235 assert!(matches!(s.kind, gen_types::DependencyKind::Direct));
236 assert_eq!(s.constraint.native_syntax.as_deref(), Some("1.0"));
237 let sj = p.dependencies.iter().find(|d| d.name == "serde_json").unwrap();
238 assert_eq!(sj.features_enabled, vec!["preserve_order".to_string()]);
239 let im = p.dependencies.iter().find(|d| d.name == "indexmap").unwrap();
240 assert!(!im.default_features);
241 }
242
243 #[test]
244 fn parses_workspace_with_member_inheritance() {
245 let dir = tempfile_dir();
246 write(
247 &dir.join("Cargo.toml"),
248 r#"
249[workspace]
250members = ["crates/a", "crates/b"]
251
252[workspace.package]
253version = "0.3.0"
254license = "MIT"
255edition = "2024"
256
257[workspace.dependencies]
258serde = "1.0"
259shared = { version = "0.5", features = ["foo"] }
260"#,
261 );
262 write(
263 &dir.join("crates/a/Cargo.toml"),
264 r#"
265[package]
266name = "a"
267version.workspace = true
268license.workspace = true
269edition.workspace = true
270
271[dependencies]
272serde = { workspace = true }
273"#,
274 );
275 write(
276 &dir.join("crates/b/Cargo.toml"),
277 r#"
278[package]
279name = "b"
280version = "0.9.0"
281edition = "2024"
282license = "MIT"
283
284[dependencies]
285shared = { workspace = true, features = ["bar"] }
286"#,
287 );
288 let m = parse(&dir).unwrap();
289 assert_eq!(m.package_count(), 2);
290 let a = m.find_package("a").unwrap();
291 assert_eq!(a.version, gen_types::Version::new(0, 3, 0));
292 assert_eq!(a.license.as_deref(), Some("MIT"));
293 assert_eq!(a.dependencies[0].name, "serde");
294
295 let b = m.find_package("b").unwrap();
296 assert_eq!(b.version, gen_types::Version::new(0, 9, 0));
297 let shared = b.dependencies.iter().find(|d| d.name == "shared").unwrap();
298 assert!(shared.features_enabled.contains(&"foo".to_string()));
300 assert!(shared.features_enabled.contains(&"bar".to_string()));
301 }
302
303 #[test]
304 fn parses_target_cfg_dependencies() {
305 let dir = tempfile_dir();
306 write(
307 &dir.join("Cargo.toml"),
308 r#"
309[package]
310name = "p"
311version = "0.1.0"
312edition = "2024"
313
314[dependencies]
315
316[target.'cfg(unix)'.dependencies]
317nix = "0.27"
318
319[target.'cfg(windows)'.dev-dependencies]
320winapi = "0.3"
321"#,
322 );
323 let m = parse(&dir).unwrap();
324 let p = m.find_package("p").unwrap();
325 let nix = p.dependencies.iter().find(|d| d.name == "nix").unwrap();
326 let pred = nix.target_predicate.as_ref().unwrap();
327 assert!(matches!(pred, gen_types::TargetPredicate::CargoCfg { expr } if expr.contains("unix")));
328 let winapi = p.dependencies.iter().find(|d| d.name == "winapi").unwrap();
329 assert!(matches!(winapi.kind, gen_types::DependencyKind::Dev));
330 }
331
332 #[test]
333 fn parses_git_dependency_with_rev() {
334 let dir = tempfile_dir();
335 write(
336 &dir.join("Cargo.toml"),
337 r#"
338[package]
339name = "p"
340version = "0.1.0"
341edition = "2024"
342
343[dependencies]
344foo = { git = "https://github.com/x/y", rev = "abc123" }
345bar = { git = "https://github.com/x/z", branch = "main" }
346"#,
347 );
348 let m = parse(&dir).unwrap();
349 let p = m.find_package("p").unwrap();
350 let foo = p.dependencies.iter().find(|d| d.name == "foo").unwrap();
351 let src = foo.source_override.as_ref().unwrap();
352 match src {
353 gen_types::PackageSource::Git { url, rev, .. } => {
354 assert_eq!(url, "https://github.com/x/y");
355 assert_eq!(rev, "abc123");
356 }
357 other => panic!("expected Git, got {other:?}"),
358 }
359 let bar = p.dependencies.iter().find(|d| d.name == "bar").unwrap();
360 match bar.source_override.as_ref().unwrap() {
361 gen_types::PackageSource::Git { rev, .. } => assert_eq!(rev, "main"),
362 other => panic!("expected Git, got {other:?}"),
363 }
364 }
365
366 #[test]
367 fn parses_optional_dep_as_optional_kind() {
368 let dir = tempfile_dir();
369 write(
370 &dir.join("Cargo.toml"),
371 r#"
372[package]
373name = "p"
374version = "0.1.0"
375edition = "2024"
376
377[dependencies]
378opt = { version = "1", optional = true }
379"#,
380 );
381 let m = parse(&dir).unwrap();
382 let p = m.find_package("p").unwrap();
383 let opt = p.dependencies.iter().find(|d| d.name == "opt").unwrap();
384 assert!(matches!(opt.kind, gen_types::DependencyKind::Optional));
385 }
386
387 #[test]
388 fn parses_features_block() {
389 let dir = tempfile_dir();
390 write(
391 &dir.join("Cargo.toml"),
392 r#"
393[package]
394name = "p"
395version = "0.1.0"
396edition = "2024"
397
398[dependencies]
399
400[features]
401default = ["std"]
402std = []
403extra = ["serde/derive", "dep:opt"]
404"#,
405 );
406 let m = parse(&dir).unwrap();
407 let p = m.find_package("p").unwrap();
408 assert_eq!(p.features.len(), 3);
409 let extra = p.features.iter().find(|f| f.name == "extra").unwrap();
410 assert!(
411 extra
412 .implies
413 .iter()
414 .any(|r| matches!(r, gen_types::FeatureRef::Namespaced { package, feature }
415 if package == "serde" && feature == "derive"))
416 );
417 assert!(
418 extra
419 .implies
420 .iter()
421 .any(|r| matches!(r, gen_types::FeatureRef::DepActivation { dep_name }
422 if dep_name == "opt"))
423 );
424 }
425
426 #[test]
427 fn parses_version_range_to_range_spec() {
428 let dir = tempfile_dir();
429 write(
430 &dir.join("Cargo.toml"),
431 r#"
432[package]
433name = "p"
434version = "0.1.0"
435edition = "2024"
436
437[dependencies]
438foo = ">=1.2.3, <2.0.0"
439"#,
440 );
441 let m = parse(&dir).unwrap();
442 let p = m.find_package("p").unwrap();
443 let foo = p.dependencies.iter().find(|d| d.name == "foo").unwrap();
444 match &foo.constraint.spec {
445 gen_types::ConstraintSpec::Range {
446 lower_inclusive,
447 upper_exclusive,
448 } => {
449 assert_eq!(*lower_inclusive, gen_types::Version::new(1, 2, 3));
450 assert_eq!(*upper_exclusive, gen_types::Version::new(2, 0, 0));
451 }
452 other => panic!("expected Range, got {other:?}"),
453 }
454 }
455
456 #[test]
457 fn parses_lockfile_when_present() {
458 let dir = tempfile_dir();
459 write(
460 &dir.join("Cargo.toml"),
461 r#"
462[package]
463name = "demo"
464version = "0.1.0"
465edition = "2024"
466"#,
467 );
468 write(
469 &dir.join("Cargo.lock"),
470 r#"
471version = 3
472
473[[package]]
474name = "demo"
475version = "0.1.0"
476
477[[package]]
478name = "serde"
479version = "1.0.228"
480source = "registry+https://github.com/rust-lang/crates.io-index"
481checksum = "0000000000000000000000000000000000000000000000000000000000000001"
482dependencies = ["serde_derive"]
483
484[[package]]
485name = "serde_derive"
486version = "1.0.228"
487source = "registry+https://github.com/rust-lang/crates.io-index"
488checksum = "0000000000000000000000000000000000000000000000000000000000000002"
489"#,
490 );
491 let m = parse(&dir).unwrap();
492 let lock = m.lockfile.as_ref().unwrap();
493 assert_eq!(lock.resolved.len(), 3);
494 let serde_entry = lock.resolved.get("serde/1.0.228").unwrap();
495 match &serde_entry.source {
496 gen_types::PackageSource::Registry {
497 registry,
498 integrity_hash,
499 ..
500 } => {
501 assert_eq!(*registry, gen_types::Registry::CratesIo);
502 assert!(integrity_hash.as_ref().unwrap().starts_with("sha256:"));
503 }
504 other => panic!("expected Registry source, got {other:?}"),
505 }
506 assert_eq!(serde_entry.resolved_dependencies.len(), 1);
507 assert_eq!(serde_entry.resolved_dependencies[0].name, "serde_derive");
508 assert_ne!(lock.content_addressed_hash, gen_types::ContentHash::genesis());
509 }
510
511 #[test]
512 fn glob_expands_workspace_members() {
513 let dir = tempfile_dir();
514 write(
515 &dir.join("Cargo.toml"),
516 r#"
517[workspace]
518members = ["crates/*"]
519"#,
520 );
521 write(
522 &dir.join("crates/alpha/Cargo.toml"),
523 r#"
524[package]
525name = "alpha"
526version = "0.1.0"
527edition = "2024"
528"#,
529 );
530 write(
531 &dir.join("crates/beta/Cargo.toml"),
532 r#"
533[package]
534name = "beta"
535version = "0.1.0"
536edition = "2024"
537"#,
538 );
539 let m = parse(&dir).unwrap();
540 assert_eq!(m.package_count(), 2);
541 assert!(m.find_package("alpha").is_some());
542 assert!(m.find_package("beta").is_some());
543 }
544
545 #[test]
546 fn missing_workspace_member_surfaces_typed_error() {
547 let dir = tempfile_dir();
548 write(
549 &dir.join("Cargo.toml"),
550 r#"
551[workspace]
552members = ["does/not/exist"]
553"#,
554 );
555 let e = parse(&dir).unwrap_err();
556 match e {
557 CargoError::MissingWorkspaceMember { member, .. } => {
558 assert_eq!(member, "does/not/exist");
559 }
560 other => panic!("expected MissingWorkspaceMember, got {other:?}"),
561 }
562 }
563
564 fn tempfile_dir() -> PathBuf {
565 use std::sync::atomic::{AtomicU64, Ordering};
566 static COUNTER: AtomicU64 = AtomicU64::new(0);
567 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
568 let base = std::env::temp_dir().join(format!(
569 "gen-cargo-test-{}-{}-{:?}",
570 std::process::id(),
571 n,
572 std::time::SystemTime::now()
573 .duration_since(std::time::UNIX_EPOCH)
574 .unwrap()
575 .as_nanos()
576 ));
577 let _ = fs::remove_dir_all(&base);
578 fs::create_dir_all(&base).unwrap();
579 base
580 }
581}