Refactor xtask subcommands to be group by common functionality (#3457)

* Create `command` submodule, extract build-related args/actions

* Extract run-related args/actions

* Fix clippy warnings

* Update `README.md` for xtask package

* Fix order of positional arguments for examples

* Update workflows and cargo aliases

* Inline function which is only called in one place

* Update HIL workflow
This commit is contained in:
Jesse Braham 2025-05-07 11:32:51 +02:00 committed by GitHub
parent 78bd99e653
commit e5ea7e35cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 675 additions and 648 deletions

View File

@ -1,5 +1,5 @@
[alias]
xtask = "run --package xtask --"
xdoc = "run --package xtask --features=deploy-docs,preview-docs --"
xfmt = "xtask fmt-packages"
qa = "xtask run-example qa-test"
xdoc = "run --package xtask --features=deploy-docs,preview-docs --"
xfmt = "xtask fmt-packages"
qa = "xtask run example qa-test"

View File

@ -25,7 +25,7 @@ jobs:
setup:
runs-on: ubuntu-latest
outputs:
packages: '${{ github.event.inputs.packages }}'
packages: "${{ github.event.inputs.packages }}"
steps:
- run: echo "Setup complete!"
build:
@ -71,7 +71,7 @@ jobs:
ref: ${{ matrix.packages.tag }}
- name: Build documentation
run: hal-xtask build-documentation --packages=${{ matrix.packages.name }} --base-url /projects/rust/
run: hal-xtask build documentation --packages=${{ matrix.packages.name }} --base-url /projects/rust/
# https://github.com/actions/deploy-pages/issues/303#issuecomment-1951207879
- name: Remove problematic '.lock' files
@ -98,11 +98,6 @@ jobs:
with:
path: "docs/"
# Create an index for _all_ packages.
- name: Create index.html
run: cargo xtask build-documentation-index
- if: ${{ github.event.inputs.server == 'preview' }}
name: Deploy to preview server
uses: appleboy/scp-action@v0.1.7

View File

@ -9,7 +9,7 @@ on:
repository:
description: "Owner and repository to test"
required: true
default: 'esp-rs/esp-hal'
default: "esp-rs/esp-hal"
branch:
description: "Branch, tag or SHA to checkout."
required: true
@ -118,7 +118,7 @@ jobs:
version: 1.85.0.0
- name: Build tests
run: cargo xtask build-tests ${{ matrix.target.soc }}
run: cargo xtask build tests ${{ matrix.target.soc }}
- uses: actions/upload-artifact@v4
with:
@ -176,7 +176,7 @@ jobs:
export PATH=$PATH:/home/espressif/.cargo/bin
chmod +x xtask
./xtask run-elfs ${{ matrix.target.soc }} tests-${{ matrix.target.soc }}
./xtask run elfs ${{ matrix.target.soc }} tests-${{ matrix.target.soc }}
- name: Clean up
if: always()

View File

@ -8,22 +8,15 @@ Automation using [cargo-xtask](https://github.com/matklad/cargo-xtask).
Usage: xtask <COMMAND>
Commands:
build-documentation Build documentation for the specified chip
build-documentation-index Build documentation index including the specified packages
build-examples Build all examples for the specified chip
build-package Build the specified package with the given options
build-tests Build all applicable tests or the specified test for a specified chip
bump-version Bump the version of the specified package(s)
fmt-packages Format all packages in the workspace with rustfmt
lint-packages Lint all packages in the workspace with clippy
publish Attempt to publish the specified package
run-doc-tests Run doctests for specified chip and package
run-example Run the given example for the specified chip
run-tests Run all applicable tests or the specified test for a specified chip
run-elfs Run all ELFs in a folder
ci Perform (parts of) the checks done in CI
tag-releases Generate git tags for all new package releases
help Print this message or the help of the given subcommand(s)
build Build-related subcommands
run Run-related subcommands
bump-version Bump the version of the specified package(s)
ci Perform (parts of) the checks done in CI
fmt-packages Format all packages in the workspace with rustfmt
lint-packages Lint all packages in the workspace with clippy
publish Attempt to publish the specified package
tag-releases Generate git tags for all new package releases
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
@ -32,14 +25,15 @@ Options:
You can get help for subcommands, too!
```text
cargo xtask build-examples --help
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
Running `target\debug\xtask.exe build-examples --help`
cargo xtask build examples --help
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `[...]/target/debug/xtask build examples --help`
Build all examples for the specified chip
Usage: xtask.exe build-examples [OPTIONS] <PACKAGE> <CHIP> [EXAMPLE]
Usage: xtask build examples [OPTIONS] <CHIP> <PACKAGE>
...
[...]
```
## Test/example metadata use

199
xtask/src/commands/build.rs Normal file
View File

@ -0,0 +1,199 @@
use std::path::Path;
use anyhow::{Result, bail};
use clap::{Args, Subcommand};
use esp_metadata::Chip;
use strum::IntoEnumIterator as _;
use super::{ExamplesArgs, TestsArgs};
use crate::{
Package,
cargo::{self, CargoAction, CargoArgsBuilder},
firmware::Metadata,
};
// ----------------------------------------------------------------------------
// Subcommands
#[derive(Debug, Subcommand)]
pub enum Build {
/// Build documentation for the specified chip.
Documentation(BuildDocumentationArgs),
/// Build all examples for the specified chip.
Examples(ExamplesArgs),
/// Build the specified package with the given options.
Package(BuildPackageArgs),
/// Build all applicable tests or the specified test for a specified chip.
Tests(TestsArgs),
}
// ----------------------------------------------------------------------------
// Subcommand Arguments
#[derive(Debug, Default, Args)]
pub struct BuildDocumentationArgs {
/// Package(s) to document.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Package::iter())]
pub packages: Vec<Package>,
/// Chip(s) to build documentation for.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Chip::iter())]
pub chips: Vec<Chip>,
/// Base URL of the deployed documentation.
#[arg(long)]
pub base_url: Option<String>,
#[cfg(feature = "preview-docs")]
#[arg(long)]
pub serve: bool,
}
#[derive(Debug, Args)]
pub struct BuildPackageArgs {
/// Package to build.
#[arg(value_enum)]
pub package: Package,
/// Target to build for.
#[arg(long)]
pub target: Option<String>,
/// Features to build with.
#[arg(long, value_delimiter = ',')]
pub features: Vec<String>,
/// Toolchain to build with.
#[arg(long)]
pub toolchain: Option<String>,
/// Don't enabled the default features.
#[arg(long)]
pub no_default_features: bool,
}
// ----------------------------------------------------------------------------
// Subcommand Actions
pub fn build_documentation(workspace: &Path, mut args: BuildDocumentationArgs) -> Result<()> {
crate::documentation::build_documentation(
workspace,
&mut args.packages,
&mut args.chips,
args.base_url,
)?;
crate::documentation::build_documentation_index(workspace, &mut args.packages)?;
#[cfg(feature = "preview-docs")]
if args.serve {
use std::{
thread::{sleep, spawn},
time::Duration,
};
use rocket::fs::{FileServer, Options};
spawn(|| {
sleep(Duration::from_millis(1000));
opener::open_browser("http://127.0.0.1:8000/").ok();
});
rocket::async_main(
{
rocket::build().mount(
"/",
FileServer::new(
"docs",
Options::Index | Options::IndexFile | Options::DotFiles,
),
)
}
.launch(),
)?;
}
Ok(())
}
pub fn build_examples(
args: ExamplesArgs,
examples: Vec<Metadata>,
package_path: &Path,
out_path: &Path,
) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
if examples.iter().any(|ex| ex.matches(&args.example)) {
// Attempt to build only the specified example:
for example in examples.iter().filter(|ex| ex.matches(&args.example)) {
crate::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Build(out_path.to_path_buf()),
1,
args.debug,
)?;
}
Ok(())
} else if args.example.is_some() {
// An invalid argument was provided:
bail!("Example not found or unsupported for the given chip")
} else {
// Attempt to build each supported example, with all required features enabled:
examples.iter().try_for_each(|example| {
crate::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Build(out_path.to_path_buf()),
1,
args.debug,
)
})
}
}
pub fn build_package(workspace: &Path, args: BuildPackageArgs) -> Result<()> {
// Absolute path of the package's root:
let package_path = crate::windows_safe_path(&workspace.join(args.package.to_string()));
// Build the package using the provided features and/or target, if any:
log::info!("Building package '{}'", package_path.display());
if !args.features.is_empty() {
log::info!(" Features: {}", args.features.join(","));
}
if let Some(ref target) = args.target {
log::info!(" Target: {}", target);
}
let mut builder = CargoArgsBuilder::default()
.subcommand("build")
.arg("--release");
if let Some(toolchain) = args.toolchain {
builder = builder.toolchain(toolchain);
}
if let Some(target) = args.target {
// If targeting an Xtensa device, we must use the '+esp' toolchain modifier:
if target.starts_with("xtensa") {
builder = builder.toolchain("esp");
builder = builder.arg("-Zbuild-std=core,alloc")
}
builder = builder.target(target);
}
if !args.features.is_empty() {
builder = builder.features(&args.features);
}
if args.no_default_features {
builder = builder.arg("--no-default-features");
}
let args = builder.build();
log::debug!("{args:#?}");
cargo::run(&args, &package_path)?;
Ok(())
}

