• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! This module is responsible for finding a Cargo workspace
2 
3 pub(crate) mod cargo_config;
4 mod crate_index_lookup;
5 mod splicer;
6 
7 use std::collections::{BTreeMap, BTreeSet};
8 use std::convert::TryFrom;
9 use std::fs;
10 use std::path::{Path, PathBuf};
11 use std::str::FromStr;
12 
13 use anyhow::{anyhow, bail, Context, Result};
14 use cargo_lock::package::SourceKind;
15 use cargo_toml::Manifest;
16 use serde::{Deserialize, Serialize};
17 
18 use crate::config::CrateId;
19 use crate::metadata::{Cargo, CargoUpdateRequest, LockGenerator};
20 use crate::select::Select;
21 use crate::utils;
22 use crate::utils::starlark::Label;
23 
24 use self::cargo_config::CargoConfig;
25 use self::crate_index_lookup::CrateIndexLookup;
26 pub(crate) use self::splicer::*;
27 
28 type DirectPackageManifest = BTreeMap<String, cargo_toml::DependencyDetail>;
29 
30 /// A collection of information used for splicing together a new Cargo manifest.
31 #[derive(Debug, Default, Serialize, Deserialize, Clone)]
32 #[serde(deny_unknown_fields)]
33 pub(crate) struct SplicingManifest {
34     /// A set of all packages directly written to the rule
35     pub(crate) direct_packages: DirectPackageManifest,
36 
37     /// A mapping of manifest paths to the labels representing them
38     pub(crate) manifests: BTreeMap<PathBuf, Label>,
39 
40     /// The path of a Cargo config file
41     pub(crate) cargo_config: Option<PathBuf>,
42 
43     /// The Cargo resolver version to use for splicing
44     pub(crate) resolver_version: cargo_toml::Resolver,
45 }
46 
47 impl FromStr for SplicingManifest {
48     type Err = serde_json::Error;
49 
from_str(s: &str) -> Result<Self, Self::Err>50     fn from_str(s: &str) -> Result<Self, Self::Err> {
51         serde_json::from_str(s)
52     }
53 }
54 
55 impl SplicingManifest {
try_from_path<T: AsRef<Path>>(path: T) -> Result<Self>56     pub(crate) fn try_from_path<T: AsRef<Path>>(path: T) -> Result<Self> {
57         let content = fs::read_to_string(path.as_ref())?;
58         Self::from_str(&content).context("Failed to load SplicingManifest")
59     }
60 
resolve(self, workspace_dir: &Path, output_base: &Path) -> Self61     pub(crate) fn resolve(self, workspace_dir: &Path, output_base: &Path) -> Self {
62         let Self {
63             manifests,
64             cargo_config,
65             ..
66         } = self;
67 
68         let workspace_dir_str = workspace_dir.to_string_lossy();
69         let output_base_str = output_base.to_string_lossy();
70 
71         // Ensure manifests all have absolute paths
72         let manifests = manifests
73             .into_iter()
74             .map(|(path, label)| {
75                 let resolved_path = path
76                     .to_string_lossy()
77                     .replace("${build_workspace_directory}", &workspace_dir_str)
78                     .replace("${output_base}", &output_base_str);
79                 (PathBuf::from(resolved_path), label)
80             })
81             .collect();
82 
83         // Ensure the cargo config is located at an absolute path
84         let cargo_config = cargo_config.map(|path| {
85             let resolved_path = path
86                 .to_string_lossy()
87                 .replace("${build_workspace_directory}", &workspace_dir_str)
88                 .replace("${output_base}", &output_base_str);
89             PathBuf::from(resolved_path)
90         });
91 
92         Self {
93             manifests,
94             cargo_config,
95             ..self
96         }
97     }
98 }
99 
100 /// The result of fully resolving a [SplicingManifest] in preparation for splicing.
101 #[derive(Debug, Serialize, Default)]
102 pub(crate) struct SplicingMetadata {
103     /// A set of all packages directly written to the rule
104     pub(crate) direct_packages: DirectPackageManifest,
105 
106     /// A mapping of manifest paths to the labels representing them
107     pub(crate) manifests: BTreeMap<Label, cargo_toml::Manifest>,
108 
109     /// The path of a Cargo config file
110     pub(crate) cargo_config: Option<CargoConfig>,
111 }
112 
113 impl TryFrom<SplicingManifest> for SplicingMetadata {
114     type Error = anyhow::Error;
115 
try_from(value: SplicingManifest) -> Result<Self, Self::Error>116     fn try_from(value: SplicingManifest) -> Result<Self, Self::Error> {
117         let direct_packages = value.direct_packages;
118 
119         let manifests = value
120             .manifests
121             .into_iter()
122             .map(|(path, label)| {
123                 // We read the content of a manifest file to buffer and use `from_slice` to
124                 // parse it. The reason is that the `from_path` version will resolve indirect
125                 // path dependencies in the workspace to absolute path, which causes the hash
126                 // to be unstable. Not resolving implicit data is okay here because the
127                 // workspace manifest is also included in the hash.
128                 // See https://github.com/bazelbuild/rules_rust/issues/2016
129                 let manifest_content = fs::read(&path)
130                     .with_context(|| format!("Failed to load manifest '{}'", path.display()))?;
131                 let manifest = cargo_toml::Manifest::from_slice(&manifest_content)
132                     .with_context(|| format!("Failed to parse manifest '{}'", path.display()))?;
133                 Ok((label, manifest))
134             })
135             .collect::<Result<BTreeMap<Label, Manifest>>>()?;
136 
137         let cargo_config = match value.cargo_config {
138             Some(path) => Some(
139                 CargoConfig::try_from_path(&path)
140                     .with_context(|| format!("Failed to load cargo config '{}'", path.display()))?,
141             ),
142             None => None,
143         };
144 
145         Ok(Self {
146             direct_packages,
147             manifests,
148             cargo_config,
149         })
150     }
151 }
152 
153 #[derive(Debug, Default, Serialize, Deserialize, Clone)]
154 pub(crate) struct SourceInfo {
155     /// A url where to a `.crate` file.
156     pub(crate) url: String,
157 
158     /// The `.crate` file's sha256 checksum.
159     pub(crate) sha256: String,
160 }
161 
162 /// Information about the Cargo workspace relative to the Bazel workspace
163 #[derive(Debug, Default, Serialize, Deserialize)]
164 pub(crate) struct WorkspaceMetadata {
165     /// A mapping of crates to information about where their source can be downloaded
166     pub(crate) sources: BTreeMap<CrateId, SourceInfo>,
167 
168     /// The path from the root of a Bazel workspace to the root of the Cargo workspace
169     pub(crate) workspace_prefix: Option<String>,
170 
171     /// Paths from the root of a Bazel workspace to a Cargo package
172     pub(crate) package_prefixes: BTreeMap<String, String>,
173 
174     /// Feature set for each target triplet and crate.
175     ///
176     /// We store this here because it's computed during the splicing phase via
177     /// calls to "cargo tree" which need the full spliced workspace.
178     pub(crate) features: BTreeMap<CrateId, Select<BTreeSet<String>>>,
179 }
180 
181 impl TryFrom<toml::Value> for WorkspaceMetadata {
182     type Error = anyhow::Error;
183 
try_from(value: toml::Value) -> Result<Self, Self::Error>184     fn try_from(value: toml::Value) -> Result<Self, Self::Error> {
185         match value.get("cargo-bazel") {
186             Some(v) => v
187                 .to_owned()
188                 .try_into()
189                 .context("Failed to deserialize toml value"),
190             None => bail!("cargo-bazel workspace metadata not found"),
191         }
192     }
193 }
194 
195 impl TryFrom<serde_json::Value> for WorkspaceMetadata {
196     type Error = anyhow::Error;
197 
try_from(value: serde_json::Value) -> Result<Self, Self::Error>198     fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
199         match value.get("cargo-bazel") {
200             Some(value) => {
201                 serde_json::from_value(value.to_owned()).context("Faield to deserialize json value")
202             }
203             None => bail!("cargo-bazel workspace metadata not found"),
204         }
205     }
206 }
207 
208 impl WorkspaceMetadata {
new( splicing_manifest: &SplicingManifest, member_manifests: BTreeMap<&PathBuf, String>, ) -> Result<Self>209     fn new(
210         splicing_manifest: &SplicingManifest,
211         member_manifests: BTreeMap<&PathBuf, String>,
212     ) -> Result<Self> {
213         let mut package_prefixes: BTreeMap<String, String> = member_manifests
214             .iter()
215             .filter_map(|(original_manifest, cargo_package_name)| {
216                 let label = match splicing_manifest.manifests.get(*original_manifest) {
217                     Some(v) => v,
218                     None => return None,
219                 };
220 
221                 let package = match label.package() {
222                     Some(package) if !package.is_empty() => PathBuf::from(package),
223                     Some(_) | None => return None,
224                 };
225 
226                 let prefix = package.to_string_lossy().to_string();
227 
228                 Some((cargo_package_name.clone(), prefix))
229             })
230             .collect();
231 
232         // It is invald for toml maps to use empty strings as keys. In the case
233         // the empty key is expected to be the root package. If the root package
234         // has a prefix, then all other packages will as well (even if no other
235         // manifest represents them). The value is then saved as a separate value
236         let workspace_prefix = package_prefixes.remove("");
237 
238         let package_prefixes = package_prefixes
239             .into_iter()
240             .map(|(k, v)| {
241                 let prefix_path = PathBuf::from(v);
242                 let prefix = prefix_path.parent().unwrap();
243                 (k, prefix.to_string_lossy().to_string())
244             })
245             .collect();
246 
247         Ok(Self {
248             sources: BTreeMap::new(),
249             workspace_prefix,
250             package_prefixes,
251             features: BTreeMap::new(),
252         })
253     }
254 
write_registry_urls_and_feature_map( cargo: &Cargo, lockfile: &cargo_lock::Lockfile, features: BTreeMap<CrateId, Select<BTreeSet<String>>>, input_manifest_path: &Path, output_manifest_path: &Path, ) -> Result<()>255     pub(crate) fn write_registry_urls_and_feature_map(
256         cargo: &Cargo,
257         lockfile: &cargo_lock::Lockfile,
258         features: BTreeMap<CrateId, Select<BTreeSet<String>>>,
259         input_manifest_path: &Path,
260         output_manifest_path: &Path,
261     ) -> Result<()> {
262         let mut manifest = read_manifest(input_manifest_path)?;
263 
264         let mut workspace_metaata = WorkspaceMetadata::try_from(
265             manifest
266                 .workspace
267                 .as_ref()
268                 .unwrap()
269                 .metadata
270                 .as_ref()
271                 .unwrap()
272                 .clone(),
273         )?;
274 
275         // Locate all packages sourced from a registry
276         let pkg_sources: Vec<&cargo_lock::Package> = lockfile
277             .packages
278             .iter()
279             .filter(|pkg| pkg.source.is_some())
280             .filter(|pkg| pkg.source.as_ref().unwrap().is_registry())
281             .collect();
282 
283         // Collect a unique set of index urls
284         let index_urls: BTreeSet<(SourceKind, String)> = pkg_sources
285             .iter()
286             .map(|pkg| {
287                 let source = pkg.source.as_ref().unwrap();
288                 (source.kind().clone(), source.url().to_string())
289             })
290             .collect();
291 
292         // Load the cargo config
293         let cargo_config = {
294             // Note that this path must match the one defined in `splicing::setup_cargo_config`
295             let config_path = input_manifest_path
296                 .parent()
297                 .unwrap()
298                 .join(".cargo")
299                 .join("config.toml");
300 
301             if config_path.exists() {
302                 Some(CargoConfig::try_from_path(&config_path)?)
303             } else {
304                 None
305             }
306         };
307 
308         // Load each index for easy access
309         let crate_indexes = index_urls
310             .into_iter()
311             .map(|(source_kind, url)| {
312                 // Ensure the correct registry is mapped based on the give Cargo config.
313                 let index_url = if let Some(config) = &cargo_config {
314                     config.resolve_replacement_url(&url)?
315                 } else {
316                     &url
317                 };
318                 let index = if cargo.use_sparse_registries_for_crates_io()?
319                     && index_url == utils::CRATES_IO_INDEX_URL
320                 {
321                     CrateIndexLookup::Http(crates_index::SparseIndex::from_url(
322                         "sparse+https://index.crates.io/",
323                     )?)
324                 } else if index_url.starts_with("sparse+") {
325                     CrateIndexLookup::Http(crates_index::SparseIndex::from_url(index_url)?)
326                 } else {
327                     match source_kind {
328                         SourceKind::Registry => {
329                             let index = {
330                                 // Load the index for the current url
331                                 let index = crates_index::GitIndex::from_url(index_url)
332                                     .with_context(|| {
333                                         format!("Failed to load index for url: {index_url}")
334                                     })?;
335 
336                                 // Ensure each index has a valid index config
337                                 index.index_config().with_context(|| {
338                                     format!("`config.json` not found in index: {index_url}")
339                                 })?;
340 
341                                 index
342                             };
343                             CrateIndexLookup::Git(index)
344                         }
345                         SourceKind::SparseRegistry => {
346                             CrateIndexLookup::Http(crates_index::SparseIndex::from_url(
347                                 format!("sparse+{}", index_url).as_str(),
348                             )?)
349                         }
350                         unknown => {
351                             return Err(anyhow!(
352                                 "'{:?}' crate index type is not supported (caused by '{}')",
353                                 &unknown,
354                                 url
355                             ));
356                         }
357                     }
358                 };
359                 Ok((url, index))
360             })
361             .collect::<Result<BTreeMap<String, _>>>()
362             .context("Failed to locate crate indexes")?;
363 
364         // Get the download URL of each package based on it's registry url.
365         let additional_sources = pkg_sources
366             .iter()
367             .map(|pkg| {
368                 let source_id = pkg.source.as_ref().unwrap();
369                 let source_url = source_id.url().to_string();
370                 let lookup = crate_indexes.get(&source_url).ok_or_else(|| {
371                     anyhow!(
372                         "Couldn't find crate_index data for SourceID {:?}",
373                         source_id
374                     )
375                 })?;
376                 lookup.get_source_info(pkg).map(|source_info| {
377                     (
378                         CrateId::new(pkg.name.as_str().to_owned(), pkg.version.clone()),
379                         source_info,
380                     )
381                 })
382             })
383             .collect::<Result<Vec<_>>>()?;
384 
385         workspace_metaata
386             .sources
387             .extend(
388                 additional_sources
389                     .into_iter()
390                     .filter_map(|(crate_id, source_info)| {
391                         source_info.map(|source_info| (crate_id, source_info))
392                     }),
393             );
394         workspace_metaata.features = features;
395         workspace_metaata.inject_into(&mut manifest)?;
396 
397         write_root_manifest(output_manifest_path, manifest)?;
398 
399         Ok(())
400     }
401 
inject_into(&self, manifest: &mut Manifest) -> Result<()>402     fn inject_into(&self, manifest: &mut Manifest) -> Result<()> {
403         let metadata_value = toml::Value::try_from(self)?;
404         let workspace = manifest.workspace.as_mut().unwrap();
405 
406         match &mut workspace.metadata {
407             Some(data) => match data.as_table_mut() {
408                 Some(map) => {
409                     map.insert("cargo-bazel".to_owned(), metadata_value);
410                 }
411                 None => bail!("The metadata field is always expected to be a table"),
412             },
413             None => {
414                 let mut table = toml::map::Map::new();
415                 table.insert("cargo-bazel".to_owned(), metadata_value);
416                 workspace.metadata = Some(toml::Value::Table(table))
417             }
418         }
419 
420         Ok(())
421     }
422 }
423 
424 #[derive(Debug)]
425 pub(crate) enum SplicedManifest {
426     Workspace(PathBuf),
427     Package(PathBuf),
428     MultiPackage(PathBuf),
429 }
430 
431 impl SplicedManifest {
as_path_buf(&self) -> &PathBuf432     pub(crate) fn as_path_buf(&self) -> &PathBuf {
433         match self {
434             SplicedManifest::Workspace(p) => p,
435             SplicedManifest::Package(p) => p,
436             SplicedManifest::MultiPackage(p) => p,
437         }
438     }
439 }
440 
read_manifest(manifest: &Path) -> Result<Manifest>441 pub(crate) fn read_manifest(manifest: &Path) -> Result<Manifest> {
442     let content = fs::read_to_string(manifest)?;
443     cargo_toml::Manifest::from_str(content.as_str()).context("Failed to deserialize manifest")
444 }
445 
generate_lockfile( manifest_path: &SplicedManifest, existing_lock: &Option<PathBuf>, cargo_bin: Cargo, rustc_bin: &Path, update_request: &Option<CargoUpdateRequest>, ) -> Result<cargo_lock::Lockfile>446 pub(crate) fn generate_lockfile(
447     manifest_path: &SplicedManifest,
448     existing_lock: &Option<PathBuf>,
449     cargo_bin: Cargo,
450     rustc_bin: &Path,
451     update_request: &Option<CargoUpdateRequest>,
452 ) -> Result<cargo_lock::Lockfile> {
453     let manifest_dir = manifest_path
454         .as_path_buf()
455         .parent()
456         .expect("Every manifest should be contained in a parent directory");
457 
458     let root_lockfile_path = manifest_dir.join("Cargo.lock");
459 
460     // Remove the file so it's not overwitten if it happens to be a symlink.
461     if root_lockfile_path.exists() {
462         fs::remove_file(&root_lockfile_path)?;
463     }
464 
465     // Generate the new lockfile
466     let lockfile = LockGenerator::new(cargo_bin, PathBuf::from(rustc_bin)).generate(
467         manifest_path.as_path_buf(),
468         existing_lock,
469         update_request,
470     )?;
471 
472     // Write the lockfile to disk
473     if !root_lockfile_path.exists() {
474         bail!("Failed to generate Cargo.lock file")
475     }
476 
477     Ok(lockfile)
478 }
479 
480 #[cfg(test)]
481 mod test {
482     use super::*;
483 
484     use std::path::PathBuf;
485 
486     #[test]
deserialize_splicing_manifest()487     fn deserialize_splicing_manifest() {
488         let runfiles = runfiles::Runfiles::create().unwrap();
489         let path = runfiles.rlocation(
490             "rules_rust/crate_universe/test_data/serialized_configs/splicing_manifest.json",
491         );
492 
493         let content = std::fs::read_to_string(path).unwrap();
494 
495         let manifest: SplicingManifest = serde_json::from_str(&content).unwrap();
496 
497         // Check manifests
498         assert_eq!(
499             manifest.manifests,
500             BTreeMap::from([
501                 (
502                     PathBuf::from("${build_workspace_directory}/submod/Cargo.toml"),
503                     Label::from_str("//submod:Cargo.toml").unwrap()
504                 ),
505                 (
506                     PathBuf::from("${output_base}/external_crate/Cargo.toml"),
507                     Label::from_str("@external_crate//:Cargo.toml").unwrap()
508                 ),
509                 (
510                     PathBuf::from("/tmp/abs/path/workspace/Cargo.toml"),
511                     Label::from_str("//:Cargo.toml").unwrap()
512                 ),
513             ])
514         );
515 
516         // Check splicing configs
517         assert_eq!(manifest.resolver_version, cargo_toml::Resolver::V2);
518 
519         // Check packages
520         assert_eq!(manifest.direct_packages.len(), 4);
521         let package = manifest.direct_packages.get("rand").unwrap();
522         assert_eq!(
523             package,
524             &cargo_toml::DependencyDetail {
525                 default_features: false,
526                 features: vec!["small_rng".to_owned()],
527                 version: Some("0.8.5".to_owned()),
528                 ..Default::default()
529             }
530         );
531         let package = manifest.direct_packages.get("cfg-if").unwrap();
532         assert_eq!(
533             package,
534             &cargo_toml::DependencyDetail {
535                 git: Some("https://github.com/rust-lang/cfg-if.git".to_owned()),
536                 rev: Some("b9c2246a".to_owned()),
537                 default_features: true,
538                 ..Default::default()
539             }
540         );
541         let package = manifest.direct_packages.get("log").unwrap();
542         assert_eq!(
543             package,
544             &cargo_toml::DependencyDetail {
545                 git: Some("https://github.com/rust-lang/log.git".to_owned()),
546                 branch: Some("master".to_owned()),
547                 default_features: true,
548                 ..Default::default()
549             }
550         );
551         let package = manifest.direct_packages.get("cargo_toml").unwrap();
552         assert_eq!(
553             package,
554             &cargo_toml::DependencyDetail {
555                 git: Some("https://gitlab.com/crates.rs/cargo_toml.git".to_owned()),
556                 tag: Some("v0.15.2".to_owned()),
557                 default_features: true,
558                 ..Default::default()
559             }
560         );
561 
562         // Check cargo config
563         assert_eq!(
564             manifest.cargo_config,
565             Some(PathBuf::from("/tmp/abs/path/workspace/.cargo/config.toml"))
566         );
567     }
568 
569     #[test]
splicing_manifest_resolve()570     fn splicing_manifest_resolve() {
571         let runfiles = runfiles::Runfiles::create().unwrap();
572         let path = runfiles.rlocation(
573             "rules_rust/crate_universe/test_data/serialized_configs/splicing_manifest.json",
574         );
575 
576         let content = std::fs::read_to_string(path).unwrap();
577 
578         let mut manifest: SplicingManifest = serde_json::from_str(&content).unwrap();
579         manifest.cargo_config = Some(PathBuf::from(
580             "${build_workspace_directory}/.cargo/config.toml",
581         ));
582         manifest = manifest.resolve(
583             &PathBuf::from("/tmp/abs/path/workspace"),
584             &PathBuf::from("/tmp/output_base"),
585         );
586 
587         // Check manifests
588         assert_eq!(
589             manifest.manifests,
590             BTreeMap::from([
591                 (
592                     PathBuf::from("/tmp/abs/path/workspace/submod/Cargo.toml"),
593                     Label::from_str("//submod:Cargo.toml").unwrap()
594                 ),
595                 (
596                     PathBuf::from("/tmp/output_base/external_crate/Cargo.toml"),
597                     Label::from_str("@external_crate//:Cargo.toml").unwrap()
598                 ),
599                 (
600                     PathBuf::from("/tmp/abs/path/workspace/Cargo.toml"),
601                     Label::from_str("//:Cargo.toml").unwrap()
602                 ),
603             ])
604         );
605 
606         // Check cargo config
607         assert_eq!(
608             manifest.cargo_config.unwrap(),
609             PathBuf::from("/tmp/abs/path/workspace/.cargo/config.toml"),
610         )
611     }
612 
613     #[test]
splicing_metadata_workspace_path()614     fn splicing_metadata_workspace_path() {
615         let runfiles = runfiles::Runfiles::create().unwrap();
616         let workspace_manifest_path = runfiles
617             .rlocation("rules_rust/crate_universe/test_data/metadata/workspace_path/Cargo.toml");
618         let workspace_path = workspace_manifest_path.parent().unwrap().to_path_buf();
619         let child_a_manifest_path = runfiles.rlocation(
620             "rules_rust/crate_universe/test_data/metadata/workspace_path/child_a/Cargo.toml",
621         );
622         let child_b_manifest_path = runfiles.rlocation(
623             "rules_rust/crate_universe/test_data/metadata/workspace_path/child_b/Cargo.toml",
624         );
625         let manifest = SplicingManifest {
626             direct_packages: BTreeMap::new(),
627             manifests: BTreeMap::from([
628                 (
629                     workspace_manifest_path,
630                     Label::from_str("//:Cargo.toml").unwrap(),
631                 ),
632                 (
633                     child_a_manifest_path,
634                     Label::from_str("//child_a:Cargo.toml").unwrap(),
635                 ),
636                 (
637                     child_b_manifest_path,
638                     Label::from_str("//child_b:Cargo.toml").unwrap(),
639                 ),
640             ]),
641             cargo_config: None,
642             resolver_version: cargo_toml::Resolver::V2,
643         };
644         let metadata = SplicingMetadata::try_from(manifest).unwrap();
645         let metadata = serde_json::to_string(&metadata).unwrap();
646         assert!(
647             !metadata.contains(workspace_path.to_str().unwrap()),
648             "serialized metadata should not contain absolute path"
649         );
650     }
651 }
652