Skip to main content

cargo_bazel/cli/
splice.rs

1//! The cli entrypoint for the `splice` subcommand
2
3use std::fs::File;
4use std::path::PathBuf;
5use std::process::Stdio;
6
7use anyhow::Context;
8use camino::Utf8PathBuf;
9use clap::Parser;
10use itertools::Itertools;
11
12use crate::cli::Result;
13use crate::config::Config;
14use crate::metadata::{Cargo, CargoUpdateRequest, TreeResolver};
15use crate::splicing::{
16    generate_lockfile, Splicer, SplicerKind, SplicingManifest, WorkspaceMetadata,
17};
18
19/// Command line options for the `splice` subcommand
20#[derive(Parser, Debug)]
21#[clap(about = "Command line options for the `splice` subcommand", version)]
22pub struct SpliceOptions {
23    /// A generated manifest of splicing inputs
24    #[clap(long)]
25    pub splicing_manifest: PathBuf,
26
27    /// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
28    #[clap(long)]
29    pub cargo_lockfile: Option<PathBuf>,
30
31    /// The desired update/repin behavior
32    #[clap(long, env = "CARGO_BAZEL_REPIN", num_args=0..=1, default_missing_value = "true")]
33    pub repin: Option<CargoUpdateRequest>,
34
35    /// The directory in which to build the workspace. If this argument is not
36    /// passed, a temporary directory will be generated.
37    #[clap(long)]
38    pub workspace_dir: Option<Utf8PathBuf>,
39
40    /// The location where the results of splicing are written.
41    #[clap(long)]
42    pub output_dir: PathBuf,
43
44    /// If true, outputs will be printed instead of written to disk.
45    #[clap(long)]
46    pub dry_run: bool,
47
48    /// The path to a Cargo configuration file.
49    #[clap(long)]
50    pub cargo_config: Option<PathBuf>,
51
52    /// The path to the config file (containing [crate::config::Config].)
53    #[clap(long)]
54    pub config: PathBuf,
55
56    /// The path to a Cargo binary to use for gathering metadata
57    #[clap(long, env = "CARGO")]
58    pub cargo: PathBuf,
59
60    /// The path to a rustc binary for use with Cargo
61    #[clap(long, env = "RUSTC")]
62    pub rustc: PathBuf,
63
64    /// The name of the repository being generated.
65    #[clap(long)]
66    pub repository_name: String,
67
68    /// Whether to skip writing the cargo lockfile back after resolving.
69    /// You may want to set this if your dependency versions are maintained externally through a non-trivial set-up.
70    /// But you probably don't want to set this.
71    #[clap(long)]
72    pub skip_cargo_lockfile_overwrite: bool,
73
74    /// The path to the Bazel root workspace (i.e. the directory containing the WORKSPACE.bazel file or similar).
75    /// BE CAREFUL with this value. We never want to include it in a lockfile hash (to keep lockfiles portable),
76    /// which means you also should not use it anywhere that _should_ be guarded by a lockfile hash.
77    /// You basically never want to use this value.
78    #[clap(long)]
79    pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
80}
81
82/// Combine a set of disjoint manifests into a single workspace.
83pub fn splice(opt: SpliceOptions) -> Result<()> {
84    // Load the all config files required for splicing a workspace
85    let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)
86        .context("Failed to parse splicing manifest")?;
87
88    // Determine the splicing workspace
89    let temp_dir;
90    let splicing_dir = match &opt.workspace_dir {
91        Some(dir) => dir.clone(),
92        None => {
93            temp_dir = tempfile::tempdir().context("Failed to generate temporary directory")?;
94            Utf8PathBuf::from_path_buf(temp_dir.as_ref().to_path_buf())
95                .unwrap_or_else(|path| panic!("Temporary directory wasn't valid UTF-8: {:?}", path))
96        }
97    };
98
99    // Generate a splicer for creating a Cargo workspace manifest
100    let splicer = Splicer::new(splicing_dir.clone(), splicing_manifest)?;
101    let prepared_splicer = splicer.prepare()?;
102
103    let cargo = Cargo::new(opt.cargo, opt.rustc.clone());
104
105    // Splice together the manifest
106    let manifest_path = prepared_splicer
107        .splice(&splicing_dir, &opt.nonhermetic_root_bazel_workspace_dir)
108        .with_context(|| format!("Failed to splice workspace {}", opt.repository_name))?;
109
110    // Use the existing lockfile if possible, otherwise generate a new one.
111    let cargo_lockfile = if let Some(cargo_lockfile_path) = opt
112        .cargo_lockfile
113        .as_ref()
114        .filter(|_| opt.skip_cargo_lockfile_overwrite)
115    {
116        cargo_lock::Lockfile::load(cargo_lockfile_path).context(format!(
117            "Failed to load lockfile: {}",
118            cargo_lockfile_path.display()
119        ))?
120    } else {
121        generate_lockfile(
122            &manifest_path,
123            &opt.cargo_lockfile,
124            cargo.clone(),
125            &opt.repin,
126        )
127        .context("Failed to generate lockfile")?
128    };
129
130    let config = Config::try_from_path(&opt.config).context("Failed to parse config")?;
131
132    let resolver_data = TreeResolver::new(cargo.clone())
133        .generate(
134            manifest_path.as_path_buf(),
135            &config.supported_platform_triples,
136        )
137        .context("Failed to generate features")?;
138
139    // Write the registry url info to the manifest now that a lockfile has been generated
140    WorkspaceMetadata::write_registry_urls_and_feature_map(
141        &cargo,
142        &cargo_lockfile,
143        resolver_data,
144        manifest_path.as_path_buf(),
145        manifest_path.as_path_buf(),
146    )
147    .context("Failed to write registry URLs and feature map")?;
148
149    // Generate the consumable outputs of the splicing process
150    std::fs::create_dir_all(&opt.output_dir).with_context(|| {
151        format!(
152            "Failed to create directories for {}",
153            opt.output_dir.display()
154        )
155    })?;
156
157    let metadata_json = File::create(opt.output_dir.join("metadata.json"))?;
158
159    // Write metadata to the workspace for future reuse
160    cargo
161        .metadata_command_with_options(
162            manifest_path.as_path_buf().as_ref(),
163            vec!["--locked".to_owned()],
164        )?
165        .cargo_command()
166        .stdout(Stdio::from(metadata_json))
167        .stderr(Stdio::null())
168        .status()
169        .context("Failed to generate cargo metadata")?;
170
171    let cargo_lockfile_path = manifest_path
172        .as_path_buf()
173        .parent()
174        .with_context(|| {
175            format!(
176                "The path {} is expected to have a parent directory",
177                manifest_path.as_path_buf()
178            )
179        })?
180        .join("Cargo.lock");
181
182    std::fs::copy(cargo_lockfile_path, opt.output_dir.join("Cargo.lock"))
183        .context("Failed to copy lockfile")?;
184
185    if let SplicerKind::Workspace { path, .. } = prepared_splicer {
186        let metadata = cargo.metadata_command_with_options(
187            path.as_std_path(),
188            vec![String::from("--no-deps")],
189        )?.exec().with_context(|| {
190                format!(
191                    "Error spawning cargo in child process to compute crate paths for workspace '{}'",
192                    path
193                )
194            })?;
195        let contents = metadata
196            .packages
197            .into_iter()
198            .map(|package| package.manifest_path)
199            .join("\n");
200        std::fs::write(opt.output_dir.join("extra_paths_to_track"), contents)?;
201    }
202    Ok(())
203}