blob: a4c6b21b3d0a4e188706874bacb6f39abebd2dc8 [file] [log] [blame]
//! The cli entrypoint for the `vendor` subcommand
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{self, ExitStatus};
use anyhow::{bail, Context as AnyhowContext, Result};
use clap::Parser;
use crate::config::{Config, VendorMode};
use crate::context::Context;
use crate::metadata::CargoUpdateRequest;
use crate::metadata::{Annotations, VendorGenerator};
use crate::metadata::{Generator, MetadataGenerator};
use crate::rendering::{render_module_label, write_outputs, Renderer};
use crate::splicing::{generate_lockfile, Splicer, SplicingManifest, WorkspaceMetadata};
/// Command line options for the `vendor` subcommand
#[derive(Parser, Debug)]
#[clap(about, version)]
pub struct VendorOptions {
/// The path to a Cargo binary to use for gathering metadata
#[clap(long, env = "CARGO")]
pub cargo: PathBuf,
/// The path to a rustc binary for use with Cargo
#[clap(long, env = "RUSTC")]
pub rustc: PathBuf,
/// The path to a buildifier binary for formatting generated BUILD files
#[clap(long)]
pub buildifier: Option<PathBuf>,
/// The config file with information about the Bazel and Cargo workspace
#[clap(long)]
pub config: PathBuf,
/// A generated manifest of splicing inputs
#[clap(long)]
pub splicing_manifest: PathBuf,
/// The path to a [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html) file.
#[clap(long)]
pub cargo_lockfile: Option<PathBuf>,
/// A [Cargo config](https://doc.rust-lang.org/cargo/reference/config.html#configuration)
/// file to use when gathering metadata
#[clap(long)]
pub cargo_config: Option<PathBuf>,
/// The desired update/repin behavior. The arguments passed here are forward to
/// [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html). See
/// [metadata::CargoUpdateRequest] for details on the values to pass here.
#[clap(long, env = "CARGO_BAZEL_REPIN")]
pub repin: Option<CargoUpdateRequest>,
/// The path to a Cargo metadata `json` file.
#[clap(long)]
pub metadata: Option<PathBuf>,
/// The path to a bazel binary
#[clap(long, env = "BAZEL_REAL", default_value = "bazel")]
pub bazel: PathBuf,
/// The directory in which to build the workspace. A `Cargo.toml` file
/// should always be produced within this directory.
#[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
pub workspace_dir: PathBuf,
/// If true, outputs will be printed instead of written to disk.
#[clap(long)]
pub dry_run: bool,
}
/// Run buildifier on a given file.
fn buildifier_format(bin: &Path, file: &Path) -> Result<ExitStatus> {
let status = process::Command::new(bin)
.args(["-lint=fix", "-mode=fix", "-warnings=all"])
.arg(file)
.status()
.context("Failed to apply buildifier fixes")?;
if !status.success() {
bail!(status)
}
Ok(status)
}
/// Query the Bazel output_base to determine the location of external repositories.
fn locate_bazel_output_base(bazel: &Path, workspace_dir: &Path) -> Result<PathBuf> {
// Allow a predefined environment variable to take precedent. This
// solves for the specific needs of Bazel CI on Github.
if let Ok(output_base) = env::var("OUTPUT_BASE") {
return Ok(PathBuf::from(output_base));
}
let output = process::Command::new(bazel)
.current_dir(workspace_dir)
.args(["info", "output_base"])
.output()
.context("Failed to query the Bazel workspace's `output_base`")?;
if !output.status.success() {
bail!(output.status)
}
Ok(PathBuf::from(
String::from_utf8_lossy(&output.stdout).trim(),
))
}
pub fn vendor(opt: VendorOptions) -> Result<()> {
let output_base = locate_bazel_output_base(&opt.bazel, &opt.workspace_dir)?;
// Load the all config files required for splicing a workspace
let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?
.resolve(&opt.workspace_dir, &output_base);
let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
// Generate a splicer for creating a Cargo workspace manifest
let splicer = Splicer::new(PathBuf::from(temp_dir.as_ref()), splicing_manifest)
.context("Failed to create splicer")?;
// Splice together the manifest
let manifest_path = splicer
.splice_workspace()
.context("Failed to splice workspace")?;
// Gather a cargo lockfile
let cargo_lockfile = generate_lockfile(
&manifest_path,
&opt.cargo_lockfile,
&opt.cargo,
&opt.rustc,
&opt.repin,
)?;
// Write the registry url info to the manifest now that a lockfile has been generated
WorkspaceMetadata::write_registry_urls(&cargo_lockfile, &manifest_path)?;
// Write metadata to the workspace for future reuse
let (cargo_metadata, cargo_lockfile) = Generator::new()
.with_cargo(opt.cargo.clone())
.with_rustc(opt.rustc.clone())
.generate(&manifest_path.as_path_buf())?;
// Load the config from disk
let config = Config::try_from_path(&opt.config)?;
// Annotate metadata
let annotations = Annotations::new(cargo_metadata, cargo_lockfile.clone(), config.clone())?;
// Generate renderable contexts for earch package
let context = Context::new(annotations)?;
// Render build files
let outputs = Renderer::new(config.rendering.clone()).render(&context)?;
// Cache the file names for potential use with buildifier
let file_names: BTreeSet<PathBuf> = outputs.keys().cloned().collect();
// First ensure vendoring and rendering happen in a clean directory
let vendor_dir_label = render_module_label(&config.rendering.crates_module_template, "BUILD")?;
let vendor_dir = opt
.workspace_dir
.join(vendor_dir_label.package.unwrap_or_default());
if vendor_dir.exists() {
fs::remove_dir_all(&vendor_dir)
.with_context(|| format!("Failed to delete {}", vendor_dir.display()))?;
}
// Store the updated Cargo.lock
if let Some(path) = &opt.cargo_lockfile {
fs::write(path, cargo_lockfile.to_string())
.context("Failed to write Cargo.lock file back to the workspace.")?;
}
// Vendor the crates from the spliced workspace
if matches!(config.rendering.vendor_mode, Some(VendorMode::Local)) {
VendorGenerator::new(opt.cargo.clone(), opt.rustc.clone())
.generate(manifest_path.as_path_buf(), &vendor_dir)
.context("Failed to vendor dependencies")?;
}
// Write outputs
write_outputs(outputs, &opt.workspace_dir, opt.dry_run)
.context("Failed writing output files")?;
// Optionally apply buildifier fixes
if let Some(buildifier_bin) = opt.buildifier {
for file in file_names {
let file_path = opt.workspace_dir.join(file);
buildifier_format(&buildifier_bin, &file_path)
.with_context(|| format!("Failed to run buildifier on {}", file_path.display()))?;
}
}
Ok(())
}