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