diff --git a/crate_universe/private/srcs.bzl b/crate_universe/private/srcs.bzl index 030ff4c092..1f83e2dba7 100644 --- a/crate_universe/private/srcs.bzl +++ b/crate_universe/private/srcs.bzl @@ -29,6 +29,7 @@ CARGO_BAZEL_SRCS = [ Label("//crate_universe:src/rendering/templates/vendor_module.j2"), Label("//crate_universe:src/splicing.rs"), Label("//crate_universe:src/splicing/cargo_config.rs"), + Label("//crate_universe:src/splicing/crate_index_lookup.rs"), Label("//crate_universe:src/splicing/splicer.rs"), Label("//crate_universe:src/test.rs"), Label("//crate_universe:src/utils.rs"), diff --git a/crate_universe/src/cli/splice.rs b/crate_universe/src/cli/splice.rs index cb88df7d2c..207c33e752 100644 --- a/crate_universe/src/cli/splice.rs +++ b/crate_universe/src/cli/splice.rs @@ -98,9 +98,11 @@ pub fn splice(opt: SpliceOptions) -> Result<()> { )?; // Write the registry url info to the manifest now that a lockfile has been generated WorkspaceMetadata::write_registry_urls_and_feature_map( + &cargo, &cargo_lockfile, feature_map, - &manifest_path, + manifest_path.as_path_buf(), + manifest_path.as_path_buf(), )?; let output_dir = opt.output_dir.clone(); diff --git a/crate_universe/src/cli/vendor.rs b/crate_universe/src/cli/vendor.rs index 97da5d390c..21311773a6 100644 --- a/crate_universe/src/cli/vendor.rs +++ b/crate_universe/src/cli/vendor.rs @@ -151,9 +151,11 @@ pub fn vendor(opt: VendorOptions) -> Result<()> { // Write the registry url info to the manifest now that a lockfile has been generated WorkspaceMetadata::write_registry_urls_and_feature_map( + &cargo, &cargo_lockfile, feature_map, - &manifest_path, + manifest_path.as_path_buf(), + manifest_path.as_path_buf(), )?; // Write metadata to the workspace for future reuse diff --git a/crate_universe/src/metadata.rs b/crate_universe/src/metadata.rs index 392c2bff7c..c84bfbb8c1 100644 --- a/crate_universe/src/metadata.rs +++ b/crate_universe/src/metadata.rs @@ -16,6 +16,7 @@ use crate::lockfile::Digest; use anyhow::{anyhow, bail, Context, Result}; use cargo_lock::Lockfile as CargoLockfile; use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand}; +use semver::Version; use crate::config::CrateId; use crate::utils::starlark::SelectList; @@ -75,7 +76,7 @@ impl MetadataGenerator for Generator { let metadata = self .cargo_bin - .metadata_command() + .metadata_command()? .current_dir(manifest_dir) .manifest_path(manifest_path.as_ref()) .other_options(["--locked".to_owned()]) @@ -103,15 +104,20 @@ impl Cargo { } /// Returns a new `Command` for running this cargo. - pub fn command(&self) -> Command { - Command::new(&self.path) + pub fn command(&self) -> Result { + let mut command = Command::new(&self.path); + command.envs(self.env()?); + Ok(command) } /// Returns a new `MetadataCommand` using this cargo. - pub fn metadata_command(&self) -> MetadataCommand { + pub fn metadata_command(&self) -> Result { let mut command = MetadataCommand::new(); command.cargo_path(&self.path); - command + for (k, v) in self.env()? { + command.env(k, v); + } + Ok(command) } /// Returns the output of running `cargo version`, trimming any leading or trailing whitespace. @@ -124,6 +130,28 @@ impl Cargo { } Ok(full_version.clone().unwrap()) } + + pub fn use_sparse_registries_for_crates_io(&self) -> Result { + let full_version = self.full_version()?; + let version_str = full_version.split(' ').nth(1); + if let Some(version_str) = version_str { + let version = Version::parse(version_str).context("Failed to parse cargo version")?; + return Ok(version.major >= 1 && version.minor >= 68); + } + bail!("Couldn't parse cargo version"); + } + + fn env(&self) -> Result> { + let mut map = BTreeMap::new(); + + if self.use_sparse_registries_for_crates_io()? { + map.insert( + "CARGO_REGISTRIES_CRATES_IO_PROTOCOL".into(), + "sparse".into(), + ); + } + Ok(map) + } } /// A configuration desrcibing how to invoke [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html). @@ -192,7 +220,7 @@ impl CargoUpdateRequest { // Simply invoke `cargo update` let output = cargo_bin - .command() + .command()? // Cargo detects config files based on `pwd` when running so // to ensure user provided Cargo config files are used, it's // critical to set the working directory to the manifest dir. @@ -267,7 +295,7 @@ impl LockGenerator { // of having just generated a new one let output = self .cargo_bin - .command() + .command()? // Cargo detects config files based on `pwd` when running so // to ensure user provided Cargo config files are used, it's // critical to set the working directory to the manifest dir. @@ -295,7 +323,7 @@ impl LockGenerator { // Simply invoke `cargo generate-lockfile` let output = self .cargo_bin - .command() + .command()? // Cargo detects config files based on `pwd` when running so // to ensure user provided Cargo config files are used, it's // critical to set the working directory to the manifest dir. @@ -347,7 +375,7 @@ impl VendorGenerator { // Simply invoke `cargo generate-lockfile` let output = self .cargo_bin - .command() + .command()? // Cargo detects config files based on `pwd` when running so // to ensure user provided Cargo config files are used, it's // critical to set the working directory to the manifest dir. @@ -410,7 +438,7 @@ impl FeatureGenerator { // - https://github.com/bazelbuild/rules_rust/issues/1662 let output = self .cargo_bin - .command() + .command()? .current_dir(manifest_dir) .arg("tree") .arg("--locked") diff --git a/crate_universe/src/splicing.rs b/crate_universe/src/splicing.rs index 9833e63a5c..a4651233bd 100644 --- a/crate_universe/src/splicing.rs +++ b/crate_universe/src/splicing.rs @@ -1,6 +1,7 @@ //! This module is responsible for finding a Cargo workspace pub(crate) mod cargo_config; +mod crate_index_lookup; mod splicer; use std::collections::{BTreeMap, BTreeSet}; @@ -9,16 +10,17 @@ use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use cargo_toml::Manifest; -use hex::ToHex; use serde::{Deserialize, Serialize}; use crate::config::CrateId; use crate::metadata::{Cargo, CargoUpdateRequest, LockGenerator}; +use crate::utils; use crate::utils::starlark::{Label, SelectList}; use self::cargo_config::CargoConfig; +use self::crate_index_lookup::CrateIndexLookup; pub use self::splicer::*; type DirectPackageManifest = BTreeMap; @@ -242,11 +244,13 @@ impl WorkspaceMetadata { } pub fn write_registry_urls_and_feature_map( + cargo: &Cargo, lockfile: &cargo_lock::Lockfile, features: BTreeMap>, - manifest_path: &SplicedManifest, + input_manifest_path: &Path, + output_manifest_path: &Path, ) -> Result<()> { - let mut manifest = read_manifest(manifest_path.as_path_buf())?; + let mut manifest = read_manifest(input_manifest_path)?; let mut workspace_metaata = WorkspaceMetadata::try_from( manifest @@ -259,7 +263,7 @@ impl WorkspaceMetadata { .clone(), )?; - // Locate all packages soruced from a registry + // Locate all packages sourced from a registry let pkg_sources: Vec<&cargo_lock::Package> = lockfile .packages .iter() @@ -276,8 +280,7 @@ impl WorkspaceMetadata { // Load the cargo config let cargo_config = { // Note that this path must match the one defined in `splicing::setup_cargo_config` - let config_path = manifest_path - .as_path_buf() + let config_path = input_manifest_path .parent() .unwrap() .join(".cargo") @@ -294,80 +297,77 @@ impl WorkspaceMetadata { let crate_indexes = index_urls .into_iter() .map(|url| { - let index = { - // Ensure the correct registry is mapped based on the give Cargo config. - let index_url = if let Some(config) = &cargo_config { - if let Some(source) = config.get_source_from_url(&url) { - if let Some(replace_with) = &source.replace_with { - if let Some(replacement) = config.get_registry_index_url_by_name(replace_with) { - replacement - } else { - bail!("Tried to replace registry {} with registry named {} but didn't have metadata about the replacement", url, replace_with); - } - } else { - &url - } - } else { - &url - } - } else { - &url + // Ensure the correct registry is mapped based on the give Cargo config. + let index_url = if let Some(config) = &cargo_config { + config.resolve_replacement_url(&url)? + } else { + &url + }; + let index = if cargo.use_sparse_registries_for_crates_io()? + && index_url == utils::CRATES_IO_INDEX_URL + { + CrateIndexLookup::Http(crates_index::SparseIndex::from_url( + "sparse+https://index.crates.io/", + )?) + } else if index_url.starts_with("sparse+https://") { + CrateIndexLookup::Http(crates_index::SparseIndex::from_url(index_url)?) + } else { + let index = { + // Load the index for the current url + let index = + crates_index::Index::from_url(index_url).with_context(|| { + format!("Failed to load index for url: {index_url}") + })?; + + // Ensure each index has a valid index config + index.index_config().with_context(|| { + format!("`config.json` not found in index: {index_url}") + })?; + + index }; - // Load the index for the current url - let index = crates_index::Index::from_url(index_url) - .with_context(|| format!("Failed to load index for url: {index_url}"))?; - - // Ensure each index has a valid index config - index.index_config().with_context(|| { - format!("`config.json` not found in index: {index_url}") - })?; - - index + CrateIndexLookup::Git(index) }; - Ok((url, index)) }) - .collect::>>() + .collect::>>() .context("Failed to locate crate indexes")?; // Get the download URL of each package based on it's registry url. let additional_sources = pkg_sources .iter() - .filter_map(|pkg| { + .map(|pkg| { let source_id = pkg.source.as_ref().unwrap(); - let index = &crate_indexes[&source_id.url().to_string()]; - let index_config = index.index_config().unwrap(); - - index.crate_(pkg.name.as_str()).map(|crate_idx| { - crate_idx - .versions() - .iter() - .find(|v| v.version() == pkg.version.to_string()) - .and_then(|v| { - v.download_url(&index_config).map(|url| { - let crate_id = - CrateId::new(v.name().to_owned(), v.version().to_owned()); - let sha256 = pkg - .checksum - .as_ref() - .and_then(|sum| { - sum.as_sha256().map(|sum| sum.encode_hex::()) - }) - .unwrap_or_else(|| v.checksum().encode_hex::()); - let source_info = SourceInfo { url, sha256 }; - (crate_id, source_info) - }) - }) + let source_url = source_id.url().to_string(); + let lookup = crate_indexes.get(&source_url).ok_or_else(|| { + anyhow!( + "Couldn't find crate_index data for SourceID {:?}", + source_id + ) + })?; + lookup.get_source_info(pkg).map(|source_info| { + ( + CrateId::new(pkg.name.as_str().to_owned(), pkg.version.to_string()), + source_info, + ) }) }) - .flatten(); - - workspace_metaata.sources.extend(additional_sources); + .collect::>>()?; + + workspace_metaata + .sources + .extend( + additional_sources + .into_iter() + .filter_map(|(crate_id, source_info)| { + source_info.map(|source_info| (crate_id, source_info)) + }), + ); workspace_metaata.features = features; workspace_metaata.inject_into(&mut manifest)?; - write_root_manifest(manifest_path.as_path_buf(), manifest)?; + write_root_manifest(output_manifest_path, manifest)?; Ok(()) } diff --git a/crate_universe/src/splicing/cargo_config.rs b/crate_universe/src/splicing/cargo_config.rs index 5f4a9aeeb8..09f715b596 100644 --- a/crate_universe/src/splicing/cargo_config.rs +++ b/crate_universe/src/splicing/cargo_config.rs @@ -5,7 +5,8 @@ use std::fs; use std::path::Path; use std::str::FromStr; -use anyhow::Result; +use crate::utils; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; /// The [`[registry]`](https://doc.rust-lang.org/cargo/reference/config.html#registry) @@ -34,7 +35,7 @@ pub struct Source { /// This is the default registry url per what's defined by Cargo. fn default_registry_url() -> String { - "https://github.com/rust-lang/crates.io-index".to_owned() + utils::CRATES_IO_INDEX_URL.to_owned() } #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -122,7 +123,13 @@ impl CargoConfig { /// Look up a registry [Source] by its url. pub fn get_source_from_url(&self, url: &str) -> Option<&Source> { - self.source.values().find(|v| v.registry == url) + if let Some(found) = self.source.values().find(|v| v.registry == url) { + Some(found) + } else if url == utils::CRATES_IO_INDEX_URL { + self.source.get("crates-io") + } else { + None + } } pub fn get_registry_index_url_by_name(&self, name: &str) -> Option<&str> { @@ -134,6 +141,22 @@ impl CargoConfig { None } } + + pub fn resolve_replacement_url<'a>(&'a self, url: &'a str) -> Result<&'a str> { + if let Some(source) = self.get_source_from_url(url) { + if let Some(replace_with) = &source.replace_with { + if let Some(replacement) = self.get_registry_index_url_by_name(replace_with) { + Ok(replacement) + } else { + bail!("Tried to replace registry {} with registry named {} but didn't have metadata about the replacement", url, replace_with); + } + } else { + Ok(url) + } + } else { + Ok(url) + } + } } #[cfg(test)] @@ -241,4 +264,134 @@ mod test { Some("https://artprod.mycompany/artifactory/git/cargo-remote.git"), ); } + + #[test] + fn registry_settings_get_source_from_url() { + let temp_dir = tempfile::tempdir().unwrap(); + let config = temp_dir.as_ref().join("config.toml"); + + fs::write( + &config, + textwrap::dedent( + r##" + [source.some-mirror] + registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git" + "##, + ), + ) + .unwrap(); + + let config = CargoConfig::try_from_path(&config).unwrap(); + assert_eq!( + config + .get_source_from_url("https://artmirror.mycompany/artifactory/cargo-mirror.git") + .map(|s| s.registry.as_str()), + Some("https://artmirror.mycompany/artifactory/cargo-mirror.git"), + ); + } + + #[test] + fn resolve_replacement_url_no_replacement() { + let temp_dir = tempfile::tempdir().unwrap(); + let config = temp_dir.as_ref().join("config.toml"); + + fs::write(&config, "").unwrap(); + + let config = CargoConfig::try_from_path(&config).unwrap(); + + assert_eq!( + config + .resolve_replacement_url(utils::CRATES_IO_INDEX_URL) + .unwrap(), + utils::CRATES_IO_INDEX_URL + ); + assert_eq!( + config + .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git") + .unwrap(), + "https://artmirror.mycompany/artifactory/cargo-mirror.git" + ); + } + + #[test] + fn resolve_replacement_url_registry() { + let temp_dir = tempfile::tempdir().unwrap(); + let config = temp_dir.as_ref().join("config.toml"); + + fs::write(&config, textwrap::dedent( + r##" + [registries] + art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" } + + [source.crates-io] + replace-with = "some-mirror" + + [source.some-mirror] + registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git" + "##, + )).unwrap(); + + let config = CargoConfig::try_from_path(&config).unwrap(); + assert_eq!( + config + .resolve_replacement_url(utils::CRATES_IO_INDEX_URL) + .unwrap(), + "https://artmirror.mycompany/artifactory/cargo-mirror.git" + ); + assert_eq!( + config + .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git") + .unwrap(), + "https://artmirror.mycompany/artifactory/cargo-mirror.git" + ); + assert_eq!( + config + .resolve_replacement_url( + "https://artprod.mycompany/artifactory/git/cargo-remote.git" + ) + .unwrap(), + "https://artprod.mycompany/artifactory/git/cargo-remote.git" + ); + } + + #[test] + fn resolve_replacement_url_source() { + let temp_dir = tempfile::tempdir().unwrap(); + let config = temp_dir.as_ref().join("config.toml"); + + fs::write(&config, textwrap::dedent( + r##" + [registries] + art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" } + + [source.crates-io] + replace-with = "art-crates-remote" + + [source.some-mirror] + registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git" + "##, + )).unwrap(); + + let config = CargoConfig::try_from_path(&config).unwrap(); + assert_eq!( + config + .resolve_replacement_url(utils::CRATES_IO_INDEX_URL) + .unwrap(), + "https://artprod.mycompany/artifactory/git/cargo-remote.git" + ); + assert_eq!( + config + .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git") + .unwrap(), + "https://artmirror.mycompany/artifactory/cargo-mirror.git" + ); + assert_eq!( + config + .resolve_replacement_url( + "https://artprod.mycompany/artifactory/git/cargo-remote.git" + ) + .unwrap(), + "https://artprod.mycompany/artifactory/git/cargo-remote.git" + ); + } } diff --git a/crate_universe/src/splicing/crate_index_lookup.rs b/crate_universe/src/splicing/crate_index_lookup.rs new file mode 100644 index 0000000000..3ba4fdad45 --- /dev/null +++ b/crate_universe/src/splicing/crate_index_lookup.rs @@ -0,0 +1,153 @@ +use crate::splicing::SourceInfo; +use anyhow::{Context, Result}; +use crates_index::IndexConfig; +use hex::ToHex; + +pub enum CrateIndexLookup { + Git(crates_index::Index), + Http(crates_index::SparseIndex), +} + +impl CrateIndexLookup { + pub fn get_source_info(&self, pkg: &cargo_lock::Package) -> Result> { + let index_config = self + .index_config() + .context("Failed to get crate index config")?; + let crate_ = match self { + // The crates we care about should all be in the cache already, + // because `cargo metadata` ran which should have fetched them. + Self::Http(index) => Some( + index + .crate_from_cache(pkg.name.as_str()) + .with_context(|| format!("Failed to get crate from cache for {pkg:?}"))?, + ), + Self::Git(index) => index.crate_(pkg.name.as_str()), + }; + let source_info = crate_.and_then(|crate_idx| { + crate_idx + .versions() + .iter() + .find(|v| v.version() == pkg.version.to_string()) + .and_then(|v| { + v.download_url(&index_config).map(|url| { + let sha256 = pkg + .checksum + .as_ref() + .and_then(|sum| sum.as_sha256().map(|sum| sum.encode_hex::())) + .unwrap_or_else(|| v.checksum().encode_hex::()); + SourceInfo { url, sha256 } + }) + }) + }); + Ok(source_info) + } + + fn index_config(&self) -> Result { + match self { + Self::Git(index) => index.index_config(), + Self::Http(index) => index.index_config(), + } + } +} + +#[cfg(test)] +mod test { + use crate::splicing::crate_index_lookup::CrateIndexLookup; + use semver::Version; + use std::ffi::OsString; + + // TODO: Avoid global state (env vars) in these tests. + // TODO: These should be separate tests methods but they have conflicting state. + + #[test] + fn sparse_index() { + let runfiles = runfiles::Runfiles::create().unwrap(); + { + let _e = EnvVarResetter::set( + "CARGO_HOME", + runfiles.rlocation( + "rules_rust/crate_universe/test_data/crate_indexes/lazy_static/cargo_home", + ), + ); + + let index = CrateIndexLookup::Http( + crates_index::SparseIndex::from_url("sparse+https://index.crates.io/").unwrap(), + ); + let source_info = index + .get_source_info(&cargo_lock::Package { + name: "lazy_static".parse().unwrap(), + version: Version::parse("1.4.0").unwrap(), + source: None, + checksum: None, + dependencies: Vec::new(), + replace: None, + }) + .unwrap() + .unwrap(); + assert_eq!( + source_info.url, + "https://crates.io/api/v1/crates/lazy_static/1.4.0/download" + ); + assert_eq!( + source_info.sha256, + "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + ); + } + { + let _e = EnvVarResetter::set("CARGO_HOME", runfiles.rlocation("rules_rust/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home")); + + let index = CrateIndexLookup::Http( + crates_index::SparseIndex::from_url("sparse+https://index.crates.io/").unwrap(), + ); + let source_info = index + .get_source_info(&cargo_lock::Package { + name: "lazy_static".parse().unwrap(), + version: Version::parse("1.4.0").unwrap(), + source: None, + checksum: None, + dependencies: Vec::new(), + replace: None, + }) + .unwrap() + .unwrap(); + assert_eq!( + source_info.url, + "https://some-mirror.com/api/v1/crates/lazy_static/1.4.0/download" + ); + assert_eq!( + source_info.sha256, + "fffffffffbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + ); + } + } + + struct EnvVarResetter { + key: OsString, + value: Option, + } + + impl EnvVarResetter { + fn set, V: Into>(key: K, value: V) -> EnvVarResetter { + let key = key.into(); + let value = value.into(); + let old_value = std::env::var_os(&key); + + std::env::set_var(&key, value); + + EnvVarResetter { + key, + value: old_value, + } + } + } + + impl Drop for EnvVarResetter { + fn drop(&mut self) { + if let Some(old_value) = self.value.as_ref() { + std::env::set_var(&self.key, old_value); + } else { + std::env::remove_var(&self.key); + } + } + } +} diff --git a/crate_universe/src/utils.rs b/crate_universe/src/utils.rs index 8953c819a1..c7f12c2258 100644 --- a/crate_universe/src/utils.rs +++ b/crate_universe/src/utils.rs @@ -2,6 +2,8 @@ pub mod starlark; +pub const CRATES_IO_INDEX_URL: &str = "https://github.com/rust-lang/crates.io-index"; + /// Convert a string into a valid crate module name by applying transforms to invalid characters pub fn sanitize_module_name(name: &str) -> String { name.replace('-', "_") diff --git a/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static b/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static new file mode 100644 index 0000000000..2fb8101cac Binary files /dev/null and b/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static differ diff --git a/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json b/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json new file mode 100644 index 0000000000..b453be65ff --- /dev/null +++ b/crate_universe/test_data/crate_indexes/lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json @@ -0,0 +1,4 @@ +{ + "dl": "https://crates.io/api/v1/crates", + "api": "https://crates.io" +} diff --git a/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static b/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static new file mode 100644 index 0000000000..78e0c5b62f Binary files /dev/null and b/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/.cache/la/zy/lazy_static differ diff --git a/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json b/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json new file mode 100644 index 0000000000..d623d80e62 --- /dev/null +++ b/crate_universe/test_data/crate_indexes/rewritten_lazy_static/cargo_home/registry/index/index.crates.io-6f17d22bba15001f/config.json @@ -0,0 +1,4 @@ +{ + "dl": "https://some-mirror.com/api/v1/crates", + "api": "https://crates.io" +}