Add an xtask subcommand for building documentation (#1160)

* Fix a silly mistake from the initial implementation

* Improve the `build-examples` subcommand, make it usable with other packages

* Add a `build-documentation` subcommand

* Update `README.md`

* Add toolchain modifier when required
This commit is contained in:
Jesse Braham 2024-02-13 22:39:01 +00:00 committed by GitHub
parent 044b38e632
commit 2b8db5c2c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 232 additions and 65 deletions

View File

@ -8,8 +8,9 @@ Automation using [cargo-xtask](https://github.com/matklad/cargo-xtask).
Usage: xtask <COMMAND> Usage: xtask <COMMAND>
Commands: Commands:
build-examples Build all examples for the specified chip build-documentation Build documentation for the specified chip
help Print this message or the help of the given subcommand(s) build-examples Build all examples for the specified chip
help Print this message or the help of the given subcommand(s)
Options: Options:
-h, --help Print help -h, --help Print help

View File

@ -5,10 +5,21 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use anyhow::{bail, Result}; use anyhow::{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)]
#[strum(serialize_all = "kebab-case")]
pub enum Package {
EspHal,
EspHalProcmacros,
EspHalSmartled,
EspLpHal,
EspRiscvRt,
Examples,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, ValueEnum)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, ValueEnum)]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub enum Chip { pub enum Chip {
@ -36,25 +47,32 @@ impl Chip {
} }
} }
pub fn toolchain(&self) -> &str { pub fn has_lp_core(&self) -> bool {
use Chip::*;
matches!(self, Esp32c6 | Esp32p4 | Esp32s2 | Esp32s3)
}
pub fn lp_target(&self) -> Result<&str> {
use Chip::*; use Chip::*;
match self { match self {
Esp32 | Esp32s2 | Esp32s3 => "xtensa", Esp32c6 => Ok("riscv32imac-unknown-none-elf"),
_ => "nightly", Esp32s2 | Esp32s3 => Ok("riscv32imc-unknown-none-elf"),
_ => bail!("Chip does not contain an LP core: '{}'", self),
} }
} }
} }
#[derive(Debug)] #[derive(Debug, Default, Clone)]
pub struct Metadata { pub struct Metadata {
path: PathBuf, example_path: PathBuf,
chips: Vec<Chip>, chips: Vec<Chip>,
features: Vec<String>, features: Vec<String>,
} }
impl Metadata { impl Metadata {
pub fn new(path: PathBuf, chips: Vec<Chip>, features: Vec<String>) -> Self { pub fn new(example_path: &Path, chips: Vec<Chip>, features: Vec<String>) -> Self {
let chips = if chips.is_empty() { let chips = if chips.is_empty() {
Chip::iter().collect() Chip::iter().collect()
} else { } else {
@ -62,15 +80,24 @@ impl Metadata {
}; };
Self { Self {
path, example_path: example_path.to_path_buf(),
chips, chips,
features, features,
} }
} }
/// Absolute path to the example. /// Absolute path to the example.
pub fn path(&self) -> &Path { pub fn example_path(&self) -> &Path {
&self.path &self.example_path
}
/// Name of the example.
pub fn name(&self) -> String {
self.example_path()
.file_name()
.unwrap()
.to_string_lossy()
.replace(".rs", "")
} }
/// A list of all features required for building a given examples. /// A list of all features required for building a given examples.
@ -84,16 +111,52 @@ impl Metadata {
} }
} }
pub fn load_examples(workspace: &Path) -> Result<Vec<Metadata>> { /// Build the documentation for the specified package and device.
let bin_path = workspace pub fn build_documentation(
.join("examples") workspace: &Path,
.join("src") package: Package,
.join("bin") chip: Chip,
.canonicalize()?; target: &str,
open: bool,
) -> Result<()> {
let package_name = package.to_string();
let package_path = workspace.join(&package_name);
log::info!("Building '{package_name}' documentation targeting '{chip}'");
let mut features = vec![chip.to_string()];
// The ESP32 and ESP32-C2 must have their Xtal frequencies explicitly stated
// when using `esp-hal` or `esp-hal-smartled`:
use Chip::*;
use Package::*;
if matches!(chip, Esp32 | Esp32c2) && matches!(package, EspHal | EspHalSmartled) {
features.push("xtal-40mhz".into());
}
// 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={}", features.join(",")),
];
if open {
args.push("--open".into());
}
log::debug!("{args:#?}");
// Execute `cargo doc` from the package root:
cargo(&args, &package_path)?;
Ok(())
}
/// Load all examples at the given path, and parse their metadata.
pub fn load_examples(path: &Path) -> Result<Vec<Metadata>> {
let mut examples = Vec::new(); let mut examples = Vec::new();
for entry in fs::read_dir(bin_path)? { for entry in fs::read_dir(path)? {
let path = entry?.path(); let path = entry?.path();
let text = fs::read_to_string(&path)?; let text = fs::read_to_string(&path)?;
@ -101,12 +164,7 @@ pub fn load_examples(workspace: &Path) -> Result<Vec<Metadata>> {
let mut features = Vec::new(); let mut features = Vec::new();
// We will indicate metadata lines using the `//%` prefix: // We will indicate metadata lines using the `//%` prefix:
let lines = text for line in text.lines().filter(|line| line.starts_with("//%")) {
.lines()
.filter(|line| line.starts_with("//%"))
.collect::<Vec<_>>();
for line in lines {
let mut split = line let mut split = line
.trim_start_matches("//%") .trim_start_matches("//%")
.trim() .trim()
@ -137,45 +195,72 @@ pub fn load_examples(workspace: &Path) -> Result<Vec<Metadata>> {
} }
} }
let meta = Metadata::new(path, chips, features); examples.push(Metadata::new(&path, chips, features));
examples.push(meta);
} }
Ok(examples) Ok(examples)
} }
pub fn build_example(workspace: &Path, chip: Chip, example: &Metadata) -> Result<()> { /// Build the specified example for the specified chip.
let path = example.path(); pub fn build_example(
let features = example.features(); package_path: &Path,
chip: Chip,
log::info!("Building example '{}' for '{}'", path.display(), chip); target: &str,
if !features.is_empty() { example: &Metadata,
log::info!(" Features: {}", features.join(",")); ) -> Result<()> {
log::info!(
"Building example '{}' for '{}'",
example.example_path().display(),
chip
);
if !example.features().is_empty() {
log::info!(" Features: {}", example.features().join(","));
} }
let bin_name = example let bin = if example
.path() .example_path()
.file_name() .strip_prefix(package_path)?
.unwrap() .starts_with("src/bin")
.to_string_lossy() {
.replace(".rs", ""); format!("--bin={}", example.name())
} else {
format!("--example={}", example.name())
};
let args = &[ // If targeting an Xtensa device, we must use the '+esp' toolchain modifier:
&format!("+{}", chip.toolchain()), let mut args = vec![];
"build", if target.starts_with("xtensa") {
"--release", args.push("+esp".into());
&format!("--target={}", chip.target()), }
&format!("--features={},{}", chip, features.join(",")),
&format!("--bin={bin_name}"), args.extend(vec![
]; "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:#?}");
Command::new("cargo") cargo(&args, package_path)?;
.args(args)
.current_dir(workspace.join("examples"))
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()?;
Ok(()) 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"))
}
}

View File

@ -1,22 +1,40 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::{bail, Result};
use clap::{Args, Parser}; use clap::{Args, Parser};
use xtask::Chip; use xtask::{Chip, Package};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Command-line Interface // Command-line Interface
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
enum Cli { enum Cli {
/// Build documentation for the specified chip.
BuildDocumentation(BuildDocumentationArgs),
/// Build all examples for the specified chip. /// Build all examples for the specified chip.
BuildExamples(BuildExamplesArgs), BuildExamples(BuildExamplesArgs),
} }
#[derive(Debug, Args)]
struct BuildDocumentationArgs {
/// Package to build documentation for.
#[arg(value_enum)]
package: Package,
/// Which chip to build the documentation for.
#[arg(value_enum)]
chip: Chip,
/// Open the documentation in the default browser once built.
#[arg(long)]
open: bool,
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
struct BuildExamplesArgs { struct BuildExamplesArgs {
/// Package to build examples for.
#[arg(value_enum)]
package: Package,
/// Which chip to build the examples for. /// Which chip to build the examples for.
#[clap(value_enum)] #[arg(value_enum)]
chip: Chip, chip: Chip,
} }
@ -32,6 +50,7 @@ fn main() -> Result<()> {
let workspace = workspace.parent().unwrap().canonicalize()?; let workspace = workspace.parent().unwrap().canonicalize()?;
match Cli::parse() { match Cli::parse() {
Cli::BuildDocumentation(args) => build_documentation(&workspace, args),
Cli::BuildExamples(args) => build_examples(&workspace, args), Cli::BuildExamples(args) => build_examples(&workspace, args),
} }
} }
@ -39,12 +58,74 @@ fn main() -> Result<()> {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Subcommands // Subcommands
fn build_examples(workspace: &Path, args: BuildExamplesArgs) -> Result<()> { fn build_documentation(workspace: &Path, args: BuildDocumentationArgs) -> Result<()> {
// Load all examples and parse their metadata. Filter down the examples to only // Ensure that the package/chip combination provided are valid:
// those for which our chip is supported, and then attempt to build each validate_package_chip(&args.package, &args.chip)?;
// remaining example, with the required features enabled:
xtask::load_examples(workspace)? // Determine the appropriate build target for the given package and chip:
.iter() let target = target_triple(&args.package, &args.chip)?;
.filter(|example| example.supports_chip(args.chip))
.try_for_each(|example| xtask::build_example(workspace, args.chip, example)) // Simply build the documentation for the specified package, targeting the
// specified chip:
xtask::build_documentation(workspace, args.package, args.chip, target, args.open)
}
fn build_examples(workspace: &Path, mut args: BuildExamplesArgs) -> Result<()> {
// Ensure that the package/chip combination provided are valid:
validate_package_chip(&args.package, &args.chip)?;
// If the 'esp-hal' package is specified, what we *really* want is the
// 'examples' package instead:
if args.package == Package::EspHal {
log::warn!(
"Package '{}' specified, using '{}' instead",
Package::EspHal,
Package::Examples
);
args.package = Package::Examples;
}
// Absolute path of the package's root:
let package_path = workspace.join(args.package.to_string());
// Absolute path to the directory containing the examples:
let example_path = if args.package == Package::Examples {
package_path.join("src").join("bin")
} else {
package_path.join("examples")
};
// Determine the appropriate build target for the given package and chip:
let target = target_triple(&args.package, &args.chip)?;
// Load all examples and parse their metadata:
xtask::load_examples(&example_path)?
.iter()
// Filter down the examples to only those for which the specified chip is supported:
.filter(|example| example.supports_chip(args.chip))
// Attempt to build each supported example, with all required features enabled:
.try_for_each(|example| xtask::build_example(&package_path, args.chip, target, example))
}
// ----------------------------------------------------------------------------
// Helper Functions
fn target_triple<'a>(package: &'a Package, chip: &'a Chip) -> Result<&'a str> {
if *package == Package::EspLpHal {
chip.lp_target()
} else {
Ok(chip.target())
}
}
fn validate_package_chip(package: &Package, chip: &Chip) -> Result<()> {
if *package == Package::EspLpHal && !chip.has_lp_core() {
bail!(
"Invalid chip provided for package '{}': '{}'",
package,
chip
);
}
Ok(())
} }