1use std::collections::BTreeMap;
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::Duration;
13
14use async_trait::async_trait;
15use futures_util::StreamExt;
16use serde::{Deserialize, Serialize};
17
18use crate::backend::native_tool::{
19 self, NativeToolFamily, NativeToolLifecycle, NativeToolPreparation, NativeToolProvider,
20 LOCKED_NATIVE_REPLAY_OPTION, LOCKED_NATIVE_RUNTIME_OPTION,
21 LOCKED_NATIVE_RUNTIME_VERSION_OPTION,
22};
23use crate::backend::{Backend, Ctx, InstallCtx};
24use crate::error::{Error, Result};
25use crate::process::{
26 CaptureLimits, CommandOutcome, CommandRunner, CommandSpec, SystemCommandRunner,
27};
28use crate::source::Source;
29use crate::tool::{InstallDependency, InstallDependencyKind, InstallIdentity, ToolId};
30use crate::version::{ToolRequest, ToolVersion, VersionInfo, VersionSpec};
31
32const CRATES_IO_API: &str = "https://crates.io/api/v1/crates";
33const CARGO_METADATA_LIMIT: usize = 8 * 1024 * 1024;
34const CARGO_METADATA_TIMEOUT: Duration = Duration::from_secs(30);
35const CARGO_RESOLUTION_FILE: &str = "cargo-resolution.json";
36const CARGO_RESOLUTION_SCHEMA: u32 = 1;
37const PROVIDER_OUTPUT_LIMIT: usize = 1024 * 1024;
38const PROVIDER_TIMEOUT: Duration = Duration::from_secs(60 * 60);
39pub const LOCKED_CARGO_INDEX_OPTION: &str = "__osdk_cargo_index";
40static NEXT_METADATA_TEMPORARY: AtomicU64 = AtomicU64::new(0);
41
42pub fn validate_registry_index(value: &str) -> Result<()> {
43 let parsed = value
44 .strip_prefix("sparse+")
45 .ok_or_else(|| Error::config("Cargo registry source must use sparse HTTPS"))?;
46 let url = reqwest::Url::parse(parsed)
47 .map_err(|_| Error::config("Cargo registry source is invalid"))?;
48 if url.scheme() != "https"
49 || url.host_str().is_none()
50 || !url.username().is_empty()
51 || url.password().is_some()
52 || url.query().is_some()
53 || url.fragment().is_some()
54 || !value.ends_with('/')
55 || format!("sparse+{}", url.as_str()) != value
56 {
57 return Err(Error::config(
58 "Cargo registry source must be canonical sparse HTTPS",
59 ));
60 }
61 Ok(())
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65enum CargoSource {
66 Registry { package: String },
67 Git { url: String },
68}
69
70#[derive(Debug, Deserialize)]
71struct CratesResponse {
72 versions: Vec<CratesVersion>,
73}
74
75#[derive(Debug)]
76struct CargoRegistrySelection {
77 source: Source,
78 metadata: CratesResponse,
79}
80
81fn version_infos(metadata: CratesResponse) -> Vec<VersionInfo> {
82 let mut versions = metadata
83 .versions
84 .into_iter()
85 .filter(|version| !version.yanked)
86 .map(|version| VersionInfo {
87 stable: semver::Version::parse(&version.num)
88 .is_ok_and(|version| version.pre.is_empty()),
89 version: version.num,
90 lts: None,
91 })
92 .collect::<Vec<_>>();
93 versions
94 .sort_by(|left, right| crate::backend::python::cmp_versions(&left.version, &right.version));
95 versions.dedup_by(|left, right| left.version == right.version);
96 versions
97}
98
99fn registry_index(source: &Source) -> Result<String> {
100 let index = source.index_url.clone().ok_or_else(|| {
101 Error::config(format!(
102 "Cargo source `{}` requires a sparse HTTPS index URL",
103 source.id
104 ))
105 })?;
106 validate_registry_index(&index)?;
107 Ok(index)
108}
109
110#[derive(Debug, Deserialize)]
111struct CratesVersion {
112 num: String,
113 #[serde(default)]
114 yanked: bool,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119struct CargoResolution {
120 schema: u32,
121 backend: String,
122 version: String,
123 source_kind: String,
124 source: String,
125 replay: String,
126}
127
128pub struct CargoPackageBackend {
130 id: String,
131 source: CargoSource,
132}
133
134impl CargoPackageBackend {
135 pub fn from_id(id: &str) -> Option<Self> {
136 let id = ToolId::parse(id).ok()?;
137 if id.namespace() != Some("cargo") {
138 return None;
139 }
140 let subject = id.subject().to_string();
141 let source = if subject.starts_with("https://") {
142 CargoSource::Git {
143 url: subject.clone(),
144 }
145 } else {
146 CargoSource::Registry {
147 package: subject.clone(),
148 }
149 };
150 Some(Self {
151 id: id.to_string(),
152 source,
153 })
154 }
155
156 fn runtime_version<'a>(&self, options: &'a BTreeMap<String, String>) -> Result<&'a str> {
157 match (
158 options.get(LOCKED_NATIVE_RUNTIME_OPTION),
159 options.get(LOCKED_NATIVE_RUNTIME_VERSION_OPTION),
160 ) {
161 (Some(runtime), Some(version)) if runtime == "rust" && !version.is_empty() => {
162 Ok(version)
163 }
164 (Some(runtime), _) if runtime != "rust" => Err(Error::config(format!(
165 "Cargo tool `{}` requires managed runtime `rust`, got `{runtime}`",
166 self.id
167 ))),
168 _ => Err(Error::config(format!(
169 "Cargo tool `{}` requires an exact managed Rust selection; add an exact `rust@<version>` request or configuration",
170 self.id
171 ))),
172 }
173 }
174
175 fn runtime_dependency(
176 &self,
177 ctx: &Ctx,
178 options: &BTreeMap<String, String>,
179 ) -> Result<InstallDependency> {
180 let version = self.runtime_version(options)?;
181 let marker = ctx.dirs.install_path("rust", version);
182 if !marker.join(".osdk-complete").is_file() {
183 return Err(Error::NotInstalled {
184 tool: "rust".into(),
185 version: version.into(),
186 });
187 }
188 if marker.join(".osdk-linked").exists() {
189 return Err(Error::config(
190 "Cargo tools require an osdk-managed Rust toolchain; linked Rust toolchains are not reproducible",
191 ));
192 }
193 Ok(InstallDependency {
194 kind: InstallDependencyKind::Runtime,
195 id: "rust".into(),
196 version: version.into(),
197 identity: Some(native_tool::rust_runtime_identity(
198 &ctx.dirs,
199 ctx.platform,
200 version,
201 )?),
202 })
203 }
204
205 fn materials(&self, _ctx: &Ctx, tv: &ToolVersion) -> BTreeMap<String, String> {
206 match &self.source {
207 CargoSource::Registry { package } => BTreeMap::from([
208 ("source-kind".into(), "registry".into()),
209 ("package".into(), package.clone()),
210 (
211 "registry-index".into(),
212 tv.options
213 .get(LOCKED_CARGO_INDEX_OPTION)
214 .cloned()
215 .unwrap_or_else(|| "sparse+https://index.crates.io/".into()),
216 ),
217 ]),
218 CargoSource::Git { url } => BTreeMap::from([
219 ("source-kind".into(), "git".into()),
220 ("git-url".into(), url.clone()),
221 ("git-selector".into(), tv.version.clone()),
222 ]),
223 }
224 }
225
226 fn materials_match(&self, tv: &ToolVersion, actual: &BTreeMap<String, String>) -> bool {
227 match &self.source {
228 CargoSource::Git { url } => {
229 actual
230 == &BTreeMap::from([
231 ("source-kind".into(), "git".into()),
232 ("git-url".into(), url.clone()),
233 ("git-selector".into(), tv.version.clone()),
234 ])
235 }
236 CargoSource::Registry { package } => {
237 if actual.get("source-kind").map(String::as_str) != Some("registry")
238 || actual.get("package").map(String::as_str) != Some(package.as_str())
239 || actual.len() != 3
240 {
241 return false;
242 }
243 let Some(index) = actual.get("registry-index") else {
244 return false;
245 };
246 if validate_registry_index(index).is_err() {
247 return false;
248 }
249 tv.options
250 .get(LOCKED_CARGO_INDEX_OPTION)
251 .is_none_or(|expected| expected == index)
252 }
253 }
254 }
255
256 fn lifecycle(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<NativeToolLifecycle> {
257 let runtime = self.runtime_dependency(ctx, &tv.options)?;
258 let materials = self.materials(ctx, tv);
259 NativeToolLifecycle::new(
260 &ctx.dirs,
261 ctx.platform,
262 &self.id,
263 &tv.version,
264 &tv.options,
265 NativeToolFamily::Cargo,
266 runtime,
267 materials,
268 )
269 }
270
271 fn selected_lifecycle(
272 &self,
273 ctx: &Ctx,
274 tv: &ToolVersion,
275 ) -> Result<Option<NativeToolLifecycle>> {
276 if tv
277 .options
278 .contains_key(LOCKED_NATIVE_RUNTIME_VERSION_OPTION)
279 && tv.options.contains_key(LOCKED_CARGO_INDEX_OPTION)
280 {
281 return self.lifecycle(ctx, tv).map(Some);
282 }
283 let expected_options = crate::backend::dynamic::identity_options(&self.id, &tv.options)?;
284 let expected_materials = self.materials(ctx, tv);
285 let report = crate::inventory::scan_installs(
286 &ctx.dirs.installs,
287 &crate::inventory::ScanOptions::default(),
288 )?;
289 let mut matching = report.installs.into_iter().filter(|install| {
290 let identity = &install.manifest.identity;
291 identity.tool == self.id
292 && identity.version == tv.version
293 && identity.platform == ctx.platform.to_string()
294 && identity.scope == crate::tool::InstallScope::Isolated
295 && identity.material_options == expected_options
296 && (tv.options.contains_key(LOCKED_CARGO_INDEX_OPTION)
297 && identity.materials == expected_materials
298 || !tv.options.contains_key(LOCKED_CARGO_INDEX_OPTION)
299 && self.materials_match(tv, &identity.materials))
300 && native_tool::validate_install_candidate(
301 &ctx.dirs,
302 NativeToolFamily::Cargo,
303 &install.install_root,
304 identity,
305 )
306 .unwrap_or(false)
307 });
308 let first = matching.next();
309 if matching.next().is_some() {
310 return Err(Error::other(format!(
311 "Cargo tool `{}@{}` has multiple matching managed Rust identities; select it through a lockfile",
312 self.id, tv.version
313 )));
314 }
315 first
316 .map(|install| {
317 NativeToolLifecycle::from_identity(
318 &ctx.dirs,
319 NativeToolFamily::Cargo,
320 install.manifest.identity,
321 )
322 })
323 .transpose()
324 }
325
326 fn replay(&self, version: &str) -> &'static str {
327 match &self.source {
328 CargoSource::Registry { .. } => "version-only",
329 CargoSource::Git { .. } if full_revision(version).is_some() => "immutable-revision",
330 CargoSource::Git { .. } => "floating-ref",
331 }
332 }
333
334 async fn registry_selection(&self, ctx: &Ctx, package: &str) -> Result<CargoRegistrySelection> {
335 let sources = crate::source::select::ranked_source_list(ctx, self).await?;
336 let tried = sources.len();
337 let mut last_error = None;
338 if !ctx.config.settings.offline {
339 for source in &sources {
340 if let Err(error) = registry_index(source) {
341 last_error = Some(error);
342 continue;
343 }
344 let url = crate::http::join_url(&source.download_url, package);
345 match fetch_live_crate_metadata(ctx, source, &url).await {
346 Ok(metadata) => {
347 return Ok(CargoRegistrySelection {
348 source: source.clone(),
349 metadata,
350 });
351 }
352 Err(error) => last_error = Some(error),
353 }
354 }
355 }
356
357 for source in sources {
358 if let Err(error) = registry_index(&source) {
359 if ctx.config.settings.offline {
360 last_error = Some(error);
361 }
362 continue;
363 }
364 let url = crate::http::join_url(&source.download_url, package);
365 match read_source_cached_crate_metadata(ctx, &source, &url) {
366 Ok(metadata) => {
367 tracing::warn!(
368 source = %source.id,
369 url,
370 "using stale cached Cargo registry metadata after all live sources failed"
371 );
372 return Ok(CargoRegistrySelection { source, metadata });
373 }
374 Err(_) if !ctx.config.settings.offline => {}
375 Err(_) => {
376 last_error = Some(Error::other(format!(
377 "offline Cargo metadata cache miss for {url}"
378 )));
379 }
380 }
381 }
382 Err(last_error.unwrap_or_else(|| Error::NoUsableSource {
383 tool: self.id.clone(),
384 tried,
385 }))
386 }
387
388 fn resolution(&self, tv: &ToolVersion) -> CargoResolution {
389 let (source_kind, source) = match &self.source {
390 CargoSource::Registry { package } => (
391 "registry",
392 tv.options
393 .get(LOCKED_CARGO_INDEX_OPTION)
394 .cloned()
395 .unwrap_or_else(|| format!("crates.io:{package}")),
396 ),
397 CargoSource::Git { url } => ("git", url.clone()),
398 };
399 CargoResolution {
400 schema: CARGO_RESOLUTION_SCHEMA,
401 backend: self.id.clone(),
402 version: tv.version.clone(),
403 source_kind: source_kind.into(),
404 source,
405 replay: self.replay(&tv.version).into(),
406 }
407 }
408
409 fn resolution_matches(&self, tv: &ToolVersion, actual: &CargoResolution) -> bool {
410 if actual.schema != CARGO_RESOLUTION_SCHEMA
411 || actual.backend != self.id
412 || actual.version != tv.version
413 || actual.replay != self.replay(&tv.version)
414 {
415 return false;
416 }
417 match &self.source {
418 CargoSource::Git { url } => actual.source_kind == "git" && actual.source == *url,
419 CargoSource::Registry { .. } => {
420 actual.source_kind == "registry"
421 && validate_registry_index(&actual.source).is_ok()
422 && tv
423 .options
424 .get(LOCKED_CARGO_INDEX_OPTION)
425 .is_none_or(|expected| expected == &actual.source)
426 }
427 }
428 }
429
430 fn toolchain_bins(
431 &self,
432 ctx: &Ctx,
433 options: &BTreeMap<String, String>,
434 ) -> Result<(PathBuf, PathBuf, PathBuf)> {
435 let version = self.runtime_version(options)?;
436 let root = crate::backend::rust::RustBackend::exact_toolchain_dir_for_dirs(
437 &ctx.dirs,
438 ctx.platform,
439 version,
440 )
441 .ok_or_else(|| Error::NotInstalled {
442 tool: "rust".into(),
443 version: version.into(),
444 })?;
445 let bin = root.join("bin");
446 let cargo = bin.join(format!("cargo{}", ctx.platform.os.exe_suffix()));
447 let rustc = bin.join(format!("rustc{}", ctx.platform.os.exe_suffix()));
448 if !regular_file(&cargo) || !regular_file(&rustc) {
449 return Err(Error::other(format!(
450 "managed Rust toolchain `{version}` is missing Cargo or rustc"
451 )));
452 }
453 Ok((bin, cargo, rustc))
454 }
455
456 fn command_env(
457 &self,
458 ctx: &Ctx,
459 stage: &Path,
460 toolchain_bin: &Path,
461 rustc: &Path,
462 ) -> Result<BTreeMap<OsString, OsString>> {
463 let home = stage.join("home");
464 let cargo_home = stage.join("cargo-home");
465 let target = stage.join("target");
466 let tmp = stage.join("tmp");
467 for path in [&home, &cargo_home, &target, &tmp] {
468 std::fs::create_dir_all(path).map_err(|error| Error::io(path, error))?;
469 }
470 let path = sanitized_provider_path(ctx, toolchain_bin, std::env::var_os("PATH"))?;
471 Ok(BTreeMap::from([
472 (OsString::from("HOME"), home.into_os_string()),
473 (
474 OsString::from("USERPROFILE"),
475 stage.join("home").into_os_string(),
476 ),
477 (OsString::from("CARGO_HOME"), cargo_home.into_os_string()),
478 (OsString::from("CARGO_TARGET_DIR"), target.into_os_string()),
479 (
480 OsString::from("CARGO_INSTALL_ROOT"),
481 stage.as_os_str().to_owned(),
482 ),
483 (
484 OsString::from("RUSTUP_HOME"),
485 ctx.dirs.rustup_home().into_os_string(),
486 ),
487 (OsString::from("RUSTC"), rustc.as_os_str().to_owned()),
488 (OsString::from("PATH"), path),
489 (OsString::from("TMPDIR"), tmp.into_os_string()),
490 (OsString::from("TEMP"), stage.join("tmp").into_os_string()),
491 (OsString::from("TMP"), stage.join("tmp").into_os_string()),
492 (OsString::from("CARGO_TERM_COLOR"), OsString::from("never")),
493 (OsString::from("GIT_TERMINAL_PROMPT"), OsString::from("0")),
494 ]))
495 }
496
497 fn cargo_install_args(&self, tv: &ToolVersion, stage: &Path) -> Result<Vec<OsString>> {
498 let mut args = vec![
499 OsString::from("install"),
500 OsString::from("--root"),
501 stage.as_os_str().to_owned(),
502 OsString::from("--no-track"),
503 ];
504 match &self.source {
505 CargoSource::Registry { package } => {
506 args.push(OsString::from("--version"));
507 args.push(OsString::from(format!("={}", tv.version)));
508 args.push(OsString::from(package));
509 }
510 CargoSource::Git { url } => {
511 args.push(OsString::from("--git"));
512 args.push(OsString::from(url));
513 match git_selector(&tv.version)? {
514 GitSelector::Head => {}
515 GitSelector::Tag(value) => {
516 args.push(OsString::from("--tag"));
517 args.push(OsString::from(value));
518 }
519 GitSelector::Branch(value) => {
520 args.push(OsString::from("--branch"));
521 args.push(OsString::from(value));
522 }
523 GitSelector::Revision(value) => {
524 args.push(OsString::from("--rev"));
525 args.push(OsString::from(value));
526 }
527 }
528 if let Some(package) = tv.options.get("crate") {
529 args.push(OsString::from(package));
530 }
531 }
532 }
533 append_build_options(&mut args, &tv.options);
534 if let Some(index) = tv.options.get(LOCKED_CARGO_INDEX_OPTION) {
535 args.push(OsString::from("--index"));
536 args.push(OsString::from(index));
537 }
538 Ok(args)
539 }
540
541 fn binstall_args(&self, tv: &ToolVersion, stage: &Path) -> Vec<OsString> {
542 let CargoSource::Registry { package } = &self.source else {
543 unreachable!("Git sources are never binstall eligible");
544 };
545 let mut args = vec![
546 OsString::from("--no-confirm"),
547 OsString::from("--disable-telemetry"),
548 OsString::from("--no-discover-github-token"),
549 OsString::from("--disable-strategies"),
550 OsString::from("compile,quick-install"),
551 OsString::from("--no-track"),
552 OsString::from("--root"),
553 stage.as_os_str().to_owned(),
554 OsString::from("--version"),
555 OsString::from(format!("={}", tv.version)),
556 ];
557 if let Some(bin) = tv.options.get("bin") {
558 args.push(OsString::from("--bin"));
559 args.push(OsString::from(bin));
560 }
561 if option_enabled(&tv.options, "locked") {
562 args.push(OsString::from("--locked"));
563 }
564 if let Some(index) = tv.options.get(LOCKED_CARGO_INDEX_OPTION) {
565 args.push(OsString::from("--index"));
566 args.push(OsString::from(index));
567 }
568 args.push(OsString::from(package));
569 args
570 }
571
572 fn binstall_eligible(&self, ctx: &Ctx, tv: &ToolVersion) -> bool {
573 if ctx.config.settings.offline
574 || !matches!(self.source, CargoSource::Registry { .. })
575 || matches!(&self.source, CargoSource::Registry { package } if package == "cargo-binstall")
576 || tv.options.contains_key("features")
577 || tv.options.get("default-features").map(String::as_str) == Some("false")
578 {
579 return false;
580 }
581 controlled_binstall(ctx).is_some()
582 }
583
584 fn run_provider(
585 &self,
586 runner: &dyn CommandRunner,
587 program: &Path,
588 args: Vec<OsString>,
589 env: &BTreeMap<OsString, OsString>,
590 cwd: &Path,
591 provider: &str,
592 ) -> ProviderStatus {
593 let command = CommandSpec::new(program.as_os_str().to_owned())
594 .args(args)
595 .envs(env.clone())
596 .current_dir(cwd)
597 .clear_env();
598 match runner.run_captured(
599 &command,
600 CaptureLimits::new(
601 PROVIDER_TIMEOUT,
602 PROVIDER_OUTPUT_LIMIT,
603 PROVIDER_OUTPUT_LIMIT,
604 ),
605 ) {
606 CommandOutcome::Exited { status, output: _ } if status.success() => {
607 ProviderStatus::Success
608 }
609 CommandOutcome::Exited { status, output } => ProviderStatus::Exit {
610 code: status.code(),
611 error: provider_error(provider, status.to_string(), &output.stderr),
612 },
613 outcome => ProviderStatus::Failure(outcome_error(provider, &outcome)),
614 }
615 }
616
617 async fn install_with_runner(
618 &self,
619 ctx: &Ctx,
620 tv: &ToolVersion,
621 runner: &dyn CommandRunner,
622 ) -> Result<()> {
623 if ctx.config.settings.offline
624 && matches!(self.source, CargoSource::Registry { .. })
625 && !tv.options.contains_key(LOCKED_CARGO_INDEX_OPTION)
626 {
627 if self.selected_lifecycle(ctx, tv)?.is_some() {
628 return Ok(());
629 }
630 return Err(Error::other(format!(
631 "offline Cargo install requires an already complete matching install for `{}`; Cargo native locks do not contain a complete source graph",
632 self.id
633 )));
634 }
635 let lifecycle = self.lifecycle(ctx, tv)?;
636 let NativeToolPreparation::Staged(mut stage) = lifecycle.prepare(&ctx.dirs).await? else {
637 return Ok(());
638 };
639 if ctx.config.settings.offline {
640 return Err(Error::other(format!(
641 "offline Cargo install requires an already complete matching install for `{}`; Cargo native locks do not contain a complete source graph",
642 self.id
643 )));
644 }
645 let (toolchain_bin, _cargo, rustc) = self.toolchain_bins(ctx, &tv.options)?;
646 let stage_root = stage.path().to_path_buf();
647 let env = self.command_env(ctx, &stage_root, &toolchain_bin, &rustc)?;
648
649 if self.binstall_eligible(ctx, tv) {
650 let binstall = controlled_binstall(ctx).expect("eligibility checked");
651 match self.run_provider(
652 runner,
653 &binstall,
654 self.binstall_args(tv, &stage_root),
655 &env,
656 &stage_root,
657 "cargo-binstall",
658 ) {
659 ProviderStatus::Success => {
660 clean_provider_workspace(&stage_root)?;
661 write_resolution(&stage_root, &self.resolution(tv))?;
662 stage.publish(NativeToolProvider::CargoBinstall)?;
663 return Ok(());
664 }
665 ProviderStatus::Exit { code: Some(94), .. } => stage.reset()?,
666 ProviderStatus::Exit { error, .. } | ProviderStatus::Failure(error) => {
667 return Err(error);
668 }
669 }
670 }
671
672 let (toolchain_bin, cargo, rustc) = self.toolchain_bins(ctx, &tv.options)?;
673 let env = self.command_env(ctx, stage.path(), &toolchain_bin, &rustc)?;
674 match self.run_provider(
675 runner,
676 &cargo,
677 self.cargo_install_args(tv, stage.path())?,
678 &env,
679 stage.path(),
680 "cargo install",
681 ) {
682 ProviderStatus::Success => {}
683 ProviderStatus::Exit { error, .. } | ProviderStatus::Failure(error) => {
684 return Err(error)
685 }
686 }
687 clean_provider_workspace(stage.path())?;
688 write_resolution(stage.path(), &self.resolution(tv))?;
689 stage.publish(NativeToolProvider::CargoInstall)?;
690 Ok(())
691 }
692}
693
694enum ProviderStatus {
695 Success,
696 Exit { code: Option<i32>, error: Error },
697 Failure(Error),
698}
699
700enum GitSelector<'a> {
701 Head,
702 Tag(&'a str),
703 Branch(&'a str),
704 Revision(&'a str),
705}
706
707fn git_selector(version: &str) -> Result<GitSelector<'_>> {
708 if version == "latest" {
709 Ok(GitSelector::Head)
710 } else if let Some(value) = version.strip_prefix("tag:") {
711 Ok(GitSelector::Tag(value))
712 } else if let Some(value) = version.strip_prefix("branch:") {
713 Ok(GitSelector::Branch(value))
714 } else if let Some(value) = full_revision(version) {
715 Ok(GitSelector::Revision(value))
716 } else {
717 Err(Error::config(format!(
718 "unsupported Cargo Git selector `{version}`"
719 )))
720 }
721}
722
723fn full_revision(version: &str) -> Option<&str> {
724 let value = version.strip_prefix("rev:")?;
725 (value.len() == 40
726 && value
727 .bytes()
728 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')))
729 .then_some(value)
730}
731
732fn append_build_options(args: &mut Vec<OsString>, options: &BTreeMap<String, String>) {
733 if let Some(features) = options.get("features") {
734 args.push(OsString::from("--features"));
735 args.push(OsString::from(features));
736 }
737 if options.get("default-features").map(String::as_str) == Some("false") {
738 args.push(OsString::from("--no-default-features"));
739 }
740 if let Some(bin) = options.get("bin") {
741 args.push(OsString::from("--bin"));
742 args.push(OsString::from(bin));
743 }
744 if option_enabled(options, "locked") {
745 args.push(OsString::from("--locked"));
746 }
747}
748
749fn option_enabled(options: &BTreeMap<String, String>, name: &str) -> bool {
750 options.get(name).map(String::as_str) == Some("true")
751}
752
753fn sanitized_provider_path(
754 ctx: &Ctx,
755 toolchain_bin: &Path,
756 inherited: Option<OsString>,
757) -> Result<OsString> {
758 let cargo_bin = ctx.dirs.cargo_home().join("bin");
759 let shims = ctx.dirs.shims();
760 let mut paths = vec![toolchain_bin.to_path_buf()];
761 if let Some(inherited) = inherited {
762 for path in std::env::split_paths(&inherited) {
763 if path.as_os_str().is_empty()
764 || path == toolchain_bin
765 || path == cargo_bin
766 || path == shims
767 || paths.iter().any(|existing| existing == &path)
768 {
769 continue;
770 }
771 paths.push(path);
772 }
773 }
774 std::env::join_paths(paths)
775 .map_err(|error| Error::config(format!("invalid sanitized provider PATH: {error}")))
776}
777
778fn controlled_binstall(ctx: &Ctx) -> Option<PathBuf> {
779 let name = format!("cargo-binstall{}", ctx.platform.os.exe_suffix());
780 let path = ctx.dirs.cargo_home().join("bin").join(name);
781 regular_file(&path).then_some(path)
782}
783
784fn clean_provider_workspace(stage: &Path) -> Result<()> {
785 for name in ["home", "cargo-home", "target", "tmp"] {
786 let path = stage.join(name);
787 match std::fs::symlink_metadata(&path) {
788 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
789 return Err(Error::other(format!(
790 "Cargo provider workspace is unsafe: {}",
791 path.display()
792 )));
793 }
794 Ok(_) => std::fs::remove_dir_all(&path).map_err(|error| Error::io(&path, error))?,
795 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
796 Err(error) => return Err(Error::io(&path, error)),
797 }
798 }
799 let crates_metadata = stage.join(".crates.toml");
800 let crates2_metadata = stage.join(".crates2.json");
801 for path in [&crates_metadata, &crates2_metadata] {
802 match std::fs::symlink_metadata(path) {
803 Ok(metadata) if metadata.file_type().is_file() => {
804 std::fs::remove_file(path).map_err(|error| Error::io(path, error))?;
805 }
806 Ok(_) => {
807 return Err(Error::other(format!(
808 "Cargo provider metadata is unsafe: {}",
809 path.display()
810 )));
811 }
812 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
813 Err(error) => return Err(Error::io(path, error)),
814 }
815 }
816 Ok(())
817}
818
819fn write_resolution(root: &Path, resolution: &CargoResolution) -> Result<()> {
820 let path = root.join(CARGO_RESOLUTION_FILE);
821 let bytes = serde_json::to_vec_pretty(resolution)?;
822 let mut options = std::fs::OpenOptions::new();
823 options.write(true).create_new(true);
824 #[cfg(unix)]
825 {
826 use std::os::unix::fs::OpenOptionsExt;
827 options.mode(0o600);
828 }
829 use std::io::Write as _;
830 let mut file = options.open(&path).map_err(|error| {
831 if error.kind() == std::io::ErrorKind::AlreadyExists {
832 Error::other(format!(
833 "Cargo provider wrote reserved metadata path {}",
834 path.display()
835 ))
836 } else {
837 Error::io(&path, error)
838 }
839 })?;
840 file.write_all(&bytes)
841 .map_err(|error| Error::io(&path, error))
842}
843
844fn load_resolution(root: &Path) -> Result<CargoResolution> {
845 let path = root.join(CARGO_RESOLUTION_FILE);
846 let bytes = crate::inventory::read_stable_regular_file(&path, 64 * 1024)
847 .map_err(|error| Error::io(&path, error))?;
848 let resolution: CargoResolution = serde_json::from_slice(&bytes)?;
849 if resolution.schema != CARGO_RESOLUTION_SCHEMA {
850 return Err(Error::config("unsupported Cargo resolution schema"));
851 }
852 Ok(resolution)
853}
854
855fn provider_error(provider: &str, status: String, stderr: &[u8]) -> Error {
856 let stderr = String::from_utf8_lossy(stderr);
857 let stderr = stderr.trim();
858 Error::Command {
859 cmd: provider.into(),
860 status,
861 stderr: (!stderr.is_empty()).then(|| stderr.to_string()),
862 }
863}
864
865fn outcome_error(provider: &str, outcome: &CommandOutcome) -> Error {
866 Error::other(format!("{provider} could not run: {outcome:?}"))
867}
868
869fn regular_file(path: &Path) -> bool {
870 std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_file())
871}
872
873#[async_trait]
874impl Backend for CargoPackageBackend {
875 fn id(&self) -> &str {
876 &self.id
877 }
878
879 fn default_sources(&self) -> Vec<Source> {
880 match self.source {
881 CargoSource::Registry { .. } => vec![
882 Source::official("crates-io", CRATES_IO_API)
883 .with_index("sparse+https://index.crates.io/"),
884 Source::mirror("rsproxy", "https://rsproxy.cn/api/v1/crates", 10)
885 .with_index("sparse+https://rsproxy.cn/index/"),
886 ],
887 CargoSource::Git { .. } => Vec::new(),
888 }
889 }
890
891 fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
892 match &self.source {
893 CargoSource::Registry { package } => {
894 Some(crate::http::join_url(&source.download_url, package))
895 }
896 CargoSource::Git { .. } => None,
897 }
898 }
899
900 async fn list_remote_versions(&self, ctx: &Ctx) -> Result<Vec<VersionInfo>> {
901 let CargoSource::Registry { package } = &self.source else {
902 return Ok(Vec::new());
903 };
904 let selection = self.registry_selection(ctx, package).await?;
905 Ok(version_infos(selection.metadata))
906 }
907
908 async fn resolve_version(&self, ctx: &Ctx, req: &ToolRequest) -> Result<ToolVersion> {
909 let id = ToolId::parse(&req.backend)?;
910 crate::tool::validate_dynamic_selector(&id, Some(&req.spec.to_string()))?;
911 crate::backend::dynamic::validate_options(&self.id, &req.options)?;
912 let (version, selected_source) = match &self.source {
913 CargoSource::Registry { package } => {
914 let (version, source) = match &req.spec {
915 VersionSpec::Exact(version)
916 if req.options.contains_key(LOCKED_CARGO_INDEX_OPTION) =>
917 {
918 validate_registry_index(
919 req.options
920 .get(LOCKED_CARGO_INDEX_OPTION)
921 .expect("checked above"),
922 )?;
923 (version.clone(), None)
924 }
925 VersionSpec::Exact(version) if ctx.config.settings.offline => (
926 version.clone(),
927 Some(crate::source::select::active_source(ctx, self).await?),
928 ),
929 VersionSpec::Exact(version) => {
930 let selection = self.registry_selection(ctx, package).await?;
931 let versions = version_infos(selection.metadata);
932 let exact = crate::version::select_version(
933 &VersionSpec::Exact(version.clone()),
934 &versions,
935 )
936 .ok_or_else(|| Error::VersionResolve {
937 tool: self.id.clone(),
938 spec: req.spec.to_string(),
939 hint: Some("exact Cargo registry release is missing or yanked".into()),
940 })?
941 .version
942 .clone();
943 (exact, Some(selection.source))
944 }
945 VersionSpec::Latest | VersionSpec::Prefix(_) => {
946 let selection = self.registry_selection(ctx, package).await?;
947 let versions = version_infos(selection.metadata);
948 let version = crate::version::select_version(&req.spec, &versions)
949 .ok_or_else(|| Error::VersionResolve {
950 tool: self.id.clone(),
951 spec: req.spec.to_string(),
952 hint: Some("no matching non-yanked crates.io release found".into()),
953 })?
954 .version
955 .clone();
956 (version, Some(selection.source))
957 }
958 _ => {
959 return Err(Error::VersionResolve {
960 tool: self.id.clone(),
961 spec: req.spec.to_string(),
962 hint: Some("Cargo registry tools require latest, an exact version, or a numeric prefix".into()),
963 });
964 }
965 };
966 (version, source)
967 }
968 CargoSource::Git { .. } => match &req.spec {
969 VersionSpec::Latest => ("latest".into(), None),
970 VersionSpec::Prefix(selector) => (selector.clone(), None),
971 VersionSpec::Exact(selector) => (selector.clone(), None),
972 _ => {
973 return Err(Error::VersionResolve {
974 tool: self.id.clone(),
975 spec: req.spec.to_string(),
976 hint: Some(
977 "Cargo Git tools require tag:, branch:, or rev:<40 lowercase hex>"
978 .into(),
979 ),
980 });
981 }
982 },
983 };
984 let mut resolved = ToolVersion::new(&self.id, version);
985 resolved.options = req.options.clone();
986 if let Some(source) = selected_source {
987 resolved
988 .options
989 .insert(LOCKED_CARGO_INDEX_OPTION.into(), registry_index(&source)?);
990 }
991 resolved.options.insert(
992 LOCKED_NATIVE_REPLAY_OPTION.into(),
993 self.replay(&resolved.version).into(),
994 );
995 Ok(resolved)
996 }
997
998 async fn install(&self, ctx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
999 self.install_with_runner(ctx.ctx, tv, &SystemCommandRunner)
1000 .await
1001 }
1002
1003 async fn uninstall(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<()> {
1004 let Some(lifecycle) = self.selected_lifecycle(ctx, tv)? else {
1005 return Ok(());
1006 };
1007 lifecycle.uninstall().await?;
1008 Ok(())
1009 }
1010
1011 fn list_installed(&self, ctx: &Ctx) -> Result<Vec<String>> {
1012 native_tool::list_installed(&ctx.dirs, ctx.platform, NativeToolFamily::Cargo, &self.id)
1013 }
1014
1015 fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
1016 let Some(lifecycle) = self.selected_lifecycle(ctx, tv)? else {
1017 return Ok(Vec::new());
1018 };
1019 Ok(lifecycle
1020 .validate_complete(&ctx.dirs)?
1021 .then(|| lifecycle.install_root().join("bin"))
1022 .into_iter()
1023 .collect())
1024 }
1025
1026 fn bin_names(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<String>> {
1027 let Some(lifecycle) = self.selected_lifecycle(ctx, tv)? else {
1028 return Err(Error::NotInstalled {
1029 tool: self.id.clone(),
1030 version: tv.version.clone(),
1031 });
1032 };
1033 if !lifecycle.validate_complete(&ctx.dirs)? {
1034 return Err(Error::NotInstalled {
1035 tool: self.id.clone(),
1036 version: tv.version.clone(),
1037 });
1038 }
1039 let receipt = native_tool::load_receipt(lifecycle.install_root())?;
1040 Ok(receipt
1041 .bins
1042 .into_iter()
1043 .map(|bin| {
1044 Path::new(&bin.path)
1045 .file_stem()
1046 .and_then(|name| name.to_str())
1047 .unwrap_or_default()
1048 .to_string()
1049 })
1050 .collect())
1051 }
1052
1053 fn dynamic_install_identity(
1054 &self,
1055 ctx: &Ctx,
1056 tv: &ToolVersion,
1057 ) -> Result<Option<InstallIdentity>> {
1058 if !tv
1059 .options
1060 .contains_key(LOCKED_NATIVE_RUNTIME_VERSION_OPTION)
1061 {
1062 return Ok(None);
1063 }
1064 self.lifecycle(ctx, tv)
1065 .map(|lifecycle| Some(lifecycle.identity().clone()))
1066 }
1067
1068 fn validate_dynamic_install(
1069 &self,
1070 ctx: &Ctx,
1071 tv: &ToolVersion,
1072 install_root: &Path,
1073 identity: &InstallIdentity,
1074 ) -> Result<bool> {
1075 if identity.tool != self.id
1076 || identity.version != tv.version
1077 || !self.materials_match(tv, &identity.materials)
1078 || identity.material_options
1079 != crate::backend::dynamic::identity_options(&self.id, &tv.options)?
1080 {
1081 return Ok(false);
1082 }
1083 if !self.resolution_matches(tv, &load_resolution(install_root)?) {
1084 return Ok(false);
1085 }
1086 NativeToolLifecycle::from_identity(&ctx.dirs, NativeToolFamily::Cargo, identity.clone())?
1087 .validate_dynamic_install(&ctx.dirs, install_root, identity)
1088 }
1089}
1090
1091async fn fetch_live_crate_metadata(
1092 ctx: &Ctx,
1093 source: &Source,
1094 url: &str,
1095) -> Result<CratesResponse> {
1096 let cache = crate::http::source_metadata_cache_path(ctx, source, url)?;
1097 let fresh = async {
1098 let response = crate::http::get_source_response(&ctx.client, source, url)
1099 .await?
1100 .error_for_status()
1101 .map_err(|error| Error::network(url, error))?;
1102 if response
1103 .content_length()
1104 .is_some_and(|size| size > CARGO_METADATA_LIMIT as u64)
1105 {
1106 return Err(Error::other(
1107 "Cargo registry metadata exceeds the 8 MiB limit",
1108 ));
1109 }
1110 let mut stream = response.bytes_stream();
1111 let mut bytes = Vec::new();
1112 while let Some(chunk) = stream.next().await {
1113 let chunk = chunk.map_err(|error| Error::network(url, error))?;
1114 if bytes.len().saturating_add(chunk.len()) > CARGO_METADATA_LIMIT {
1115 return Err(Error::other(
1116 "Cargo registry metadata exceeds the 8 MiB limit",
1117 ));
1118 }
1119 bytes.extend_from_slice(&chunk);
1120 }
1121 Ok::<_, Error>(bytes)
1122 };
1123 let bytes = match tokio::time::timeout(CARGO_METADATA_TIMEOUT, fresh).await {
1124 Ok(Ok(bytes)) => bytes,
1125 Ok(Err(error)) => return Err(error),
1126 Err(_) => {
1127 return Err(Error::other(
1128 "Cargo registry metadata exceeded the 30 second timeout",
1129 ))
1130 }
1131 };
1132 let parsed = serde_json::from_slice(&bytes)?;
1133 if let Some(parent) = cache.parent() {
1134 std::fs::create_dir_all(parent).map_err(|error| Error::io(parent, error))?;
1135 }
1136 let serial = NEXT_METADATA_TEMPORARY.fetch_add(1, Ordering::Relaxed);
1137 let temporary = cache.with_extension(format!("tmp-{}-{serial}", std::process::id()));
1138 if std::fs::write(&temporary, &bytes).is_ok() {
1139 let _ = std::fs::rename(&temporary, &cache);
1140 let _ = std::fs::remove_file(&temporary);
1141 }
1142 Ok(parsed)
1143}
1144
1145fn read_source_cached_crate_metadata(
1146 ctx: &Ctx,
1147 source: &Source,
1148 url: &str,
1149) -> Result<CratesResponse> {
1150 let cache = crate::http::source_metadata_cache_path(ctx, source, url)?;
1151 read_cached_crate_metadata(&cache)
1152}
1153
1154fn read_cached_crate_metadata(path: &Path) -> Result<CratesResponse> {
1155 let bytes = crate::inventory::read_stable_regular_file(path, CARGO_METADATA_LIMIT as u64)
1156 .map_err(|error| Error::io(path, error))?;
1157 Ok(serde_json::from_slice(&bytes)?)
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163 use std::io::{Read, Write};
1164 use std::net::TcpListener;
1165 use std::sync::mpsc;
1166 use std::sync::{Arc, Mutex};
1167 use std::thread;
1168
1169 use crate::config::{Config, Settings};
1170 use crate::dirs::Dirs;
1171 use crate::platform::Platform;
1172 use crate::process::CapturedOutput;
1173 use crate::store::Cas;
1174
1175 fn context(root: &Path, offline: bool) -> Ctx {
1176 let dirs = Dirs::resolve_from(|key| match key {
1177 "OSDK_DATA_DIR" => Some(root.join("data").display().to_string()),
1178 "OSDK_CACHE_DIR" => Some(root.join("cache").display().to_string()),
1179 "OSDK_CONFIG_DIR" => Some(root.join("config").display().to_string()),
1180 "OSDK_STORE_DIR" => Some(root.join("store").display().to_string()),
1181 "OSDK_INSTALL_DIR" => Some(root.join("installs").display().to_string()),
1182 _ => None,
1183 })
1184 .unwrap();
1185 dirs.ensure().unwrap();
1186 Ctx {
1187 cas: Arc::new(Cas::new(dirs.store.clone())),
1188 dirs,
1189 platform: Platform::current(),
1190 config: Config {
1191 settings: Settings {
1192 offline,
1193 ..Default::default()
1194 },
1195 sources: Default::default(),
1196 tools: Default::default(),
1197 tool_configs: Default::default(),
1198 global_tools: Default::default(),
1199 global_tool_configs: Default::default(),
1200 tool_origins: Default::default(),
1201 aliases: Default::default(),
1202 project_config_path: None,
1203 },
1204 client: reqwest::Client::new(),
1205 show_progress: false,
1206 }
1207 }
1208
1209 struct MetadataServer {
1210 base_url: String,
1211 requests: Arc<Mutex<Vec<String>>>,
1212 shutdown: Option<mpsc::Sender<()>>,
1213 handle: Option<thread::JoinHandle<()>>,
1214 }
1215
1216 impl MetadataServer {
1217 fn start(responses: Vec<(&'static str, &'static str, &'static str)>) -> Self {
1218 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1219 listener.set_nonblocking(true).unwrap();
1220 let base_url = format!("http://{}", listener.local_addr().unwrap());
1221 let requests = Arc::new(Mutex::new(Vec::new()));
1222 let server_requests = Arc::clone(&requests);
1223 let (shutdown, shutdown_rx) = mpsc::channel();
1224 let handle = thread::spawn(move || loop {
1225 if shutdown_rx.try_recv().is_ok() {
1226 break;
1227 }
1228 match listener.accept() {
1229 Ok((mut stream, _)) => {
1230 stream.set_nonblocking(false).unwrap();
1234 stream
1235 .set_read_timeout(Some(Duration::from_secs(2)))
1236 .unwrap();
1237 let mut request = Vec::new();
1238 let mut buffer = [0u8; 1024];
1239 while !request.ends_with(b"\r\n\r\n") {
1240 let read = stream.read(&mut buffer).unwrap();
1241 if read == 0 {
1242 break;
1243 }
1244 request.extend_from_slice(&buffer[..read]);
1245 }
1246 let request = String::from_utf8(request).unwrap();
1247 let path = request
1248 .lines()
1249 .next()
1250 .and_then(|line| line.split_whitespace().nth(1))
1251 .unwrap_or("/")
1252 .to_string();
1253 server_requests.lock().unwrap().push(path.clone());
1254 let (status, body) = responses
1255 .iter()
1256 .find(|(expected, _, _)| *expected == path)
1257 .map(|(_, status, body)| (*status, *body))
1258 .unwrap_or(("404 Not Found", ""));
1259 write!(
1260 stream,
1261 "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1262 body.len()
1263 )
1264 .unwrap();
1265 }
1266 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1267 thread::sleep(Duration::from_millis(1));
1268 }
1269 Err(error) => panic!("metadata server failed: {error}"),
1270 }
1271 });
1272 Self {
1273 base_url,
1274 requests,
1275 shutdown: Some(shutdown),
1276 handle: Some(handle),
1277 }
1278 }
1279
1280 fn requests(&self) -> Vec<String> {
1281 self.requests.lock().unwrap().clone()
1282 }
1283 }
1284
1285 impl Drop for MetadataServer {
1286 fn drop(&mut self) {
1287 if let Some(shutdown) = self.shutdown.take() {
1288 let _ = shutdown.send(());
1289 }
1290 if let Some(handle) = self.handle.take() {
1291 handle.join().unwrap();
1292 }
1293 }
1294 }
1295
1296 fn configure_registry_sources(
1297 ctx: &mut Ctx,
1298 backend: &CargoPackageBackend,
1299 base_url: &str,
1300 ) -> (Source, Source) {
1301 let preferred = Source::mirror("preferred", &format!("{base_url}/preferred"), 0)
1302 .with_index("sparse+https://preferred.example.test/index/");
1303 let fallback = Source::mirror("fallback", &format!("{base_url}/fallback"), 10)
1304 .with_index("sparse+https://fallback.example.test/index/");
1305 ctx.config.sources.selection = crate::source::Selection::Ordered;
1306 ctx.config.sources.per_tool.insert(
1307 backend.id().into(),
1308 crate::config::ToolSources {
1309 disable: vec!["crates-io".into(), "rsproxy".into()],
1310 custom: vec![preferred.clone(), fallback.clone()],
1311 ..Default::default()
1312 },
1313 );
1314 (preferred, fallback)
1315 }
1316
1317 fn write_crate_metadata_cache(ctx: &Ctx, source: &Source, package: &str, body: &[u8]) {
1318 let url = crate::http::join_url(&source.download_url, package);
1319 let cache = crate::http::source_metadata_cache_path(ctx, source, &url).unwrap();
1320 std::fs::create_dir_all(cache.parent().unwrap()).unwrap();
1321 std::fs::write(cache, body).unwrap();
1322 }
1323
1324 fn write_executable(path: &Path, bytes: &[u8]) {
1325 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1326 std::fs::write(path, bytes).unwrap();
1327 #[cfg(unix)]
1328 {
1329 use std::os::unix::fs::PermissionsExt;
1330 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
1331 }
1332 }
1333
1334 fn managed_rust(ctx: &Ctx, version: &str) {
1335 let root = ctx.dirs.rustup_home().join("toolchains").join(version);
1336 write_executable(
1337 &root
1338 .join("bin")
1339 .join(format!("cargo{}", ctx.platform.os.exe_suffix())),
1340 b"cargo",
1341 );
1342 write_executable(
1343 &root
1344 .join("bin")
1345 .join(format!("rustc{}", ctx.platform.os.exe_suffix())),
1346 b"rustc",
1347 );
1348 let rustlib = root
1349 .join("lib/rustlib")
1350 .join(ctx.platform.llvm_triple())
1351 .join("lib");
1352 std::fs::create_dir_all(&rustlib).unwrap();
1353 std::fs::write(rustlib.join("libstd-fixture.rlib"), b"std").unwrap();
1354 std::fs::write(root.join("lib/librustc_driver-fixture.so"), b"driver").unwrap();
1355 std::fs::write(
1356 root.join("lib/rustlib/manifest-rustc-fixture"),
1357 format!("file:bin/rustc{}", ctx.platform.os.exe_suffix()),
1358 )
1359 .unwrap();
1360 std::fs::write(
1361 root.join("lib/rustlib/manifest-rust-std-fixture"),
1362 b"file:libstd-fixture.rlib",
1363 )
1364 .unwrap();
1365 std::fs::write(
1366 root.join("lib/rustlib/manifest-cargo-fixture"),
1367 format!("file:bin/cargo{}", ctx.platform.os.exe_suffix()),
1368 )
1369 .unwrap();
1370 let marker = ctx.dirs.install_path("rust", version);
1371 std::fs::create_dir_all(&marker).unwrap();
1372 std::fs::write(marker.join(".osdk-complete"), b"").unwrap();
1373 }
1374
1375 fn version(backend: &CargoPackageBackend, runtime: &str) -> ToolVersion {
1376 let mut version = ToolVersion::new(backend.id(), "14.1.1");
1377 version.options.extend(BTreeMap::from([
1378 (LOCKED_NATIVE_RUNTIME_OPTION.into(), "rust".into()),
1379 (LOCKED_NATIVE_RUNTIME_VERSION_OPTION.into(), runtime.into()),
1380 (LOCKED_NATIVE_REPLAY_OPTION.into(), "version-only".into()),
1381 ]));
1382 version
1383 }
1384
1385 #[derive(Clone)]
1386 struct FixtureRunner {
1387 calls: Arc<Mutex<Vec<CommandSpec>>>,
1388 statuses: Arc<Mutex<Vec<i32>>>,
1389 forge_resolution: bool,
1390 }
1391
1392 impl FixtureRunner {
1393 fn new(statuses: impl IntoIterator<Item = i32>) -> Self {
1394 Self {
1395 calls: Arc::new(Mutex::new(Vec::new())),
1396 statuses: Arc::new(Mutex::new(statuses.into_iter().collect())),
1397 forge_resolution: false,
1398 }
1399 }
1400
1401 fn forging_resolution(statuses: impl IntoIterator<Item = i32>) -> Self {
1402 Self {
1403 forge_resolution: true,
1404 ..Self::new(statuses)
1405 }
1406 }
1407 }
1408
1409 impl CommandRunner for FixtureRunner {
1410 fn run_captured(&self, command: &CommandSpec, _limits: CaptureLimits) -> CommandOutcome {
1411 self.calls.lock().unwrap().push(command.clone());
1412 let stage = command.working_directory().unwrap();
1413 std::fs::create_dir_all(stage.join("bin")).unwrap();
1414 let code = self.statuses.lock().unwrap().remove(0);
1415 if code == 0 {
1416 write_executable(
1417 &stage
1418 .join("bin")
1419 .join(if cfg!(windows) { "rg.exe" } else { "rg" }),
1420 b"fixture binary",
1421 );
1422 if self.forge_resolution {
1423 std::fs::write(stage.join(CARGO_RESOLUTION_FILE), b"{}").unwrap();
1424 }
1425 } else {
1426 std::fs::write(stage.join("partial"), b"provider partial").unwrap();
1427 }
1428 exited(code)
1429 }
1430
1431 fn run_foreground(
1432 &self,
1433 _command: &CommandSpec,
1434 ) -> std::io::Result<std::process::ExitStatus> {
1435 unreachable!()
1436 }
1437 }
1438
1439 #[cfg(unix)]
1440 fn exited(code: i32) -> CommandOutcome {
1441 use std::os::unix::process::ExitStatusExt;
1442 CommandOutcome::Exited {
1443 status: std::process::ExitStatus::from_raw(code << 8),
1444 output: CapturedOutput::default(),
1445 }
1446 }
1447
1448 #[cfg(windows)]
1449 fn exited(code: i32) -> CommandOutcome {
1450 use std::os::windows::process::ExitStatusExt;
1451 CommandOutcome::Exited {
1452 status: std::process::ExitStatus::from_raw(code as u32),
1453 output: CapturedOutput::default(),
1454 }
1455 }
1456
1457 #[test]
1458 fn factory_accepts_registry_and_canonical_git_ids() {
1459 assert_eq!(
1460 CargoPackageBackend::from_id("cargo:RipGrep").unwrap().id(),
1461 "cargo:ripgrep"
1462 );
1463 assert!(CargoPackageBackend::from_id("cargo:https://github.com/acme/tool.git").is_some());
1464 assert!(CargoPackageBackend::from_id("cargo:http://example.test/tool").is_none());
1465 }
1466
1467 #[tokio::test]
1468 async fn exact_registry_resolution_is_network_free_and_records_replay() {
1469 let temp = tempfile::tempdir().unwrap();
1470 let ctx = context(temp.path(), true);
1471 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1472 let request = ToolRequest::parse("cargo:ripgrep@14.1.1").unwrap();
1473 let resolved = backend.resolve_version(&ctx, &request).await.unwrap();
1474 assert_eq!(resolved.version, "14.1.1");
1475 assert_eq!(
1476 resolved.options[LOCKED_NATIVE_REPLAY_OPTION],
1477 "version-only"
1478 );
1479 assert_eq!(
1480 resolved.options[LOCKED_CARGO_INDEX_OPTION],
1481 "sparse+https://index.crates.io/"
1482 );
1483 }
1484
1485 #[tokio::test]
1486 async fn online_exact_registry_resolution_requires_non_yanked_metadata_evidence() {
1487 let server = MetadataServer::start(vec![
1488 ("/preferred/ripgrep", "503 Service Unavailable", ""),
1489 ("/fallback/ripgrep", "503 Service Unavailable", ""),
1490 ]);
1491 let temp = tempfile::tempdir().unwrap();
1492 let mut ctx = context(temp.path(), false);
1493 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1494 let (preferred, _) = configure_registry_sources(&mut ctx, &backend, &server.base_url);
1495 write_crate_metadata_cache(
1496 &ctx,
1497 &preferred,
1498 "ripgrep",
1499 br#"{"versions":[{"num":"14.1.1","yanked":false},{"num":"14.1.0","yanked":true}]}"#,
1500 );
1501
1502 let exact = backend
1503 .resolve_version(&ctx, &ToolRequest::parse("cargo:ripgrep@14.1.1").unwrap())
1504 .await
1505 .unwrap();
1506 assert_eq!(exact.version, "14.1.1");
1507 assert_eq!(
1508 exact.options[LOCKED_CARGO_INDEX_OPTION],
1509 "sparse+https://preferred.example.test/index/"
1510 );
1511
1512 for missing in ["14.1.0", "99.0.0"] {
1513 let error = backend
1514 .resolve_version(
1515 &ctx,
1516 &ToolRequest::parse(&format!("cargo:ripgrep@{missing}")).unwrap(),
1517 )
1518 .await
1519 .unwrap_err();
1520 assert!(error.to_string().contains("missing or yanked"), "{error}");
1521 }
1522 assert_eq!(
1523 server.requests(),
1524 vec![
1525 "/preferred/ripgrep",
1526 "/fallback/ripgrep",
1527 "/preferred/ripgrep",
1528 "/fallback/ripgrep",
1529 "/preferred/ripgrep",
1530 "/fallback/ripgrep",
1531 ]
1532 );
1533 }
1534
1535 #[tokio::test]
1536 async fn latest_live_fallback_beats_preferred_stale_metadata() {
1537 let server = MetadataServer::start(vec![
1538 ("/preferred/ripgrep", "503 Service Unavailable", ""),
1539 (
1540 "/fallback/ripgrep",
1541 "200 OK",
1542 r#"{"versions":[{"num":"14.1.1","yanked":false}]}"#,
1543 ),
1544 ]);
1545 let temp = tempfile::tempdir().unwrap();
1546 let mut ctx = context(temp.path(), false);
1547 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1548 let (preferred, _) = configure_registry_sources(&mut ctx, &backend, &server.base_url);
1549 write_crate_metadata_cache(
1550 &ctx,
1551 &preferred,
1552 "ripgrep",
1553 br#"{"versions":[{"num":"99.0.0","yanked":false}]}"#,
1554 );
1555
1556 let resolved = backend
1557 .resolve_version(&ctx, &ToolRequest::parse("cargo:ripgrep@latest").unwrap())
1558 .await
1559 .unwrap();
1560
1561 assert_eq!(resolved.version, "14.1.1");
1562 assert_eq!(
1563 resolved.options[LOCKED_CARGO_INDEX_OPTION],
1564 "sparse+https://fallback.example.test/index/"
1565 );
1566 assert_eq!(
1567 server.requests(),
1568 vec!["/preferred/ripgrep", "/fallback/ripgrep"]
1569 );
1570 }
1571
1572 #[tokio::test]
1573 async fn exact_live_fallback_yank_state_beats_preferred_stale_metadata() {
1574 let server = MetadataServer::start(vec![
1575 ("/preferred/ripgrep", "503 Service Unavailable", ""),
1576 (
1577 "/fallback/ripgrep",
1578 "200 OK",
1579 r#"{"versions":[{"num":"14.1.1","yanked":true}]}"#,
1580 ),
1581 ]);
1582 let temp = tempfile::tempdir().unwrap();
1583 let mut ctx = context(temp.path(), false);
1584 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1585 let (preferred, _) = configure_registry_sources(&mut ctx, &backend, &server.base_url);
1586 write_crate_metadata_cache(
1587 &ctx,
1588 &preferred,
1589 "ripgrep",
1590 br#"{"versions":[{"num":"14.1.1","yanked":false}]}"#,
1591 );
1592
1593 let error = backend
1594 .resolve_version(&ctx, &ToolRequest::parse("cargo:ripgrep@14.1.1").unwrap())
1595 .await
1596 .unwrap_err();
1597
1598 assert!(error.to_string().contains("missing or yanked"), "{error}");
1599 assert_eq!(
1600 server.requests(),
1601 vec!["/preferred/ripgrep", "/fallback/ripgrep"]
1602 );
1603 }
1604
1605 #[tokio::test]
1606 async fn stale_fallback_preserves_the_cached_sources_index() {
1607 let server = MetadataServer::start(vec![
1608 ("/preferred/ripgrep", "503 Service Unavailable", ""),
1609 ("/fallback/ripgrep", "503 Service Unavailable", ""),
1610 ]);
1611 let temp = tempfile::tempdir().unwrap();
1612 let mut ctx = context(temp.path(), false);
1613 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1614 let (preferred, fallback) =
1615 configure_registry_sources(&mut ctx, &backend, &server.base_url);
1616 write_crate_metadata_cache(&ctx, &preferred, "ripgrep", b"invalid json");
1617 write_crate_metadata_cache(
1618 &ctx,
1619 &fallback,
1620 "ripgrep",
1621 br#"{"versions":[{"num":"13.0.0","yanked":false}]}"#,
1622 );
1623
1624 let resolved = backend
1625 .resolve_version(&ctx, &ToolRequest::parse("cargo:ripgrep@latest").unwrap())
1626 .await
1627 .unwrap();
1628
1629 assert_eq!(resolved.version, "13.0.0");
1630 assert_eq!(
1631 resolved.options[LOCKED_CARGO_INDEX_OPTION],
1632 "sparse+https://fallback.example.test/index/"
1633 );
1634 assert_eq!(
1635 server.requests(),
1636 vec!["/preferred/ripgrep", "/fallback/ripgrep"]
1637 );
1638 }
1639
1640 #[tokio::test]
1641 async fn reconstructed_invalid_cargo_selector_is_rejected_before_provider_work() {
1642 let temp = tempfile::tempdir().unwrap();
1643 let ctx = context(temp.path(), true);
1644 let backend =
1645 CargoPackageBackend::from_id("cargo:https://github.com/acme/tool.git").unwrap();
1646 let request = ToolRequest {
1647 backend: backend.id().into(),
1648 spec: VersionSpec::Prefix("branch:bad..ref".into()),
1649 options: BTreeMap::new(),
1650 };
1651 assert!(backend.resolve_version(&ctx, &request).await.is_err());
1652 }
1653
1654 #[test]
1655 fn git_replay_classifies_full_revisions_and_floating_refs_honestly() {
1656 let backend =
1657 CargoPackageBackend::from_id("cargo:https://github.com/acme/tool.git").unwrap();
1658 assert_eq!(
1659 backend.replay("rev:0123456789abcdef0123456789abcdef01234567"),
1660 "immutable-revision"
1661 );
1662 assert_eq!(backend.replay("branch:main"), "floating-ref");
1663 assert_eq!(backend.replay("tag:v1.0.0"), "floating-ref");
1664 }
1665
1666 #[test]
1667 fn provider_path_keeps_system_helpers_but_filters_managed_proxies() {
1668 let temp = tempfile::tempdir().unwrap();
1669 let ctx = context(temp.path(), false);
1670 let toolchain = temp.path().join("toolchain/bin");
1671 let system = temp.path().join("system/bin");
1672 let inherited = std::env::join_paths([
1673 ctx.dirs.shims(),
1674 ctx.dirs.cargo_home().join("bin"),
1675 system.clone(),
1676 toolchain.clone(),
1677 system.clone(),
1678 ])
1679 .unwrap();
1680 let actual = sanitized_provider_path(&ctx, &toolchain, Some(inherited)).unwrap();
1681 assert_eq!(
1682 std::env::split_paths(&actual).collect::<Vec<_>>(),
1683 vec![toolchain, system]
1684 );
1685 }
1686
1687 #[test]
1688 fn case_distinct_git_urls_have_distinct_identity_roots_and_locks() {
1689 let temp = tempfile::tempdir().unwrap();
1690 let ctx = context(temp.path(), false);
1691 managed_rust(&ctx, "1.91.1");
1692 let upper = CargoPackageBackend::from_id("cargo:https://github.com/Acme/tool.git").unwrap();
1693 let lower = CargoPackageBackend::from_id("cargo:https://github.com/acme/tool.git").unwrap();
1694 let mut upper_version = version(&upper, "1.91.1");
1695 upper_version.version = "rev:0123456789abcdef0123456789abcdef01234567".into();
1696 upper_version.options.insert(
1697 LOCKED_NATIVE_REPLAY_OPTION.into(),
1698 "immutable-revision".into(),
1699 );
1700 let mut lower_version = version(&lower, "1.91.1");
1701 lower_version.version = upper_version.version.clone();
1702 lower_version.options.insert(
1703 LOCKED_NATIVE_REPLAY_OPTION.into(),
1704 "immutable-revision".into(),
1705 );
1706 let upper = upper.lifecycle(&ctx, &upper_version).unwrap();
1707 let lower = lower.lifecycle(&ctx, &lower_version).unwrap();
1708 assert_ne!(upper.identity().install_id, lower.identity().install_id);
1709 assert_ne!(upper.install_root(), lower.install_root());
1710 assert_ne!(upper.lock_path(), lower.lock_path());
1711 }
1712
1713 #[tokio::test]
1714 async fn binstall_success_uses_controlled_binary_and_isolated_environment() {
1715 let temp = tempfile::tempdir().unwrap();
1716 let ctx = context(temp.path(), false);
1717 managed_rust(&ctx, "1.91.1");
1718 let binstall = ctx
1719 .dirs
1720 .cargo_home()
1721 .join("bin")
1722 .join(format!("cargo-binstall{}", ctx.platform.os.exe_suffix()));
1723 write_executable(&binstall, b"binstall");
1724 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1725 let version = version(&backend, "1.91.1");
1726 let runner = FixtureRunner::new([0]);
1727
1728 backend
1729 .install_with_runner(&ctx, &version, &runner)
1730 .await
1731 .unwrap();
1732
1733 let calls = runner.calls.lock().unwrap();
1734 assert_eq!(calls.len(), 1);
1735 let call = &calls[0];
1736 assert_eq!(call.program(), binstall);
1737 assert!(call.environment_is_cleared());
1738 assert!(call.arguments().iter().any(|argument| argument == "--root"));
1739 let args = call
1740 .arguments()
1741 .iter()
1742 .map(|argument| argument.to_string_lossy())
1743 .collect::<Vec<_>>();
1744 assert!(args
1745 .windows(2)
1746 .any(|pair| { pair == ["--disable-strategies", "compile,quick-install"] }));
1747 assert!(args
1748 .iter()
1749 .any(|argument| argument == "--no-discover-github-token"));
1750 for name in ["HOME", "CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC"] {
1751 assert!(call.environment().contains_key(std::ffi::OsStr::new(name)));
1752 }
1753 assert_eq!(
1754 std::env::split_paths(
1755 call.environment()
1756 .get(std::ffi::OsStr::new("PATH"))
1757 .unwrap()
1758 )
1759 .next()
1760 .unwrap(),
1761 backend.toolchain_bins(&ctx, &version.options).unwrap().0
1762 );
1763 let lifecycle = backend.lifecycle(&ctx, &version).unwrap();
1764 let receipt = native_tool::load_receipt(lifecycle.install_root()).unwrap();
1765 assert_eq!(receipt.provider, NativeToolProvider::CargoBinstall);
1766 }
1767
1768 #[tokio::test]
1769 async fn exit_94_resets_partial_output_then_falls_back_once() {
1770 let temp = tempfile::tempdir().unwrap();
1771 let ctx = context(temp.path(), false);
1772 managed_rust(&ctx, "1.91.1");
1773 write_executable(
1774 &ctx.dirs
1775 .cargo_home()
1776 .join("bin")
1777 .join(format!("cargo-binstall{}", ctx.platform.os.exe_suffix())),
1778 b"binstall",
1779 );
1780 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1781 let version = version(&backend, "1.91.1");
1782 let runner = FixtureRunner::new([94, 0]);
1783
1784 backend
1785 .install_with_runner(&ctx, &version, &runner)
1786 .await
1787 .unwrap();
1788
1789 assert_eq!(runner.calls.lock().unwrap().len(), 2);
1790 let lifecycle = backend.lifecycle(&ctx, &version).unwrap();
1791 assert!(!lifecycle.install_root().join("partial").exists());
1792 let receipt = native_tool::load_receipt(lifecycle.install_root()).unwrap();
1793 assert_eq!(receipt.provider, NativeToolProvider::CargoInstall);
1794 }
1795
1796 #[tokio::test]
1797 async fn non_94_binstall_failure_is_terminal_and_does_not_publish() {
1798 let temp = tempfile::tempdir().unwrap();
1799 let ctx = context(temp.path(), false);
1800 managed_rust(&ctx, "1.91.1");
1801 write_executable(
1802 &ctx.dirs
1803 .cargo_home()
1804 .join("bin")
1805 .join(format!("cargo-binstall{}", ctx.platform.os.exe_suffix())),
1806 b"binstall",
1807 );
1808 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1809 let version = version(&backend, "1.91.1");
1810 let runner = FixtureRunner::new([1]);
1811 let error = backend
1812 .install_with_runner(&ctx, &version, &runner)
1813 .await
1814 .unwrap_err();
1815 assert!(error.to_string().contains("cargo-binstall"));
1816 assert_eq!(runner.calls.lock().unwrap().len(), 1);
1817 assert!(!backend
1818 .lifecycle(&ctx, &version)
1819 .unwrap()
1820 .install_root()
1821 .exists());
1822 }
1823
1824 #[tokio::test]
1825 async fn provider_cannot_forge_cargo_resolution_metadata() {
1826 let temp = tempfile::tempdir().unwrap();
1827 let ctx = context(temp.path(), false);
1828 managed_rust(&ctx, "1.91.1");
1829 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1830 let mut selected = version(&backend, "1.91.1");
1831 selected.options.insert("features".into(), "pcre2".into());
1832 let error = backend
1833 .install_with_runner(&ctx, &selected, &FixtureRunner::forging_resolution([0]))
1834 .await
1835 .unwrap_err();
1836 assert!(error.to_string().contains("reserved metadata path"));
1837 assert!(!backend
1838 .lifecycle(&ctx, &selected)
1839 .unwrap()
1840 .install_root()
1841 .exists());
1842 }
1843
1844 #[tokio::test]
1845 async fn source_options_skip_binstall_and_complete_installs_are_reused() {
1846 let temp = tempfile::tempdir().unwrap();
1847 let ctx = context(temp.path(), false);
1848 managed_rust(&ctx, "1.91.1");
1849 write_executable(
1850 &ctx.dirs
1851 .cargo_home()
1852 .join("bin")
1853 .join(format!("cargo-binstall{}", ctx.platform.os.exe_suffix())),
1854 b"binstall",
1855 );
1856 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1857 let mut version = version(&backend, "1.91.1");
1858 version.options.insert("features".into(), "pcre2".into());
1859 let runner = FixtureRunner::new([0]);
1860 backend
1861 .install_with_runner(&ctx, &version, &runner)
1862 .await
1863 .unwrap();
1864 let cargo = backend.toolchain_bins(&ctx, &version.options).unwrap().1;
1865 assert_eq!(
1866 runner.calls.lock().unwrap()[0].program().to_string_lossy(),
1867 cargo.to_string_lossy()
1868 );
1869
1870 let no_calls = FixtureRunner::new([]);
1871 backend
1872 .install_with_runner(&ctx, &version, &no_calls)
1873 .await
1874 .unwrap();
1875 assert!(no_calls.calls.lock().unwrap().is_empty());
1876 }
1877
1878 #[tokio::test]
1879 async fn offline_reuses_complete_install_but_rejects_cold_install() {
1880 let temp = tempfile::tempdir().unwrap();
1881 let online = context(temp.path(), false);
1882 managed_rust(&online, "1.91.1");
1883 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1884 let selected = version(&backend, "1.91.1");
1885 backend
1886 .install_with_runner(&online, &selected, &FixtureRunner::new([0]))
1887 .await
1888 .unwrap();
1889
1890 let offline = context(temp.path(), true);
1891 backend
1892 .install_with_runner(&offline, &selected, &FixtureRunner::new([]))
1893 .await
1894 .unwrap();
1895
1896 let other = CargoPackageBackend::from_id("cargo:fd-find").unwrap();
1897 let cold = version(&other, "1.91.1");
1898 let error = other
1899 .install_with_runner(&offline, &cold, &FixtureRunner::new([]))
1900 .await
1901 .unwrap_err();
1902 assert!(error.to_string().contains("offline Cargo install"));
1903 }
1904
1905 #[tokio::test]
1906 async fn unlocked_restart_finds_nondefault_registry_install_without_aliasing_sources() {
1907 let temp = tempfile::tempdir().unwrap();
1908 let ctx = context(temp.path(), false);
1909 managed_rust(&ctx, "1.91.1");
1910 let backend = CargoPackageBackend::from_id("cargo:ripgrep").unwrap();
1911 let mut installed = version(&backend, "1.91.1");
1912 installed.options.insert(
1913 LOCKED_CARGO_INDEX_OPTION.into(),
1914 "sparse+https://rsproxy.cn/index/".into(),
1915 );
1916 backend
1917 .install_with_runner(&ctx, &installed, &FixtureRunner::new([0]))
1918 .await
1919 .unwrap();
1920
1921 let mut unlocked = installed.clone();
1922 unlocked.options.remove(LOCKED_CARGO_INDEX_OPTION);
1923 assert_eq!(
1924 backend.bin_names(&ctx, &unlocked).unwrap(),
1925 vec!["rg".to_string()]
1926 );
1927
1928 let mut locked_other = unlocked;
1929 locked_other.options.insert(
1930 LOCKED_CARGO_INDEX_OPTION.into(),
1931 "sparse+https://index.crates.io/".into(),
1932 );
1933 assert!(backend.bin_paths(&ctx, &locked_other).unwrap().is_empty());
1934 }
1935}