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:
parent
044b38e632
commit
2b8db5c2c2
@ -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
|
||||||
|
|||||||
189
xtask/src/lib.rs
189
xtask/src/lib.rs
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user