Skip to main content

osdk_core/backend/
cargo_package.rs

1//! Cargo package tools installed with an exact osdk-managed Rust toolchain.
2//!
3//! Registry packages and HTTPS Git repositories are addressed by the dynamic
4//! `cargo:` namespace. Provider commands receive absolute managed tool paths
5//! and a private environment, write only to a lifecycle-owned stage, and are
6//! published through [`NativeToolLifecycle`].
7
8use 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
128/// Backend bound to one registry crate or canonical HTTPS Git repository.
129pub 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                        // Accepted sockets inherit nonblocking mode on Windows.
1231                        // Restore blocking reads so a partial request cannot be
1232                        // mistaken for a failed fixture response under Wine/CI.
1233                        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}