From 0483b47e77d821ed33742bbbcd51fba5c421474d Mon Sep 17 00:00:00 2001 From: Jesse Braham Date: Fri, 16 Feb 2024 11:21:31 +0000 Subject: [PATCH] Add `build-package` and `bump-version` subcommands to `xtask` package (#1172) * Implement builder pattern for cargo command-line args, refactoring * Add an `xtask` subcommand to build a package (not its examples) * Add an `xtask` subcommand to bump the versions of packages --- xtask/Cargo.toml | 2 + xtask/README.md | 2 + xtask/src/cargo.rs | 108 ++++++++++++++++++++++++++++++++ xtask/src/lib.rs | 152 ++++++++++++++++++++++++++++++++++----------- xtask/src/main.rs | 52 +++++++++++++++- 5 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 xtask/src/cargo.rs diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index ccdd1bc71..cd4ea33e0 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,4 +9,6 @@ anyhow = "1.0.79" clap = { version = "4.5.0", features = ["derive"] } env_logger = "0.11.1" log = "0.4.20" +semver = "1.0.21" strum = { version = "0.26.1", features = ["derive"] } +toml_edit = "0.22.5" diff --git a/xtask/README.md b/xtask/README.md index 21bfa37ff..e0d4bc398 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -10,6 +10,8 @@ Usage: xtask Commands: build-documentation Build documentation for the specified chip build-examples Build all examples for the specified chip + build-package Build the specified package with the given options + bump-version Bump the version of the specified package(s) help Print this message or the help of the given subcommand(s) Options: diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs new file mode 100644 index 000000000..de22919eb --- /dev/null +++ b/xtask/src/cargo.rs @@ -0,0 +1,108 @@ +//! Tools for working with Cargo. + +use std::{ + path::Path, + process::{Command, Stdio}, +}; + +use anyhow::{bail, Result}; + +/// Execute cargo with the given arguments and from the specified directory. +pub fn run(args: &[String], cwd: &Path) -> Result<()> { + if !cwd.is_dir() { + bail!("The `cwd` argument MUST be a directory"); + } + + let status = Command::new("cargo") + .args(args) + .current_dir(cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .status()?; + + // Make sure that we return an appropriate exit code here, as Github Actions + // requires this in order to function correctly: + if status.success() { + Ok(()) + } else { + bail!("Failed to execute cargo subcommand") + } +} + +#[derive(Debug, Default)] +pub struct CargoArgsBuilder { + toolchain: Option, + subcommand: String, + target: Option, + features: Vec, + args: Vec, +} + +impl CargoArgsBuilder { + #[must_use] + pub fn toolchain(mut self, toolchain: S) -> Self + where + S: Into, + { + self.toolchain = Some(toolchain.into()); + self + } + + #[must_use] + pub fn subcommand(mut self, subcommand: S) -> Self + where + S: Into, + { + self.subcommand = subcommand.into(); + self + } + + #[must_use] + pub fn target(mut self, target: S) -> Self + where + S: Into, + { + self.target = Some(target.into()); + self + } + + #[must_use] + pub fn features(mut self, features: &[String]) -> Self { + self.features = features.to_vec(); + self + } + + #[must_use] + pub fn arg(mut self, arg: S) -> Self + where + S: Into, + { + self.args.push(arg.into()); + self + } + + #[must_use] + pub fn build(self) -> Vec { + let mut args = vec![]; + + if let Some(toolchain) = self.toolchain { + args.push(format!("+{toolchain}")); + } + + args.push(self.subcommand); + + if let Some(target) = self.target { + args.push(format!("--target={target}")); + } + + if !self.features.is_empty() { + args.push(format!("--features={}", self.features.join(","))); + } + + for arg in self.args { + args.push(arg); + } + + args + } +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index 35ecdc60c..eebaf47e3 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -2,14 +2,17 @@ use std::{ collections::VecDeque, fs, path::{Path, PathBuf}, - process::{Command, Stdio}, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::ValueEnum; use strum::{Display, EnumIter, IntoEnumIterator}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, ValueEnum)] +use self::cargo::CargoArgsBuilder; + +mod cargo; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, ValueEnum)] #[strum(serialize_all = "kebab-case")] pub enum Package { EspHal, @@ -111,6 +114,14 @@ impl Metadata { } } +#[derive(Debug, Clone, Copy, Display, ValueEnum)] +#[strum(serialize_all = "lowercase")] +pub enum Version { + Major, + Minor, + Patch, +} + /// Build the documentation for the specified package and device. pub fn build_documentation( workspace: &Path, @@ -124,20 +135,22 @@ pub fn build_documentation( log::info!("Building '{package_name}' documentation targeting '{chip}'"); - // Build up an array of command-line arguments to pass to `cargo doc`: - let mut args = vec![ - "doc".into(), - "-Zbuild-std=core".into(), // Required for Xtensa, for some reason - format!("--target={target}"), - format!("--features={}", chip.to_string()), - ]; + // Build up an array of command-line arguments to pass to `cargo`: + let mut builder = CargoArgsBuilder::default() + .subcommand("doc") + .arg("-Zbuild-std=core") // Required for Xtensa, for some reason + .target(target) + .features(&[chip.to_string()]); + if open { - args.push("--open".into()); + builder = builder.arg("--open"); } + + let args = builder.build(); log::debug!("{args:#?}"); // Execute `cargo doc` from the package root: - cargo(&args, &package_path)?; + cargo::run(&args, &package_path)?; Ok(()) } @@ -217,40 +230,105 @@ pub fn build_example( format!("--example={}", example.name()) }; + let mut features = example.features().to_vec(); + features.push(chip.to_string()); + + let mut builder = CargoArgsBuilder::default() + .subcommand("build") + .arg("-Zbuild-std=alloc,core") + .arg("--release") + .target(target) + .features(&features) + .arg(bin); + // If targeting an Xtensa device, we must use the '+esp' toolchain modifier: - let mut args = vec![]; if target.starts_with("xtensa") { - args.push("+esp".into()); + builder = builder.toolchain("esp"); } - args.extend(vec![ - "build".into(), - "-Zbuild-std=alloc,core".into(), - "--release".into(), - format!("--target={target}"), - format!("--features={},{}", chip, example.features().join(",")), - bin, - ]); + let args = builder.build(); log::debug!("{args:#?}"); - cargo(&args, package_path)?; + cargo::run(&args, package_path)?; Ok(()) } -fn cargo(args: &[String], cwd: &Path) -> Result<()> { - let status = Command::new("cargo") - .args(args) - .current_dir(cwd) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .status()?; - - // Make sure that we return an appropriate exit code here, as Github Actions - // requires this in order to function correctly: - if status.success() { - Ok(()) - } else { - Err(anyhow!("Failed to execute cargo subcommand")) +/// Build the specified package, using the given toolchain/target/features if +/// provided. +pub fn build_package( + package_path: &Path, + features: Vec, + toolchain: Option, + target: Option, +) -> Result<()> { + log::info!("Building package '{}'", package_path.display()); + if !features.is_empty() { + log::info!(" Features: {}", features.join(",")); } + if let Some(ref target) = target { + log::info!(" Target: {}", target); + } + + let mut builder = CargoArgsBuilder::default() + .subcommand("build") + .arg("-Zbuild-std=core") + .arg("--release"); + + if let Some(toolchain) = toolchain { + builder = builder.toolchain(toolchain); + } + + if let Some(target) = target { + builder = builder.target(target); + } + + if !features.is_empty() { + builder = builder.features(&features); + } + + let args = builder.build(); + log::debug!("{args:#?}"); + + cargo::run(&args, package_path)?; + + Ok(()) +} + +/// Bump the version of the specified package by the specified amount. +pub fn bump_version(workspace: &Path, package: Package, amount: Version) -> Result<()> { + let manifest_path = workspace.join(package.to_string()).join("Cargo.toml"); + let manifest = fs::read_to_string(&manifest_path)?; + + let mut manifest = manifest.parse::()?; + + let version = manifest["package"]["version"] + .to_string() + .trim() + .trim_matches('"') + .to_string(); + let prev_version = &version; + + let mut version = semver::Version::parse(&version)?; + match amount { + Version::Major => { + version.major += 1; + version.minor = 0; + version.patch = 0; + } + Version::Minor => { + version.minor += 1; + version.patch = 0; + } + Version::Patch => { + version.patch += 1; + } + } + + log::info!("Bumping version for package: {package} ({prev_version} -> {version})"); + + manifest["package"]["version"] = toml_edit::value(version.to_string()); + fs::write(manifest_path, manifest.to_string())?; + + Ok(()) } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 015ffc2ae..b3fdd508d 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2,7 +2,8 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use clap::{Args, Parser}; -use xtask::{Chip, Package}; +use strum::IntoEnumIterator; +use xtask::{Chip, Package, Version}; // ---------------------------------------------------------------------------- // Command-line Interface @@ -13,6 +14,10 @@ enum Cli { BuildDocumentation(BuildDocumentationArgs), /// Build all examples for the specified chip. BuildExamples(BuildExamplesArgs), + /// Build the specified package with the given options. + BuildPackage(BuildPackageArgs), + /// Bump the version of the specified package(s) + BumpVersion(BumpVersionArgs), } #[derive(Debug, Args)] @@ -38,6 +43,32 @@ struct BuildExamplesArgs { chip: Chip, } +#[derive(Debug, Args)] +struct BuildPackageArgs { + /// Package to build. + #[arg(value_enum)] + package: Package, + /// Target to build for. + #[arg(long)] + target: Option, + /// Features to build with. + #[arg(long, value_delimiter = ',')] + features: Vec, + /// Toolchain to build with. + #[arg(long)] + toolchain: Option, +} + +#[derive(Debug, Args)] +struct BumpVersionArgs { + /// How much to bump the version by. + #[arg(value_enum)] + amount: Version, + /// Package(s) to target. + #[arg(value_enum, default_values_t = Package::iter())] + packages: Vec, +} + // ---------------------------------------------------------------------------- // Application @@ -52,6 +83,8 @@ fn main() -> Result<()> { match Cli::parse() { Cli::BuildDocumentation(args) => build_documentation(&workspace, args), Cli::BuildExamples(args) => build_examples(&workspace, args), + Cli::BuildPackage(args) => build_package(&workspace, args), + Cli::BumpVersion(args) => bump_version(&workspace, args), } } @@ -107,6 +140,23 @@ fn build_examples(workspace: &Path, mut args: BuildExamplesArgs) -> Result<()> { .try_for_each(|example| xtask::build_example(&package_path, args.chip, target, example)) } +fn build_package(workspace: &Path, args: BuildPackageArgs) -> Result<()> { + // Absolute path of the package's root: + let package_path = workspace.join(args.package.to_string()); + + // Build the package using the provided features and/or target, if any: + xtask::build_package(&package_path, args.features, args.toolchain, args.target) +} + +fn bump_version(workspace: &Path, args: BumpVersionArgs) -> Result<()> { + // Bump the version by the specified amount for each given package: + for package in args.packages { + xtask::bump_version(workspace, package, args.amount)?; + } + + Ok(()) +} + // ---------------------------------------------------------------------------- // Helper Functions