147
xtask/src/commands/mod.rs Normal file
View File

@ -0,0 +1,147 @@
use std::path::Path;
use anyhow::{Result, bail};
use clap::Args;
use esp_metadata::Chip;
pub use self::{build::*, run::*};
use crate::{Package, cargo::CargoAction};
mod build;
mod run;
// ----------------------------------------------------------------------------
// Subcommand Arguments
#[derive(Debug, Args)]
pub struct ExamplesArgs {
/// Package whose examples we which to act on.
#[arg(value_enum)]
pub package: Package,
/// Chip to target.
#[arg(value_enum)]
pub chip: Chip,
/// Build examples in debug mode only
#[arg(long)]
pub debug: bool,
/// Optional example to act on (all examples used if omitted)
#[arg(long)]
pub example: Option<String>,
}
#[derive(Debug, Args)]
pub struct TestsArgs {
/// Chip to target.
#[arg(value_enum)]
pub chip: Chip,
/// Repeat the tests for a specific number of times.
#[arg(long, default_value_t = 1)]
pub repeat: usize,
/// Optional test to act on (all tests used if omitted)
#[arg(long, short = 't')]
pub test: Option<String>,
}
// ----------------------------------------------------------------------------
// Subcommand Actions
pub fn examples(workspace: &Path, mut args: ExamplesArgs, action: CargoAction) -> Result<()> {
// Ensure that the package/chip combination provided are valid:
args.package.validate_package_chip(&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 = crate::windows_safe_path(&workspace.join(args.package.to_string()));
let example_path = match args.package {
Package::Examples | Package::QaTest => package_path.join("src").join("bin"),
Package::HilTest => package_path.join("tests"),
_ => package_path.join("examples"),
};
// Load all examples which support the specified chip and parse their metadata:
let mut examples = crate::firmware::load(&example_path)?
.into_iter()
.filter(|example| example.supports_chip(args.chip))
.collect::<Vec<_>>();
// Sort all examples by name:
examples.sort_by_key(|a| a.binary_name());
// Execute the specified action:
match action {
CargoAction::Build(out_path) => build_examples(args, examples, &package_path, &out_path),
CargoAction::Run if args.example.is_some() => run_example(args, examples, &package_path),
CargoAction::Run => run_examples(args, examples, &package_path),
}
}
pub fn tests(workspace: &Path, args: TestsArgs, action: CargoAction) -> Result<()> {
// Absolute path of the 'hil-test' package's root:
let package_path = crate::windows_safe_path(&workspace.join("hil-test"));
// Determine the appropriate build target for the given package and chip:
let target = Package::HilTest.target_triple(&args.chip)?;
// Load all tests which support the specified chip and parse their metadata:
let mut tests = crate::firmware::load(&package_path.join("tests"))?
.into_iter()
.filter(|example| example.supports_chip(args.chip))
.collect::<Vec<_>>();
// Sort all tests by name:
tests.sort_by_key(|a| a.binary_name());
// Execute the specified action:
if tests.iter().any(|test| test.matches(&args.test)) {
for test in tests.iter().filter(|test| test.matches(&args.test)) {
crate::execute_app(
&package_path,
args.chip,
target,
test,
action.clone(),
args.repeat,
false,
)?;
}
Ok(())
} else if args.test.is_some() {
bail!("Test not found or unsupported for the given chip")
} else {
let mut failed = Vec::new();
for test in tests {
if crate::execute_app(
&package_path,
args.chip,
target,
&test,
action.clone(),
args.repeat,
false,
)
.is_err()
{
failed.push(test.name_with_configuration());
}
}
if !failed.is_empty() {
bail!("Failed tests: {:#?}", failed);
}
Ok(())
}
}

228
xtask/src/commands/run.rs Normal file
View File

@ -0,0 +1,228 @@
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context as _, Result, bail, ensure};
use clap::{Args, Subcommand};
use esp_metadata::Chip;
use super::{ExamplesArgs, TestsArgs};
use crate::{
cargo::{CargoAction, CargoArgsBuilder},
firmware::Metadata,
};
// ----------------------------------------------------------------------------
// Subcommands
#[derive(Debug, Subcommand)]
pub enum Run {
/// Run doctests for specified chip and package.
DocTests(ExamplesArgs),
/// Run all ELFs in a folder.
Elfs(RunElfsArgs),
/// Run the given example for the specified chip.
Example(ExamplesArgs),
/// Run all applicable tests or the specified test for a specified chip.
Tests(TestsArgs),
}
// ----------------------------------------------------------------------------
// Subcommand Arguments
#[derive(Debug, Args)]
pub struct RunElfsArgs {
/// Which chip to run the tests for.
#[arg(value_enum)]
pub chip: Chip,
/// Path to the ELFs.
pub path: PathBuf,
}
// ----------------------------------------------------------------------------
// Subcommand Actions
pub fn run_doc_tests(workspace: &Path, args: ExamplesArgs) -> Result<()> {
let chip = args.chip;
let package_name = args.package.to_string();
let package_path = crate::windows_safe_path(&workspace.join(&package_name));
// Determine the appropriate build target, and cargo features for the given
// package and chip:
let target = args.package.target_triple(&chip)?;
let features = vec![chip.to_string(), "unstable".to_string()];
// We need `nightly` for building the doc tests, unfortunately:
let toolchain = if chip.is_xtensa() { "esp" } else { "nightly" };
// Build up an array of command-line arguments to pass to `cargo`:
let builder = CargoArgsBuilder::default()
.toolchain(toolchain)
.subcommand("test")
.arg("--doc")
.arg("-Zdoctest-xcompile")
.arg("-Zbuild-std=core,panic_abort")
.target(target)
.features(&features)
.arg("--release");
let args = builder.build();
log::debug!("{args:#?}");
// Execute `cargo doc` from the package root:
crate::cargo::run(&args, &package_path)?;
Ok(())
}
pub fn run_elfs(args: RunElfsArgs) -> Result<()> {
let mut failed: Vec<String> = Vec::new();
for elf in fs::read_dir(&args.path)? {
let entry = elf?;
let elf_path = entry.path();
let elf_name = elf_path
.with_extension("")
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
log::info!("Running test '{}' for '{}'", elf_name, args.chip);
let mut command = Command::new("probe-rs");
command.arg("run").arg(elf_path);
if args.chip == Chip::Esp32c2 {
command.arg("--speed").arg("15000");
};
command.arg("--verify");
let mut command = command.spawn().context("Failed to execute probe-rs")?;
let status = command
.wait()
.context("Error while waiting for probe-rs to exit")?;
log::info!("'{elf_name}' done");
if !status.success() {
failed.push(elf_name);
}
}
if !failed.is_empty() {
bail!("Failed tests: {:?}", failed);
}
Ok(())
}
pub fn run_example(args: ExamplesArgs, examples: Vec<Metadata>, package_path: &Path) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
// Filter the examples down to only the binary we're interested in, assuming it
// actually supports the specified chip:
let mut found_one = false;
for example in examples.iter().filter(|ex| ex.matches(&args.example)) {
found_one = true;
crate::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Run,
1,
args.debug,
)?;
}
ensure!(
found_one,
"Example not found or unsupported for {}",
args.chip
);
Ok(())
}
pub fn run_examples(
args: ExamplesArgs,
examples: Vec<Metadata>,
package_path: &Path,
) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
// Filter the examples down to only the binaries we're interested in
let mut examples: Vec<Metadata> = examples
.iter()
.filter(|ex| ex.supports_chip(args.chip))
.cloned()
.collect();
examples.sort_by_key(|ex| ex.tag());
let console = console::Term::stdout();
for example in examples {
let mut skip = false;
log::info!("Running example '{}'", example.output_file_name());
if let Some(description) = example.description() {
log::info!(
"\n\n{}\n\nPress ENTER to run example, `s` to skip",
description.trim()
);
} else {
log::info!("\n\nPress ENTER to run example, `s` to skip");
}
loop {
let key = console.read_key();
match key {
Ok(console::Key::Enter) => break,
Ok(console::Key::Char('s')) => {
skip = true;
break;
}
_ => (),
}
}
if !skip {
while !skip
&& crate::execute_app(
package_path,
args.chip,
target,
&example,
CargoAction::Run,
1,
args.debug,
)
.is_err()
{
log::info!("Failed to run example. Retry or skip? (r/s)");
loop {
let key = console.read_key();
match key {
Ok(console::Key::Char('r')) => break,
Ok(console::Key::Char('s')) => {
skip = true;
break;
}
_ => (),
}
}
}
}
}
Ok(())
}

