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
This commit is contained in:
Jesse Braham 2024-02-16 11:21:31 +00:00 committed by GitHub
parent 2a5996f408
commit 0483b47e77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 278 additions and 38 deletions

View File

@ -9,4 +9,6 @@ anyhow = "1.0.79"
clap = { version = "4.5.0", features = ["derive"] } clap = { version = "4.5.0", features = ["derive"] }
env_logger = "0.11.1" env_logger = "0.11.1"
log = "0.4.20" log = "0.4.20"
semver = "1.0.21"
strum = { version = "0.26.1", features = ["derive"] } strum = { version = "0.26.1", features = ["derive"] }
toml_edit = "0.22.5"

View File

@ -10,6 +10,8 @@ Usage: xtask <COMMAND>
Commands: Commands:
build-documentation Build documentation for the specified chip build-documentation Build documentation for the specified chip
build-examples Build all examples 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) help Print this message or the help of the given subcommand(s)
Options: Options:

108
xtask/src/cargo.rs Normal file
View File

@ -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<String>,
subcommand: String,
target: Option<String>,
features: Vec<String>,
args: Vec<String>,
}
impl CargoArgsBuilder {
#[must_use]
pub fn toolchain<S>(mut self, toolchain: S) -> Self
where
S: Into<String>,
{
self.toolchain = Some(toolchain.into());
self
}
#[must_use]
pub fn subcommand<S>(mut self, subcommand: S) -> Self
where
S: Into<String>,
{
self.subcommand = subcommand.into();
self
}
#[must_use]
pub fn target<S>(mut self, target: S) -> Self
where
S: Into<String>,
{
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<S>(mut self, arg: S) -> Self
where
S: Into<String>,
{
self.args.push(arg.into());
self
}
#[must_use]
pub fn build(self) -> Vec<String> {
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
}
}

View File

@ -2,14 +2,17 @@ use std::{
collections::VecDeque, collections::VecDeque,
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Stdio},
}; };
use anyhow::{anyhow, bail, Result}; use anyhow::{bail, Result};
use clap::ValueEnum; use clap::ValueEnum;
use strum::{Display, EnumIter, IntoEnumIterator}; 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")] #[strum(serialize_all = "kebab-case")]
pub enum Package { pub enum Package {
EspHal, 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. /// Build the documentation for the specified package and device.
pub fn build_documentation( pub fn build_documentation(
workspace: &Path, workspace: &Path,
@ -124,20 +135,22 @@ pub fn build_documentation(
log::info!("Building '{package_name}' documentation targeting '{chip}'"); log::info!("Building '{package_name}' documentation targeting '{chip}'");
// Build up an array of command-line arguments to pass to `cargo doc`: // Build up an array of command-line arguments to pass to `cargo`:
let mut args = vec![ let mut builder = CargoArgsBuilder::default()
"doc".into(), .subcommand("doc")
"-Zbuild-std=core".into(), // Required for Xtensa, for some reason .arg("-Zbuild-std=core") // Required for Xtensa, for some reason
format!("--target={target}"), .target(target)
format!("--features={}", chip.to_string()), .features(&[chip.to_string()]);
];
if open { if open {
args.push("--open".into()); builder = builder.arg("--open");
} }
let args = builder.build();
log::debug!("{args:#?}"); log::debug!("{args:#?}");
// Execute `cargo doc` from the package root: // Execute `cargo doc` from the package root:
cargo(&args, &package_path)?; cargo::run(&args, &package_path)?;
Ok(()) Ok(())
} }
@ -217,40 +230,105 @@ pub fn build_example(
format!("--example={}", example.name()) 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: // If targeting an Xtensa device, we must use the '+esp' toolchain modifier:
let mut args = vec![];
if target.starts_with("xtensa") { if target.starts_with("xtensa") {
args.push("+esp".into()); builder = builder.toolchain("esp");
} }
args.extend(vec![ let args = builder.build();
"build".into(),
"-Zbuild-std=alloc,core".into(),
"--release".into(),
format!("--target={target}"),
format!("--features={},{}", chip, example.features().join(",")),
bin,
]);
log::debug!("{args:#?}"); log::debug!("{args:#?}");
cargo(&args, package_path)?; cargo::run(&args, package_path)?;
Ok(()) Ok(())
} }
fn cargo(args: &[String], cwd: &Path) -> Result<()> { /// Build the specified package, using the given toolchain/target/features if
let status = Command::new("cargo") /// provided.
.args(args) pub fn build_package(
.current_dir(cwd) package_path: &Path,
.stdout(Stdio::piped()) features: Vec<String>,
.stderr(Stdio::inherit()) toolchain: Option<String>,
.status()?; target: Option<String>,
) -> 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)?;
// Make sure that we return an appropriate exit code here, as Github Actions
// requires this in order to function correctly:
if status.success() {
Ok(()) Ok(())
} else { }
Err(anyhow!("Failed to execute cargo subcommand"))
/// 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::<toml_edit::Document>()?;
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(())
}

View File

@ -2,7 +2,8 @@ use std::path::{Path, PathBuf};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::{Args, Parser}; use clap::{Args, Parser};
use xtask::{Chip, Package}; use strum::IntoEnumIterator;
use xtask::{Chip, Package, Version};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Command-line Interface // Command-line Interface
@ -13,6 +14,10 @@ enum Cli {
BuildDocumentation(BuildDocumentationArgs), BuildDocumentation(BuildDocumentationArgs),
/// Build all examples for the specified chip. /// Build all examples for the specified chip.
BuildExamples(BuildExamplesArgs), 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)] #[derive(Debug, Args)]
@ -38,6 +43,32 @@ struct BuildExamplesArgs {
chip: Chip, chip: Chip,
} }
#[derive(Debug, Args)]
struct BuildPackageArgs {
/// Package to build.
#[arg(value_enum)]
package: Package,
/// Target to build for.
#[arg(long)]
target: Option<String>,
/// Features to build with.
#[arg(long, value_delimiter = ',')]
features: Vec<String>,
/// Toolchain to build with.
#[arg(long)]
toolchain: Option<String>,
}
#[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<Package>,
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Application // Application
@ -52,6 +83,8 @@ fn main() -> Result<()> {
match Cli::parse() { match Cli::parse() {
Cli::BuildDocumentation(args) => build_documentation(&workspace, args), Cli::BuildDocumentation(args) => build_documentation(&workspace, args),
Cli::BuildExamples(args) => build_examples(&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)) .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 // Helper Functions