View File

@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{ensure, Context as _, Result};
use anyhow::{Context as _, Result, ensure};
use clap::ValueEnum;
use esp_metadata::Config;
use kuchikiki::traits::*;
@ -13,7 +13,7 @@ use minijinja::Value;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::{cargo::CargoArgsBuilder, Chip, Package};
use crate::{Chip, Package, cargo::CargoArgsBuilder};
// ----------------------------------------------------------------------------
// Build Documentation
@ -159,11 +159,7 @@ fn build_documentation_for_package(
if package.chip_features_matter() {
version.to_string()
} else {
format!(
"{}/{}",
version.to_string(),
package.to_string().replace('-', "_")
)
format!("{}/{}", version, package.to_string().replace('-', "_"))
}
)
.as_bytes(),
@ -200,7 +196,7 @@ fn cargo_doc(workspace: &Path, package: Package, chip: Option<Chip>) -> Result<P
let mut features = vec![];
if let Some(chip) = &chip {
features.push(chip.to_string());
features.extend(package.feature_rules(Config::for_chip(&chip)));
features.extend(package.feature_rules(Config::for_chip(chip)));
} else {
features.extend(package.feature_rules(&Config::empty()));
}
@ -319,7 +315,9 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) ->
// If the chip features are not relevant, then there is no need to generate an
// index for the given package's documentation:
if !package.chip_features_matter() {
log::warn!("Package '{package}' does not have device-specific documentation, no need to generate an index");
log::warn!(
"Package '{package}' does not have device-specific documentation, no need to generate an index"
);
continue;
}
@ -354,14 +352,12 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) ->
.map(|path| {
let chip = path
.components()
.last()
.next_back()
.unwrap()
.as_os_str()
.to_string_lossy();
let chip = Chip::from_str(&chip, true).unwrap();
chip
Chip::from_str(&chip, true).unwrap()
})
.collect::<Vec<_>>();
@ -376,7 +372,7 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) ->
minijinja::context! { metadata => meta },
)?;
let path = version_path.join("index.html");
fs::write(&path, html).context(format!("Failed to write index.html"))?;
fs::write(&path, html).context("Failed to write index.html")?;
log::info!("Created {}", path.display());
}
}
@ -388,7 +384,7 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) ->
)
.context("Failed to copy esp-rs.svg")?;
let meta = generate_documentation_meta_for_index(&workspace)?;
let meta = generate_documentation_meta_for_index(workspace)?;
// Render the template to HTML and write it out to the desired path:
let html = render_template(
@ -397,7 +393,7 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) ->
minijinja::context! { metadata => meta },
)?;
let path = docs_path.join("index.html");
fs::write(&path, html).context(format!("Failed to write index.html"))?;
fs::write(&path, html).context("Failed to write index.html")?;
log::info!("Created {}", path.display());
Ok(())

View File

@ -12,6 +12,7 @@ use strum::{Display, EnumIter, IntoEnumIterator as _};
use crate::{cargo::CargoArgsBuilder, firmware::Metadata};
pub mod cargo;
pub mod commands;
pub mod documentation;
pub mod firmware;
@ -172,15 +173,12 @@ impl Package {
pub fn lint_feature_rules(&self, _config: &Config) -> Vec<Vec<String>> {
let mut cases = Vec::new();
match self {
Package::EspWifi => {
// minimal set of features that when enabled _should_ still compile
cases.push(vec![
"esp-hal/unstable".to_owned(),
"builtin-scheduler".to_owned(),
]);
}
_ => {}
if self == &Package::EspWifi {
// Minimal set of features that when enabled _should_ still compile:
cases.push(vec![
"esp-hal/unstable".to_owned(),
"builtin-scheduler".to_owned(),
]);
}
cases
@ -307,7 +305,7 @@ pub fn execute_app(
let output = cargo::run_with_env(&args, package_path, env_vars, true)?;
for line in output.lines() {
if let Ok(artifact) = serde_json::from_str::<cargo::Artifact>(line) {
let out_dir = out_dir.join(&chip.to_string());
let out_dir = out_dir.join(chip.to_string());
std::fs::create_dir_all(&out_dir)?;
let output_file = out_dir.join(app.output_file_name());
@ -327,56 +325,6 @@ pub fn execute_app(
Ok(())
}
/// Build the specified package, using the given toolchain/target/features if
/// provided.
pub fn build_package(
package_path: &Path,
features: Vec<String>,
no_default_features: bool,
toolchain: Option<String>,
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("--release");
if let Some(toolchain) = toolchain {
builder = builder.toolchain(toolchain);
}
if let Some(target) = target {
// If targeting an Xtensa device, we must use the '+esp' toolchain modifier:
if target.starts_with("xtensa") {
builder = builder.toolchain("esp");
builder = builder.arg("-Zbuild-std=core,alloc")
}
builder = builder.target(target);
}
if !features.is_empty() {
builder = builder.features(&features);
}
if no_default_features {
builder = builder.arg("--no-default-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");
@ -431,9 +379,9 @@ pub fn bump_version(workspace: &Path, package: Package, amount: Version) -> Resu
" Bumping {package} version for package {pkg}: ({prev_version} -> {version})"
);
manifest["dependencies"].as_table_mut().map(|table| {
table[&package.to_string()]["version"] = toml_edit::value(version.to_string())
});
if let Some(table) = manifest["dependencies"].as_table_mut() {
table[&package.to_string()]["version"] = toml_edit::value(version.to_string());
}
fs::write(&manifest_path, manifest.to_string())
.with_context(|| format!("Could not write {}", manifest_path.display()))?;
@ -471,10 +419,8 @@ pub fn package_paths(workspace: &Path) -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
for entry in fs::read_dir(workspace)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
if entry.path().join("Cargo.toml").exists() {
paths.push(entry.path());
}
if entry.file_type()?.is_dir() && entry.path().join("Cargo.toml").exists() {
paths.push(entry.path());
}
}

View File

@ -5,7 +5,7 @@ use std::{
time::Instant,
};
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{Result, bail};
use clap::{Args, Parser};
use esp_metadata::{Chip, Config};
use strum::IntoEnumIterator;
@ -13,7 +13,7 @@ use xtask::{
Package,
Version,
cargo::{CargoAction, CargoArgsBuilder},
firmware::Metadata,
commands::*,
};
// ----------------------------------------------------------------------------
@ -21,18 +21,17 @@ use xtask::{
#[derive(Debug, Parser)]
enum Cli {
/// Build documentation for the specified chip.
BuildDocumentation(BuildDocumentationArgs),
/// Build documentation index including the specified packages.
BuildDocumentationIndex(BuildDocumentationIndexArgs),
/// Build all examples for the specified chip.
BuildExamples(ExampleArgs),
/// Build the specified package with the given options.
BuildPackage(BuildPackageArgs),
/// Build all applicable tests or the specified test for a specified chip.
BuildTests(TestArgs),
/// Build-related subcommands
#[clap(subcommand)]
Build(Build),
/// Run-related subcommands
#[clap(subcommand)]
Run(Run),
/// Bump the version of the specified package(s).
BumpVersion(BumpVersionArgs),
/// Perform (parts of) the checks done in CI
Ci(CiArgs),
/// Format all packages in the workspace with rustfmt
#[clap(alias = "format-packages")]
FmtPackages(FmtPackagesArgs),
@ -40,91 +39,10 @@ enum Cli {
LintPackages(LintPackagesArgs),
/// Attempt to publish the specified package.
Publish(PublishArgs),
/// Run doctests for specified chip and package.
#[clap(alias = "run-doc-test")]
RunDocTests(ExampleArgs),
/// Run the given example for the specified chip.
RunExample(ExampleArgs),
/// Run all applicable tests or the specified test for a specified chip.
RunTests(TestArgs),
/// Run all ELFs in a folder.
RunElfs(RunElfArgs),
/// Perform (parts of) the checks done in CI
Ci(CiArgs),
/// Generate git tags for all new package releases.
TagReleases(TagReleasesArgs),
}
#[derive(Debug, Args)]
struct ExampleArgs {
/// Package whose examples we which to act on.
#[arg(value_enum)]
package: Package,
/// Chip to target.
#[arg(value_enum)]
chip: Chip,
/// Optional example to act on (all examples used if omitted)
example: Option<String>,
/// Build examples in debug mode only
#[arg(long)]
debug: bool,
}
#[derive(Debug, Args)]
struct TestArgs {
/// Chip to target.
#[arg(value_enum)]
chip: Chip,
/// Optional test to act on (all tests used if omitted)
#[arg(short = 't', long)]
test: Option<String>,
/// Repeat the tests for a specific number of times.
#[arg(long)]
repeat: Option<usize>,
}
#[derive(Debug, Args)]
struct BuildDocumentationArgs {
/// Package(s) to document.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Package::iter())]
packages: Vec<Package>,
/// Chip(s) to build documentation for.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Chip::iter())]
chips: Vec<Chip>,
/// Base URL of the deployed documentation.
#[arg(long)]
base_url: Option<String>,
}
#[derive(Debug, Args)]
struct BuildDocumentationIndexArgs {
/// Package(s) to build documentation index for.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Package::iter())]
packages: Vec<Package>,
#[cfg(feature = "preview-docs")]
#[arg(long)]
serve: bool,
}
#[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>,
/// Don't enabled the default features.
#[arg(long)]
no_default_features: bool,
}
#[derive(Debug, Args)]
struct BumpVersionArgs {
/// How much to bump the version by.
@ -135,6 +53,13 @@ struct BumpVersionArgs {
packages: Vec<Package>,
}
#[derive(Debug, Args)]
struct CiArgs {
/// Chip to target.
#[arg(value_enum)]
chip: Chip,
}
#[derive(Debug, Args)]
struct FmtPackagesArgs {
/// Run in 'check' mode; exists with 0 if formatted correctly, 1 otherwise
@ -146,15 +71,6 @@ struct FmtPackagesArgs {
packages: Vec<Package>,
}
#[derive(Debug, Args)]
struct GenerateEfuseFieldsArgs {
/// Path to the local ESP-IDF repository.
idf_path: PathBuf,
/// Chip to build eFuse fields table for.
#[arg(value_enum)]
chip: Chip,
}
#[derive(Debug, Args)]
struct LintPackagesArgs {
/// Package(s) to target.
@ -181,22 +97,6 @@ struct PublishArgs {
no_dry_run: bool,
}
#[derive(Debug, Args)]
struct RunElfArgs {
/// Which chip to run the tests for.
#[arg(value_enum)]
chip: Chip,
/// Path to the ELFs.
path: PathBuf,
}
#[derive(Debug, Args)]
struct CiArgs {
/// Chip to target.
#[arg(value_enum)]
chip: Chip,
}
#[derive(Debug, Args)]
struct TagReleasesArgs {
/// Package(s) to tag.
@ -218,28 +118,35 @@ fn main() -> Result<()> {
let target_path = Path::new("target");
match Cli::parse() {
Cli::BuildDocumentation(args) => build_documentation(&workspace, args),
Cli::BuildDocumentationIndex(args) => build_documentation_index(&workspace, args),
Cli::BuildExamples(args) => examples(
&workspace,
args,
CargoAction::Build(target_path.join("examples")),
),
Cli::BuildPackage(args) => build_package(&workspace, args),
Cli::BuildTests(args) => tests(
&workspace,
args,
CargoAction::Build(target_path.join("tests")),
),
// Build-related subcommands:
Cli::Build(build) => match build {
Build::Documentation(args) => build_documentation(&workspace, args),
Build::Examples(args) => examples(
&workspace,
args,
CargoAction::Build(target_path.join("examples")),
),
Build::Package(args) => build_package(&workspace, args),
Build::Tests(args) => tests(
&workspace,
args,
CargoAction::Build(target_path.join("tests")),
),
},
// Run-related subcommands:
Cli::Run(run) => match run {
Run::DocTests(args) => run_doc_tests(&workspace, args),
Run::Elfs(args) => run_elfs(args),
Run::Example(args) => examples(&workspace, args, CargoAction::Run),
Run::Tests(args) => tests(&workspace, args, CargoAction::Run),
},
Cli::BumpVersion(args) => bump_version(&workspace, args),
Cli::Ci(args) => run_ci_checks(&workspace, args),
Cli::FmtPackages(args) => fmt_packages(&workspace, args),
Cli::LintPackages(args) => lint_packages(&workspace, args),
Cli::Publish(args) => publish(&workspace, args),
Cli::RunDocTests(args) => run_doc_tests(&workspace, args),
Cli::RunElfs(args) => run_elfs(args),
Cli::RunExample(args) => examples(&workspace, args, CargoAction::Run),
Cli::RunTests(args) => tests(&workspace, args, CargoAction::Run),
Cli::Ci(args) => run_ci_checks(&workspace, args),
Cli::TagReleases(args) => tag_releases(&workspace, args),
}
}
@ -247,314 +154,6 @@ fn main() -> Result<()> {
// ----------------------------------------------------------------------------
// Subcommands
fn examples(workspace: &Path, mut args: ExampleArgs, action: CargoAction) -> Result<()> {
// Ensure that the package/chip combination provided are valid:
args.package.validate_package_chip(&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 = xtask::windows_safe_path(&workspace.join(args.package.to_string()));
let example_path = match args.package {
Package::Examples | Package::QaTest => package_path.join("src").join("bin"),
Package::HilTest => package_path.join("tests"),
_ => package_path.join("examples"),
};
// Load all examples which support the specified chip and parse their metadata:
let mut examples = xtask::firmware::load(&example_path)?
.iter()
.filter_map(|example| {
if example.supports_chip(args.chip) {
Some(example.clone())
} else {
None
}
})
.collect::<Vec<_>>();
// Sort all examples by name:
examples.sort_by_key(|a| a.binary_name());
// Execute the specified action:
match action {
CargoAction::Build(out_path) => build_examples(args, examples, &package_path, out_path),
CargoAction::Run if args.example.is_some() => run_example(args, examples, &package_path),
CargoAction::Run => run_examples(args, examples, &package_path),
}
}
fn build_examples(
args: ExampleArgs,
examples: Vec<Metadata>,
package_path: &Path,
out_path: PathBuf,
) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
if examples
.iter()
.find(|ex| ex.matches(&args.example))
.is_some()
{
// Attempt to build only the specified example:
for example in examples.iter().filter(|ex| ex.matches(&args.example)) {
xtask::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Build(out_path.clone()),
1,
args.debug,
)?;
}
Ok(())
} else if args.example.is_some() {
// An invalid argument was provided:
bail!("Example not found or unsupported for the given chip")
} else {
// Attempt to build each supported example, with all required features enabled:
examples.iter().try_for_each(|example| {
xtask::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Build(out_path.clone()),
1,
args.debug,
)
})
}
}
fn run_example(args: ExampleArgs, examples: Vec<Metadata>, package_path: &Path) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
// Filter the examples down to only the binary we're interested in, assuming it
// actually supports the specified chip:
let mut found_one = false;
for example in examples.iter().filter(|ex| ex.matches(&args.example)) {
found_one = true;
xtask::execute_app(
package_path,
args.chip,
target,
example,
CargoAction::Run,
1,
args.debug,
)?;
}
ensure!(
found_one,
"Example not found or unsupported for {}",
args.chip
);
Ok(())
}
fn run_examples(args: ExampleArgs, examples: Vec<Metadata>, package_path: &Path) -> Result<()> {
// Determine the appropriate build target for the given package and chip:
let target = args.package.target_triple(&args.chip)?;
// Filter the examples down to only the binaries we're interested in
let mut examples: Vec<Metadata> = examples
.iter()
.filter(|ex| ex.supports_chip(args.chip))
.cloned()
.collect();
examples.sort_by_key(|ex| ex.tag());
let console = console::Term::stdout();
for example in examples {
let mut skip = false;
log::info!("Running example '{}'", example.output_file_name());
if let Some(description) = example.description() {
log::info!(
"\n\n{}\n\nPress ENTER to run example, `s` to skip",
description.trim()
);
} else {
log::info!("\n\nPress ENTER to run example, `s` to skip");
}
loop {
let key = console.read_key();
match key {
Ok(console::Key::Enter) => break,
Ok(console::Key::Char('s')) => {
skip = true;
break;
}
_ => (),
}
}
if !skip {
while !skip
&& xtask::execute_app(
package_path,
args.chip,
target,
&example,
CargoAction::Run,
1,
args.debug,
)
.is_err()
{
log::info!("Failed to run example. Retry or skip? (r/s)");
loop {
let key = console.read_key();
match key {
Ok(console::Key::Char('r')) => break,
Ok(console::Key::Char('s')) => {
skip = true;
break;
}
_ => (),
}
}
}
}
}
Ok(())
}
fn tests(workspace: &Path, args: TestArgs, action: CargoAction) -> Result<()> {
// Absolute path of the 'hil-test' package's root:
let package_path = xtask::windows_safe_path(&workspace.join("hil-test"));
// Determine the appropriate build target for the given package and chip:
let target = Package::HilTest.target_triple(&args.chip)?;
// Load all tests which support the specified chip and parse their metadata:
let mut tests = xtask::firmware::load(&package_path.join("tests"))?
.into_iter()
.filter(|example| example.supports_chip(args.chip))
.collect::<Vec<_>>();
// Sort all tests by name:
tests.sort_by_key(|a| a.binary_name());
// Execute the specified action:
if tests.iter().find(|test| test.matches(&args.test)).is_some() {
for test in tests.iter().filter(|test| test.matches(&args.test)) {
xtask::execute_app(
&package_path,
args.chip,
target,
test,
action.clone(),
args.repeat.unwrap_or(1),
false,
)?;
}
Ok(())
} else if args.test.is_some() {
bail!("Test not found or unsupported for the given chip")
} else {
let mut failed = Vec::new();
for test in tests {
if xtask::execute_app(
&package_path,
args.chip,
target,
&test,
action.clone(),
args.repeat.unwrap_or(1),
false,
)
.is_err()
{
failed.push(test.name_with_configuration());
}
}
if !failed.is_empty() {
bail!("Failed tests: {:#?}", failed);
}
Ok(())
}
}
fn build_documentation(workspace: &Path, mut args: BuildDocumentationArgs) -> Result<()> {
xtask::documentation::build_documentation(
workspace,
&mut args.packages,
&mut args.chips,
args.base_url,
)
}
fn build_documentation_index(
workspace: &Path,
mut args: BuildDocumentationIndexArgs,
) -> Result<()> {
xtask::documentation::build_documentation_index(workspace, &mut args.packages)?;
#[cfg(feature = "preview-docs")]
if args.serve {
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_millis(1000));
opener::open_browser("http://127.0.0.1:8000/").ok();
});
rocket::async_main(
{
rocket::build().mount(
"/",
rocket::fs::FileServer::new(
"docs",
rocket::fs::Options::Index
| rocket::fs::Options::IndexFile
| rocket::fs::Options::DotFiles,
),
)
}
.launch(),
)?;
}
Ok(())
}
fn build_package(workspace: &Path, args: BuildPackageArgs) -> Result<()> {
// Absolute path of the package's root:
let package_path = xtask::windows_safe_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.no_default_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 {
@ -617,7 +216,7 @@ fn lint_packages(workspace: &Path, args: LintPackagesArgs) -> Result<()> {
for chip in &args.chips {
let device = Config::for_chip(chip);
if let Err(_) = package.validate_package_chip(chip) {
if package.validate_package_chip(chip).is_err() {
continue;
}
@ -734,83 +333,6 @@ fn publish(workspace: &Path, args: PublishArgs) -> Result<()> {
Ok(())
}
fn run_elfs(args: RunElfArgs) -> Result<()> {
let mut failed: Vec<String> = Vec::new();
for elf in fs::read_dir(&args.path)? {
let entry = elf?;
let elf_path = entry.path();
let elf_name = elf_path
.with_extension("")
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
log::info!("Running test '{}' for '{}'", elf_name, args.chip);
let mut command = Command::new("probe-rs");
command.arg("run").arg(elf_path);
if args.chip == Chip::Esp32c2 {
command.arg("--speed").arg("15000");
};
command.arg("--verify");
let mut command = command.spawn().context("Failed to execute probe-rs")?;
let status = command
.wait()
.context("Error while waiting for probe-rs to exit")?;
log::info!("'{elf_name}' done");
if !status.success() {
failed.push(elf_name);
}
}
if !failed.is_empty() {
bail!("Failed tests: {:?}", failed);
}
Ok(())
}
fn run_doc_tests(workspace: &Path, args: ExampleArgs) -> Result<()> {
let chip = args.chip;
let package_name = args.package.to_string();
let package_path = xtask::windows_safe_path(&workspace.join(&package_name));
// Determine the appropriate build target, and cargo features for the given
// package and chip:
let target = args.package.target_triple(&chip)?;
let features = vec![chip.to_string(), "unstable".to_string()];
// We need `nightly` for building the doc tests, unfortunately:
let toolchain = if chip.is_xtensa() { "esp" } else { "nightly" };
// Build up an array of command-line arguments to pass to `cargo`:
let builder = CargoArgsBuilder::default()
.toolchain(toolchain)
.subcommand("test")
.arg("--doc")
.arg("-Zdoctest-xcompile")
.arg("-Zbuild-std=core,panic_abort")
.target(target)
.features(&features)
.arg("--release");
let args = builder.build();
log::debug!("{args:#?}");
// Execute `cargo doc` from the package root:
xtask::cargo::run(&args, &package_path)?;
Ok(())
}
fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
let mut failure = false;
let started_at = Instant::now();
@ -832,7 +354,7 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
// Check doc-tests
run_doc_tests(
workspace,
ExampleArgs {
ExamplesArgs {
package: Package::EspHal,
chip: args.chip,
example: None,
@ -848,7 +370,7 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
BuildDocumentationArgs {
packages: vec![Package::EspHal, Package::EspWifi, Package::EspHalEmbassy],
chips: vec![args.chip],
base_url: None,
..Default::default()
},
)
.inspect_err(|_| failure = true)
@ -863,7 +385,7 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
// expects it
examples(
workspace,
ExampleArgs {
ExamplesArgs {
package: Package::EspLpHal,
chip: args.chip,
example: None,
@ -879,7 +401,7 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
let from_dir = PathBuf::from(format!(
"./esp-lp-hal/target/{}/release/examples/{}",
args.chip.target(),
args.chip.to_string()
args.chip
));
let to_dir = PathBuf::from(format!(
"./esp-lp-hal/target/{}/release/examples",
@ -901,7 +423,7 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
BuildDocumentationArgs {
packages: vec![Package::EspLpHal],
chips: vec![args.chip],
base_url: None,
..Default::default()
},
)
.inspect_err(|_| failure = true)
@ -925,13 +447,13 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
// Build (examples)
examples(
workspace,
ExampleArgs {
ExamplesArgs {
package: Package::Examples,
chip: args.chip,
example: None,
debug: true,
},
CargoAction::Build(PathBuf::from(format!("./examples/target/"))),
CargoAction::Build(PathBuf::from("./examples/target/")),
)
.inspect_err(|_| failure = true)
.ok();
@ -939,13 +461,13 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> {
// Build (qa-test)
examples(
workspace,
ExampleArgs {
ExamplesArgs {
package: Package::QaTest,
chip: args.chip,
example: None,
debug: true,
},
CargoAction::Build(PathBuf::from(format!("./qa-test/target/"))),
CargoAction::Build(PathBuf::from("./qa-test/target/")),
)
.inspect_err(|_| failure = true)
.ok();