diff --git a/.github/actions/book-prerequisites/action.yml b/.github/actions/book-prerequisites/action.yml new file mode 100644 index 000000000000..51ed6a63d01e --- /dev/null +++ b/.github/actions/book-prerequisites/action.yml @@ -0,0 +1,20 @@ +name: Install prerequisites for building the book +description: "Install mdBook, mdBook-mermaid, and lychee" + +runs: + using: "composite" + steps: + - name: Install lychee + uses: taiki-e/install-action@v2 + with: + tool: lychee + + - name: Install mdbook + uses: taiki-e/install-action@v2 + with: + tool: mdbook + + - name: Install mdbook-mermaid + uses: taiki-e/install-action@v2 + with: + tool: mdbook-mermaid \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dbdbc0e6ee68..45a3aac4812e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,10 @@ env: OLDOLDSTABLE_VERSION: 0.5 OLDOLDOLDSTABLE_VERSION: 0.4 + OLD_BOOKS_ROOT: old/ + CURRENT_BOOK_ROOT: current/ + WEB_ROOT: deploy/ + jobs: # Run cargo xtask format-check formatcheck: @@ -131,7 +135,7 @@ jobs: - name: Check the examples if: ${{ matrix.backend == 'thumbv8-base' }} - run: cargo xtask --backend ${{ matrix.backend }} --exampleexclude pool example-check + run: cargo xtask --backend ${{ matrix.backend }} example-check --exclude pool - name: Check the examples if: ${{ matrix.backend != 'thumbv8-base' }} @@ -248,191 +252,34 @@ jobs: - name: Run cargo test run: cargo xtask --deny-warnings --backend ${{ matrix.backend }} test ${{ matrix.package }} - # Build documentation, check links - docs: - name: build docs + # Build book for current version, check links + current-book: + name: Build book for the current version runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v3 - - name: Install lychee - uses: taiki-e/install-action@v2 - with: - tool: lychee - - - name: Remove cargo-config - run: rm -f .cargo/config.toml + - name: Install book prerequisites + uses: ./.github/actions/book-prerequisites + # Build book & docs for the current version + # + # This also checks the links for the API docs and + # the book using lychee - name: Build docs - # TODO: Any difference between backends? - run: cargo doc --features thumbv7-backend - - - name: Check links - run: | - td=$(mktemp -d) - cp -r target/doc $td/api - echo rtic - lychee --offline --format detailed $td/api/rtic/ - - echo rtic_common - lychee --offline --format detailed $td/api/rtic_common/ - - echo rtic_macros - lychee --offline --format detailed $td/api/rtic_macros/ - - echo rtic_monotonics - lychee --offline --format detailed $td/api/rtic_monotonics/ - - echo rtic_sync - lychee --offline --format detailed $td/api/rtic_sync/ - - echo rtic_time - lychee --offline --format detailed $td/api/rtic_time/ - - - - name: Archive the API docs - run: | - cp -r target/doc apidocs - tar -cf apidocs.tar apidocs - - - name: Store the API docs - uses: actions/upload-artifact@v3 - with: - name: apidocs - path: apidocs.tar - - # Build the books - mdbook: - name: build mdbook - needs: docs - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install lychee - uses: taiki-e/install-action@v2 - with: - tool: lychee + run: cargo xtask book --output-path current - - name: Install mdbook - uses: taiki-e/install-action@v2 - with: - tool: mdbook - - - name: Install mdbook-mermaid - uses: taiki-e/install-action@v2 - with: - tool: mdbook-mermaid - - - name: Build book in English - run: cargo xtask book - - - name: Download built API docs - uses: actions/download-artifact@v3 - with: - name: apidocs - - - name: Extract the API docs - run: tar -xf apidocs.tar + - name: Archive the current documentation + run: tar -czf current-book.tar.gz current/ - - name: Check links - run: | - td=$(mktemp -d) - mkdir $td/book - cp -r book/en/book $td/book/en - cp LICENSE-* $td/book/en - cp -r apidocs/ $td/api - - lychee --offline --format detailed $td/book/en/ - mv $td bookroot - - - name: Archive the book + API docs - run: | - tar -cf book.tar bookroot - - - name: Store the Book + API docs + - name: Store the current API docs + book uses: actions/upload-artifact@v3 with: - name: book - path: book.tar + name: current-book + path: current-book.tar.gz - mdbookold: - name: build docs and mdbook for older releases - needs: pushtostablebranch - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install mdbook - uses: taiki-e/install-action@v2 - with: - tool: mdbook - - - name: Install mdbook-mermaid - uses: taiki-e/install-action@v2 - with: - tool: mdbook-mermaid - - - name: Remove cargo-config - run: rm -f .cargo/config - - - name: Prepare output folder - run: mkdir -p mdbookold - - - name: Fetch and build books for older versions - run: | - # The latest stable must be the first element in the array - vers=( "${{ env.STABLE_VERSION }}" "${{ env.OLDSTABLE_VERSION }}" ) - langs=( en ) - root=$(pwd) - webroot=$(pwd)/mdbookold - - for ver in ${vers[@]}; do - - mkdir -p src/$ver - src=$root/src/$ver - curl -L https://github.com/rtic-rs/rtic/archive/release/v${ver}.tar.gz | tar xz --strip-components 1 -C $src - - pushd $src - rm -f .cargo/config - # Version 1 and below uses cargo doc directly - if [[ $ver -gt 1 ]] - then - # Version 2 and above - cargo xtask doc - else - cargo doc || cargo doc --features timer-queue - fi - mkdir -p $webroot/$ver/book - cp -r target/doc $webroot/$ver/api - - sed 's|URL|rtic/index.html|g' $root/redirect.html > $webroot/$ver/api/index.html - popd - - for lang in ${langs[@]}; do - cargo xtask book build $src/book/$lang - - cp -r $src/book/$lang/book $webroot/$ver/book/$lang - cp LICENSE-* $webroot/$ver/book/$lang/ - done - # using master branch redirect file - sed 's|URL|book/en|g' $root/redirect.html > $webroot/$ver/index.html - - rm -rf $src - done - - - name: Archive the old books - run: | - tar -cf mdbookold.tar mdbookold - - - name: Store the old API docs - uses: actions/upload-artifact@v3 - with: - name: mdbookold - path: mdbookold.tar + # All the steps below are for the book & archiving purposes parseversion: name: Parse the master branch RTIC version @@ -448,14 +295,13 @@ jobs: id: parseversion # Parse metadata for version number, extract the Semver Major run: | - VERSION=$(cargo metadata --format-version 1 --no-deps --offline | jq -r '.packages[] | select(.name =="rtic") | .version') + VERSION=$(./scripts/parse-version.sh) VERSIONMAJOR=${VERSION%.*.*} echo "branch=release/v$VERSIONMAJOR" >> "$GITHUB_OUTPUT" echo "versionmajor=$VERSIONMAJOR" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - # Update stable branch # # This is only valid when current stable resides in @@ -465,7 +311,7 @@ jobs: # Thus, no need to push changes # # This needs to run before book is built, as bookbuilding fetches from the branch - pushtostablebranch: + merge-to-stable-branch: name: Also push branch into release/vX when pushing to master runs-on: ubuntu-22.04 needs: @@ -489,127 +335,70 @@ jobs: if: ${{ env.versionmajor != env.STABLE_VERSION }} run: echo "Master branch contains a development release, no git push performed" + old-books: + name: Build book and API docs for older versions + runs-on: ubuntu-22.04 + needs: + - merge-to-stable-branch + steps: + - uses: actions/checkout@v3 + + - name: Install book prerequisites + uses: ./.github/actions/book-prerequisites + + # Build book for older releases + - name: Build old docs + run: ./scripts/build-old-books.sh "${{ env.STABLE_VERSION }} ${{ env.OLDSTABLE_VERSION }}" + + - name: Archive the current documentation + run: tar -czf old-books.tar.gz old/ + + - name: Store the old versions of API docs + book + uses: actions/upload-artifact@v3 + with: + name: old-books + path: old-books.tar.gz + # Only runs when pushing to master branch # If all tests pass, then deploy stage is run deploy: name: deploy runs-on: ubuntu-22.04 needs: - - pushtostablebranch - - docs - - mdbookold - - mdbook + - merge-to-stable-branch + - current-book + - old-books # Only run this when pushing to master branch if: github.ref == 'refs/heads/master' steps: - uses: actions/checkout@v3 - - name: Install lychee - uses: taiki-e/install-action@v2 - with: - tool: lychee - - - name: Install mdbook-mermaid - uses: taiki-e/install-action@v2 - with: - tool: mdbook-mermaid - - - name: mdBook Action - uses: peaceiris/actions-mdbook@v1 - with: - mdbook-version: 'latest' - - - name: Remove cargo-config - run: rm -f .cargo/config - - name: Download built dev-ver book and API docs uses: actions/download-artifact@v3 with: - name: book - - - name: Extract the dev-version book and API docs - run: | - tar -xf book.tar + name: current-book - name: Download built old versions of books and API docs uses: actions/download-artifact@v3 with: - name: mdbookold - - - name: Extract the old version books and API docs - run: | - tar -xf mdbookold.tar + name: old-books - name: Prepare books shell: 'script --return --quiet --command "bash {0}"' run: | - langs=( en ) - devver=( dev ) - # The latest stable must be the first element in the array - vers=( "${{ env.STABLE_VERSION }}" "${{ env.OLDSTABLE_VERSION }}" ) - - # All releases start with "v" - # followed by MAJOR.MINOR.PATCH, see semver.org - # Store first in array as stable - stable=${vers} - crateversion=${{ env.versionmajor }} - - echo "Latest stable version: $stable" - echo "Current crate version: $crateversion" - - # Create directories - td=$(mktemp -d) - mkdir -p $td/$devver/ - cp -r bookroot/* $td/$devver/ - - # Redirect rtic.rs/meeting/index.html to hackmd - mkdir $td/meeting - sed "s|URL|https://hackmd.io/c_mFUZL-Q2C6614MlrrxOg|g" redirect.html > $td/meeting/index.html - sed -i "s|Page Redirection|RTIC Meeting|" $td/meeting/index.html - sed -i "s|If you|Redirecting to RTIC HackMD. If you|" $td/meeting/index.html - - # Redirect the main site to the stable release - sed "s|URL|$stable|g" redirect.html > $td/index.html - - # Create the redirects for dev-version - # If the current stable and the version being built differ, - # then there is a dev-version and the links should point to it. - if [[ "$stable" != "$crateversion" ]]; - then - sed 's|URL|rtic/index.html|g' redirect.html > $td/$devver/api/index.html - sed 's|URL|book/en|g' redirect.html > $td/$devver/index.html - else - # If the current stable and the "dev" version in master branch - # share the same major version, redirect dev/ to stable book - # This makes sense, preferable to have doc/book updates going live directly to rtic.rs - sed 's|URL|rtic.rs/$stable/api/rtic|g' redirect.html > $td/$devver/api/index.html - sed 's|URL|rtic.rs/$stable|g' redirect.html > $td/$devver/index.html - fi - - # Package older versions, including stable - - # Copy the stable book to the stable alias - cp -r mdbookold/${{ env.STABLE_VERSION }} $td/stable - - # Copy the stable book to the webroot - cp -r mdbookold/${{ env.STABLE_VERSION }} $td/ - # Copy the old stable book to the webroot - cp -r mdbookold/${{ env.OLDSTABLE_VERSION }} $td/ - - # Forward CNAME file - cp CNAME $td/ - mv $td/ bookstodeploy + tar xf current-book.tar.gz + tar xf old-books.tar.gz + CURRENT_VERSION=${{ env.versionmajor }} ./scripts/prep-books.sh "${{ env.STABLE_VERSION }} ${{ env.OLDSTABLE_VERSION }}" - name: Archive the webroot - run: | - tar -cf bookstodeploy.tar bookstodeploy + run: tar -czf book-webroot.tar.gz deploy - name: Store the books uses: actions/upload-artifact@v3 with: - name: bookstodeploy - path: bookstodeploy.tar + name: book-webroot + path: book-webroot.tar.gz ghapages: name: Publish rtic.rs @@ -620,17 +409,16 @@ jobs: - name: Download books uses: actions/download-artifact@v3 with: - name: bookstodeploy + name: book-webroot - name: Extract the books - run: | - tar -xf bookstodeploy.tar + run: tar -xf book-webroot.tar.gz - name: Deploy to GH-pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./bookstodeploy + publish_dir: ./deploy force_orphan: true # ALL THE PREVIOUS JOBS NEEDS TO BE ADDED TO THE `needs` SECTION OF THIS JOB! @@ -644,9 +432,8 @@ jobs: - checkexamples - testexamples - tests - - docs - - mdbook + - current-book runs-on: ubuntu-22.04 steps: - name: Mark the job as a success - run: exit 0 + run: exit 0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6fccc1da5428..de457f1dd641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "rtic-time", "xtask", ] +resolver = "2" [profile.release] codegen-units = 1 diff --git a/rtic-monotonics/src/lib.rs b/rtic-monotonics/src/lib.rs index 82e22a517b32..58fa7a69e1a0 100644 --- a/rtic-monotonics/src/lib.rs +++ b/rtic-monotonics/src/lib.rs @@ -4,13 +4,19 @@ //! To enable the implementations, you must enable a feature for the specific MCU you're targeting. //! //! # Cortex-M Systick -//! The [`systick`] monotonic works on all cortex-M parts, and requires that the feature `cortex-m-systick` is enabled. +//! The +#![cfg_attr(feature = "cortex-m-systick", doc = "[`systick`]")] +#![cfg_attr(not(feature = "cortex-m-systick"), doc = "`systick`")] +//! monotonic works on all cortex-M parts, and requires that the feature `cortex-m-systick` is enabled. //! //! # RP2040 -//! The RP2040 monotonics require that the `rp2040` feature is enabled. +//! The +#![cfg_attr(feature = "rp2040", doc = "[`rp2040`]")] +#![cfg_attr(not(feature = "rp2040"), doc = "`rp2040`")] +//! monotonics require that the `rp2040` feature is enabled. //! //! # nRF -//! nRF monotonics require that one of the available `nrf52*` features is enabled. +//! The nRF monotonics require that one of the available `nrf52*` features is enabled. //! //! All implementations of timers for the nRF52 family are documented here. Monotonics that //! are not available on all parts in this family will have an `Available on crate features X only` diff --git a/scripts/build-old-books.sh b/scripts/build-old-books.sh new file mode 100755 index 000000000000..8cf3de32171e --- /dev/null +++ b/scripts/build-old-books.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -e + +root=$(pwd) + +mkredirect(){ + mkdir -p $(dirname $2) + sed -e "s|URL|$1|g" $root/redirect.html > $2 +} + +clean_build_output=${CLEAN_BUILD_OUTPUT:-1} +vers=($1) +buildroot=${OLD_BOOKS_ROOT:-"book-target/old"} + +webroot=$buildroot/versions +rm -rf $webroot +mkdir -p $webroot + +webroot=$(realpath $webroot) + +srcdir=$buildroot/src + +for ver in ${vers[@]}; do + echo "Building book v${ver}" + mkdir -p $srcdir/$ver + src=$srcdir/$ver + curl -fL https://github.com/rtic-rs/rtic/archive/release/v${ver}.tar.gz | tar xz --strip-components 1 -C $src + + pushd $src + + # Build the docs: there are a few combinations we have to try to cover all of + # the versions + cargo doc || cargo doc --features thumbv7-backend + + mkdir -p $webroot/$ver/api + cp -r $(realpath target/doc) $webroot/$ver/api + + mkredirect "rtic/index.html" $webroot/$ver/api/index.html + + # Build and copy all of the languages + langs=( book/* ) + for lang in ${langs[@]}; do + lang=$(basename $lang) + lang_root=$webroot/$ver/book/$lang + mkdir -p $lang_root + pushd book/$lang + echo $(pwd) + mdbook build -d $lang_root + popd + cp LICENSE-* $lang_root + done + + mkredirect "book/en" $webroot/$ver/index.html + + popd + + rm -rf $buildroot/$ver +done + +# Move all versions into the build root for easier access +cp -r $webroot/* $buildroot + +if [ $clean_build_output -eq 1 ]; then + rm -rf $srcdir + rm -rf $webroot +fi diff --git a/scripts/parse-version.sh b/scripts/parse-version.sh new file mode 100755 index 000000000000..024dd6b7207e --- /dev/null +++ b/scripts/parse-version.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +version=$(cargo metadata --format-version 1 --no-deps --offline | jq -r '.packages[] | select(.name == "rtic") | .version') + +echo $version \ No newline at end of file diff --git a/scripts/prep-books.sh b/scripts/prep-books.sh new file mode 100755 index 000000000000..7acf33ed56f2 --- /dev/null +++ b/scripts/prep-books.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +set -e + +mkredirect() { + mkdir -p $(dirname $2) + sed -e "s|URL|$1|g" redirect.html > $2 +} + +langs=( en ) +devver=( dev ) +vers=( $1 ) +webroot=${WEB_ROOT:-"book-target/deploy"} +oldbooks=${OLD_BOOKS_ROOT:-"book-target/old"} +current_book=${CURRENT_BOOK_ROOT:-"book-target/current"} + +stable="${vers[0]}" +oldstable="${vers[1]}" + +if [ -z "$CURRENT_VERSION" ]; then + CURRENT_VERSION=$(./scripts/parse-version.sh) +fi + +crate_version="$CURRENT_VERSION" + +echo "Latest stable version: $stable" +echo "Current crate version: $crate_version" + +# Create directories +rm -rf $webroot +mkdir -p $webroot/$devver + +# Copy the current dev version +echo "Copy current dev version" +cp -r $current_book/* $webroot/$devver + +echo "Inserting redirects" +# Replace relevant links to make rtic.rs/meeting/index.html +# redirect to the meeting and make the text a bit more descriptive +mkredirect "https://hackmd.io/c_mFUZL-Q2C6614MlrrxOg" $webroot/meeting/index.html +sed -e "s|Page Redirection|RTIC Meeting|g" \ + -e "s|If you|Redirecting to RTIC HackMD. If you|g" \ + -i $webroot/meeting/index.html + +# Redirect the main site to the stable release +mkredirect "$stable" $webroot/index.html + +# Create redirects for the dev version +if [ "$stable" != "$crate_version" ]; then + # Current stable version being built differ + # so we want to display the current dev version + echo "Redirecting dev version dev version files" + mkredirect "rtic/index.html" $webroot/$devver/api/index.html + mkredirect "book/en" $webroot/$devver/index.html +else + # The stable and crate version are the same + # so we redirec to the stable version instead + echo "Redirecting dev version to stable" + mkredirect "https://rtic.rs/$stable/api/rtic" $webroot/$devver/api/index.html + mkredirect "https://rtic.rs/$stable" $webroot/$devver/index.html +fi + +# Pack up all of the older versions, including stable + +echo "Copying stable" + +# Copy the stable book to the stable alias +cp -r $oldbooks/$stable $webroot/stable + +echo "Copying older versions" + +# Copy the stable book to the webroot +cp -r $oldbooks/$stable $webroot/ +# Copy the old stable book to the webroot +cp -r $oldbooks/$oldstable $webroot/ + +# Forward CNAME file +cp CNAME $webroot diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 652fdedadc8c..1e12a01bd190 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -11,3 +11,4 @@ pretty_env_logger = "0.5.0" log = "0.4.17" rayon = { version = "1.6.1", optional = true } diffy = "0.3.0" +fs_extra = "1.3" \ No newline at end of file diff --git a/xtask/src/argument_parsing.rs b/xtask/src/argument_parsing.rs index 2e1eef4882af..a3b1063eaa31 100644 --- a/xtask/src/argument_parsing.rs +++ b/xtask/src/argument_parsing.rs @@ -1,6 +1,7 @@ use crate::{cargo_command::CargoCommand, Target, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN}; use clap::{Args, Parser, Subcommand}; use core::fmt; +use log::{error, trace}; #[derive(clap::ValueEnum, Copy, Clone, Debug)] pub enum Package { @@ -204,25 +205,7 @@ pub struct Globals { /// For which backend to build. #[arg(value_enum, short, default_value = "thumbv7", long, global = true)] - pub backend: Option, - - /// List of comma separated examples to include, all others are excluded - /// - /// If omitted all examples are included - /// - /// Example: `cargo xtask --example complex,spawn,init` - /// would include complex, spawn and init - #[arg(short, long, group = "example_group", global = true)] - pub example: Option, - - /// List of comma separated examples to exclude, all others are included - /// - /// If omitted all examples are included - /// - /// Example: `cargo xtask --excludeexample complex,spawn,init` - /// would exclude complex, spawn and init - #[arg(long, group = "example_group", global = true)] - pub exampleexclude: Option, + backend: Option, /// Enable more verbose output, repeat up to `-vvv` for even more #[arg(short, long, action = clap::ArgAction::Count, global = true)] @@ -235,11 +218,12 @@ pub struct Globals { /// clutter, but can make debugging long-running processes a lot easier. #[arg(short, long, global = true)] pub stderr_inherited: bool, +} - /// Don't build/check/test all feature combinations that are available, only - /// a necessary subset. - #[arg(long, global = true)] - pub partial: bool, +impl Globals { + pub fn backend(&self) -> Backends { + self.backend.unwrap_or_default() + } } #[derive(Parser)] @@ -270,10 +254,10 @@ pub enum Commands { Build(PackageOpt), /// Check all examples - ExampleCheck, + ExampleCheck(ExampleArgs), /// Build all examples - ExampleBuild, + ExampleBuild(ExampleArgs), /// Run `cargo size` on selected or all examples /// @@ -281,7 +265,7 @@ pub enum Commands { /// arguments will be passed on /// /// Example: `cargo xtask size -- -A` - Size(Arg), + Size(ExampleArgs), /// Run examples in QEMU and compare against expected output /// @@ -299,19 +283,11 @@ pub enum Commands { /// Requires that an ARM target is selected Run(QemuAndRun), - /// Build docs - /// - /// To pass options to `cargo doc`, add `--` and then the following - /// arguments will be passed on - /// - /// Example: `cargo xtask doc -- --open` - Doc(Arg), - /// Run tests Test(PackageOpt), /// Build books with mdbook - Book(Arg), + Book(BookArgs), /// Check one or more usage examples. /// @@ -325,6 +301,116 @@ pub enum Commands { UsageExampleBuild(UsageExamplesOpt), } +#[derive(Args, Clone, Debug)] +pub struct ExampleArgs { + /// List of comma separated examples to include, all others are excluded + /// + /// If omitted all examples are included + /// + /// Example: `cargo xtask --example complex,spawn,init` + /// would include complex, spawn and init + #[arg(short, long, group = "example_group", global = true)] + example: Option, + + /// List of comma separated examples to exclude, all others are included + /// + /// If omitted all examples are included + /// + /// Example: `cargo xtask --excludeexample complex,spawn,init` + /// would exclude complex, spawn and init + #[arg(long, group = "example_group", global = true)] + exclude: Option, + + /// Additional arguments to pass to the invoked tool + #[command(subcommand)] + pub arguments: Option, +} + +impl ExampleArgs { + pub fn example_list(&self) -> anyhow::Result> { + let examples: Vec<_> = std::fs::read_dir("./rtic/examples")? + .filter_map(|p| p.ok()) + .map(|p| p.path()) + .filter(|p| p.display().to_string().ends_with(".rs")) + .map(|path| path.file_stem().unwrap().to_str().unwrap().to_string()) + .collect(); + + let mut to_run = examples.clone(); + + if let Some(example) = &self.example { + to_run = examples.clone(); + let examples_to_exclude = example.split(',').collect::>(); + // From the list of all examples, remove all not listed as included + for ex in examples_to_exclude { + to_run.retain(|x| *x.as_str() == *ex); + } + }; + + if let Some(example) = &self.exclude { + to_run = examples.clone(); + let examples_to_exclude = example.split(',').collect::>(); + // From the list of all examples, remove all those listed as excluded + for ex in examples_to_exclude { + to_run.retain(|x| *x.as_str() != *ex); + } + }; + + trace!("All examples (N: {}): {examples:?}", examples.len()); + trace!("Examples to run (N: {}): {to_run:?}", to_run.len()); + + if to_run.is_empty() { + error!("The example(s) you specified cannot be found. Available examples are:"); + error!("{examples:#?}"); + error!(""); + error!("By default, all examples are tested"); + Err(anyhow::anyhow!("Incorrect usage")) + } else { + Ok(to_run) + } + } +} + +#[derive(Args, Clone, Debug)] +pub struct BookArgs { + /// If this flag is set, the links in the book are + /// not verified + #[clap(long, short = 'l')] + pub skip_link_check: bool, + + /// If this flag is set, the links in the API documentation + /// included with the book are not verified. + /// + /// This flag is ignored if `--api-docs` is set + #[clap(long, short = 'c')] + pub skip_api_link_check: bool, + + /// The path to the API docs if you do not wish + /// to build them again. + #[clap(long, short = 'a')] + pub api_docs: Option, + + /// The path to which the contents of the book should + /// be written. + #[clap(long, short, default_value = "book-target/current")] + pub output_path: String, + + /// Additional arguments to pass to `mdbook` + #[command(subcommand)] + pub arguments: Option, +} + +#[derive(Args, Clone, Debug)] +pub struct DocArgs { + /// If this flag is set, the links in the API + /// docs are not verified. + #[clap(long, short = 'l')] + pub skip_link_check: bool, + + /// Additional arguments to pass to `cargo doc` + #[command(subcommand)] + pub arguments: Option, +} + #[derive(Args, Clone, Debug)] pub struct UsageExamplesOpt { /// The usage examples to build. All usage examples are selected if this argument is not provided. @@ -333,6 +419,16 @@ pub struct UsageExamplesOpt { examples: Option, } +#[derive(Args, Clone, Debug)] +pub struct BookOpt { + /// The directory in which the book is located + #[clap(default_value = "./book/en", short, long)] + pub directory: String, + /// The directory to put the final book in. + #[clap(default_value = "./book-target", short, long)] + pub output_dir: String, +} + impl UsageExamplesOpt { pub fn examples(&self) -> anyhow::Result> { let usage_examples: Vec<_> = std::fs::read_dir("./examples")? @@ -375,6 +471,11 @@ pub struct FormatOpt { #[derive(Args, Debug, Clone)] /// Restrict to package, or run on whole workspace pub struct PackageOpt { + /// Don't build/check/test all feature combinations that are available, only + /// a necessary subset. + #[arg(long)] + pub partial: bool, + /// For which package/workspace member to operate /// /// If omitted, work on all @@ -402,6 +503,9 @@ impl PackageOpt { #[derive(Args, Debug, Clone)] pub struct QemuAndRun { + #[clap(flatten)] + pub examples: ExampleArgs, + /// If expected output is missing or mismatching, recreate the file /// /// This overwrites only missing or mismatching @@ -411,7 +515,7 @@ pub struct QemuAndRun { #[derive(Debug, Parser, Clone)] pub struct Arg { - /// Options to pass to `cargo size` + /// Additional arguments to pass to called binaries. #[command(subcommand)] pub arguments: Option, } diff --git a/xtask/src/cargo_command.rs b/xtask/src/cargo_command.rs index 1d5f3c57308e..6c2f4b62bc0b 100644 --- a/xtask/src/cargo_command.rs +++ b/xtask/src/cargo_command.rs @@ -15,7 +15,7 @@ pub enum CargoCommand<'a> { #[allow(dead_code)] Run { cargoarg: &'a Option<&'a str>, - example: &'a str, + example: String, target: Option>, features: Option, mode: BuildMode, @@ -23,7 +23,7 @@ pub enum CargoCommand<'a> { }, Qemu { cargoarg: &'a Option<&'a str>, - example: &'a str, + example: String, target: Option>, features: Option, mode: BuildMode, @@ -32,7 +32,7 @@ pub enum CargoCommand<'a> { }, ExampleBuild { cargoarg: &'a Option<&'a str>, - example: &'a str, + example: String, target: Option>, features: Option, mode: BuildMode, @@ -41,7 +41,7 @@ pub enum CargoCommand<'a> { }, ExampleCheck { cargoarg: &'a Option<&'a str>, - example: &'a str, + example: String, target: Option>, features: Option, mode: BuildMode, @@ -90,11 +90,12 @@ pub enum CargoCommand<'a> { deny_warnings: bool, }, Book { + output_path: Option, arguments: Option, }, ExampleSize { cargoarg: &'a Option<&'a str>, - example: &'a str, + example: String, target: Option>, features: Option, mode: BuildMode, @@ -102,6 +103,9 @@ pub enum CargoCommand<'a> { dir: Option, deny_warnings: bool, }, + Lychee { + path: PathBuf, + }, } impl core::fmt::Display for CargoCommand<'_> { @@ -337,7 +341,19 @@ impl core::fmt::Display for CargoCommand<'_> { let feat = feat(features); write!(f, "Run {test} in {p} ({deny_warnings}features: {feat})") } - CargoCommand::Book { arguments: _ } => write!(f, "Build the book"), + CargoCommand::Book { + arguments: _, + output_path, + } => { + write!( + f, + "Build the book (output: {})", + output_path + .as_ref() + .map(|p| format!("{}", p.display())) + .unwrap_or("not specified".to_string()) + ) + } CargoCommand::ExampleSize { cargoarg, example, @@ -352,6 +368,9 @@ impl core::fmt::Display for CargoCommand<'_> { let details = details(warns, target, Some(mode), features, cargoarg, dir.as_ref()); write!(f, "Compute size of example {example} {details}") } + CargoCommand::Lychee { path } => { + write!(f, "Check links in {}", path.display()) + } } } } @@ -375,8 +394,8 @@ impl<'a> CargoCommand<'a> { format!("{env}{cd}{executable} {args}") } - fn command(&self) -> &'static str { - match self { + fn command(&self) -> Option<&'static str> { + let command = match self { CargoCommand::Run { .. } | CargoCommand::Qemu { .. } => "run", CargoCommand::ExampleCheck { .. } | CargoCommand::Check { .. } => "check", CargoCommand::ExampleBuild { .. } | CargoCommand::Build { .. } => "build", @@ -386,7 +405,9 @@ impl<'a> CargoCommand<'a> { CargoCommand::Doc { .. } => "doc", CargoCommand::Book { .. } => "build", CargoCommand::Test { .. } => "test", - } + CargoCommand::Lychee { .. } => return None, + }; + Some(command) } pub fn executable(&self) -> &'static str { match self { @@ -402,6 +423,7 @@ impl<'a> CargoCommand<'a> { | CargoCommand::Test { .. } | CargoCommand::Doc { .. } => "cargo", CargoCommand::Book { .. } => "mdbook", + CargoCommand::Lychee { .. } => "lychee", } } @@ -410,11 +432,12 @@ impl<'a> CargoCommand<'a> { fn build_args<'i, T: Iterator>( &'i self, nightly: bool, + color: bool, cargoarg: &'i Option<&'i str>, features: &'i Option, mode: Option<&'i BuildMode>, extra: T, - ) -> Vec<&str> { + ) -> Vec { let mut args: Vec<&str> = Vec::new(); if nightly { @@ -425,7 +448,13 @@ impl<'a> CargoCommand<'a> { args.push(cargoarg); } - args.push(self.command()); + if let Some(subcommand) = self.command() { + args.push(subcommand); + } + + if color { + args.extend_from_slice(&["--color", "always"]) + } if let Some(target) = self.target() { args.extend_from_slice(&["--target", target.triple()]) @@ -441,7 +470,7 @@ impl<'a> CargoCommand<'a> { args.extend(extra); - args + args.into_iter().map(String::from).collect() } /// Turn the ExtraArguments into an interator that contains the separating dashes @@ -462,7 +491,7 @@ impl<'a> CargoCommand<'a> { args.into_iter() } - pub fn args(&self) -> Vec<&str> { + pub fn args(&self) -> Vec { fn p(package: &Option) -> impl Iterator { if let Some(package) = package { vec!["--package", &package].into_iter() @@ -483,6 +512,7 @@ impl<'a> CargoCommand<'a> { // Target is added by build_args target: _, } => self.build_args( + true, true, cargoarg, features, @@ -502,6 +532,7 @@ impl<'a> CargoCommand<'a> { deny_warnings: _, } => self.build_args( true, + false, cargoarg, features, Some(mode), @@ -518,7 +549,7 @@ impl<'a> CargoCommand<'a> { dir: _, // deny_warnings is exposed through `extra_env` deny_warnings: _, - } => self.build_args(true, cargoarg, features, Some(mode), p(package)), + } => self.build_args(true, true, cargoarg, features, Some(mode), p(package)), CargoCommand::Check { cargoarg, package, @@ -530,7 +561,7 @@ impl<'a> CargoCommand<'a> { target: _, // deny_warnings is exposed through `extra_env` deny_warnings: _, - } => self.build_args(true, cargoarg, features, Some(mode), p(package)), + } => self.build_args(true, true, cargoarg, features, Some(mode), p(package)), CargoCommand::Clippy { cargoarg, package, @@ -546,7 +577,7 @@ impl<'a> CargoCommand<'a> { }; let extra = p(package).chain(deny_warnings); - self.build_args(true, cargoarg, features, None, extra) + self.build_args(true, true, cargoarg, features, None, extra) } CargoCommand::Doc { cargoarg, @@ -556,7 +587,7 @@ impl<'a> CargoCommand<'a> { deny_warnings: _, } => { let extra = Self::extra_args(arguments.as_ref()); - self.build_args(true, cargoarg, features, None, extra) + self.build_args(true, true, cargoarg, features, None, extra) } CargoCommand::Test { package, @@ -572,20 +603,28 @@ impl<'a> CargoCommand<'a> { }; let package = p(package); let extra = extra.into_iter().chain(package); - self.build_args(true, &None, features, None, extra) + self.build_args(true, true, &None, features, None, extra) } - CargoCommand::Book { arguments } => { + CargoCommand::Book { + arguments, + output_path, + } => { let mut args = vec![]; + // If there are extra arguments, just proxy if let Some(ExtraArguments::Other(arguments)) = arguments { for arg in arguments { - args.extend_from_slice(&[arg.as_str()]); + args.extend_from_slice(&[arg.to_string()]); } } else { // If no argument given, run mdbook build // with default path to book - args.extend_from_slice(&[self.command()]); - args.extend_from_slice(&["book/en"]); + args.extend(["build", "book/en"].into_iter().map(String::from)); + + if let Some(output_path) = output_path { + args.push("--dest-dir".to_string()); + args.push(format!("{}", output_path.display())) + } } args } @@ -598,6 +637,7 @@ impl<'a> CargoCommand<'a> { let package = p(package); self.build_args( true, + false, cargoarg, &None, None, @@ -616,6 +656,7 @@ impl<'a> CargoCommand<'a> { // deny_warnings is exposed through `extra_env` deny_warnings: _, } => self.build_args( + true, true, cargoarg, features, @@ -632,6 +673,7 @@ impl<'a> CargoCommand<'a> { // deny_warnings is exposed through `extra_env` deny_warnings: _, } => self.build_args( + true, true, cargoarg, features, @@ -655,8 +697,17 @@ impl<'a> CargoCommand<'a> { .into_iter() .chain(Self::extra_args(arguments.as_ref())); - self.build_args(true, cargoarg, features, Some(mode), extra) + self.build_args(true, false, cargoarg, features, Some(mode), extra) } + CargoCommand::Lychee { path } => [ + "--offline", + "--format", + "detailed", + &format!("{}", path.display()), + ] + .into_iter() + .map(String::from) + .collect(), } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 30c3da050382..b1ea81f766c7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -8,10 +8,10 @@ use clap::Parser; use core::fmt; use std::{path::Path, str}; -use log::{error, info, log_enabled, trace, Level}; +use log::{log_enabled, trace, Level}; use crate::{ - argument_parsing::{Backends, BuildOrCheck, Cli, Commands}, + argument_parsing::{BuildOrCheck, Cli, Commands}, build::init_build_dir, run::*, }; @@ -65,13 +65,6 @@ fn main() -> anyhow::Result<()> { )); } - let examples: Vec<_> = std::fs::read_dir("./rtic/examples")? - .filter_map(|p| p.ok()) - .map(|p| p.path()) - .filter(|p| p.display().to_string().ends_with(".rs")) - .map(|path| path.file_stem().unwrap().to_str().unwrap().to_string()) - .collect(); - let cli = Cli::parse(); let globals = &cli.globals; @@ -92,57 +85,6 @@ fn main() -> anyhow::Result<()> { "Stderr of child processes is inherited: {}", globals.stderr_inherited ); - log::debug!("Partial features: {}", globals.partial); - - let backend = if let Some(backend) = globals.backend { - backend - } else { - Backends::default() - }; - - let example = globals.example.clone(); - let exampleexclude = globals.exampleexclude.clone(); - - let examples_to_run = { - let mut examples_to_run = examples.clone(); - - if let Some(example) = example { - examples_to_run = examples.clone(); - let examples_to_exclude = example.split(',').collect::>(); - // From the list of all examples, remove all not listed as included - for ex in examples_to_exclude { - examples_to_run.retain(|x| *x.as_str() == *ex); - } - }; - - if let Some(example) = exampleexclude { - examples_to_run = examples.clone(); - let examples_to_exclude = example.split(',').collect::>(); - // From the list of all examples, remove all those listed as excluded - for ex in examples_to_exclude { - examples_to_run.retain(|x| *x.as_str() != *ex); - } - }; - - if log_enabled!(Level::Trace) { - trace!("All examples:\n{examples:?} number: {}", examples.len()); - trace!( - "examples_to_run:\n{examples_to_run:?} number: {}", - examples_to_run.len() - ); - } - - if examples_to_run.is_empty() { - error!( - "\nThe example(s) you specified is not available. Available examples are:\ - \n{examples:#?}\n\ - By default if example flag is emitted, all examples are tested.", - ); - return Err(anyhow::anyhow!("Incorrect usage")); - } else { - examples_to_run - } - }; init_build_dir()?; #[allow(clippy::if_same_then_else)] @@ -161,78 +103,51 @@ fn main() -> anyhow::Result<()> { let final_run_results = match &cli.command { Commands::Format(args) => cargo_format(globals, &cargologlevel, &args.package, args.check), - Commands::Clippy(args) => { - info!("Running clippy on backend: {backend:?}"); - cargo_clippy(globals, &cargologlevel, &args, backend) - } - Commands::Check(args) => { - info!("Checking on backend: {backend:?}"); - cargo(globals, BuildOrCheck::Check, &cargologlevel, &args, backend) - } - Commands::Build(args) => { - info!("Building for backend: {backend:?}"); - cargo(globals, BuildOrCheck::Build, &cargologlevel, &args, backend) + Commands::Clippy(args) => cargo_clippy(globals, &cargologlevel, &args, args.partial), + Commands::Check(args) => cargo( + globals, + BuildOrCheck::Check, + &cargologlevel, + &args, + args.partial, + ), + Commands::Build(args) => cargo( + globals, + BuildOrCheck::Build, + &cargologlevel, + &args, + args.partial, + ), + Commands::ExampleCheck(ex) => { + let ex = ex.example_list()?; + cargo_example(globals, BuildOrCheck::Check, &cargologlevel, ex) } - Commands::ExampleCheck => { - info!("Checking on backend: {backend:?}"); - cargo_example( - globals, - BuildOrCheck::Check, - &cargologlevel, - backend, - &examples_to_run, - ) - } - Commands::ExampleBuild => { - info!("Building for backend: {backend:?}"); - cargo_example( - globals, - BuildOrCheck::Build, - &cargologlevel, - backend, - &examples_to_run, - ) + Commands::ExampleBuild(ex) => { + let ex = ex.example_list()?; + cargo_example(globals, BuildOrCheck::Build, &cargologlevel, ex) } Commands::Size(args) => { // x86_64 target not valid - info!("Measuring for backend: {backend:?}"); - build_and_check_size( - globals, - &cargologlevel, - backend, - &examples_to_run, - &args.arguments, - ) + let ex = args.example_list()?; + build_and_check_size(globals, &cargologlevel, ex, &args.arguments) } Commands::Qemu(args) | Commands::Run(args) => { // x86_64 target not valid - info!("Testing for backend: {backend:?}"); - qemu_run_examples( - globals, - &cargologlevel, - backend, - &examples_to_run, - args.overwrite_expected, - ) - } - Commands::Doc(args) => { - info!("Running cargo doc on backend: {backend:?}"); - cargo_doc(globals, &cargologlevel, backend, &args.arguments) - } - Commands::Test(args) => { - info!("Running cargo test on backend: {backend:?}"); - cargo_test(globals, &args, backend) + let ex = args.examples.example_list()?; + qemu_run_examples(globals, &cargologlevel, ex, args.overwrite_expected) } + Commands::Test(args) => cargo_test(globals, &args), Commands::Book(args) => { - info!("Running mdbook"); - cargo_book(globals, &args.arguments) + let links = !args.skip_link_check; + let api_links = !args.skip_api_link_check; + let output = std::path::PathBuf::from(&args.output_path); + let api = args.api_docs.clone().map(std::path::PathBuf::from); + cargo_book(globals, links, api_links, output, api, &args.arguments) } Commands::UsageExampleCheck(examples) => { - info!("Checking usage examples"); cargo_usage_example(globals, BuildOrCheck::Check, examples.examples()?) } Commands::UsageExampleBuild(examples) => { - info!("Building usage examples"); cargo_usage_example(globals, BuildOrCheck::Build, examples.examples()?) } }; diff --git a/xtask/src/run.rs b/xtask/src/run.rs index 605755116dbd..d14b26fc249f 100644 --- a/xtask/src/run.rs +++ b/xtask/src/run.rs @@ -15,11 +15,11 @@ mod iter; use iter::{into_iter, CoalescingRunner}; use crate::{ - argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, + argument_parsing::{BuildOrCheck, ExtraArguments, Globals, Package, PackageOpt, TestMetadata}, cargo_command::{BuildMode, CargoCommand}, }; -use log::{error, info}; +use log::{debug, error, info}; #[cfg(feature = "rayon")] use rayon::prelude::*; @@ -28,7 +28,7 @@ fn run_and_convert<'a>( (global, command, overwrite): (&Globals, CargoCommand<'a>, bool), ) -> FinalRunResult<'a> { // Run the command - let result = command_parser(global, &command, overwrite); + let result = interpret_command(global, &command, overwrite); let output = match result { // If running the command succeeded without looking at any of the results, @@ -50,7 +50,7 @@ fn run_and_convert<'a>( } // run example binary `example` -fn command_parser( +fn interpret_command( glob: &Globals, command: &CargoCommand, overwrite: bool, @@ -61,7 +61,7 @@ fn command_parser( OutputMode::PipedAndCollected }; - match *command { + match command { CargoCommand::Qemu { example, .. } | CargoCommand::Run { example, .. } => { /// Check if `run` was successful. /// returns Ok in case the run went as expected, @@ -140,7 +140,8 @@ fn command_parser( | CargoCommand::Doc { .. } | CargoCommand::Test { .. } | CargoCommand::Book { .. } - | CargoCommand::ExampleSize { .. } => { + | CargoCommand::ExampleSize { .. } + | CargoCommand::Lychee { .. } => { let cargo_result = run_command(command, output_mode, true)?; Ok(cargo_result) } @@ -153,13 +154,24 @@ pub fn cargo<'c>( operation: BuildOrCheck, cargoarg: &'c Option<&'c str>, package: &'c PackageOpt, - backend: Backends, + partial: bool, ) -> Vec> { + let backend = globals.backend(); + + match operation { + BuildOrCheck::Check => { + info!("Checking on backend: {backend:?}") + } + BuildOrCheck::Build => { + info!("Building for backend: {backend:?}") + } + } + let runner = package .packages() .flat_map(|package| { let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); + let features = package.features(target, backend, partial); into_iter(features).map(move |f| (package, target, f)) }) .map(move |(package, target, features)| { @@ -199,6 +211,10 @@ pub fn cargo_usage_example( operation: BuildOrCheck, usage_examples: Vec, ) -> Vec> { + match operation { + BuildOrCheck::Check => info!("Checking usage examples"), + BuildOrCheck::Build => info!("Building usage examples"), + } into_iter(&usage_examples) .map(|example| { let path = format!("examples/{example}"); @@ -235,9 +251,19 @@ pub fn cargo_example<'c>( globals: &Globals, operation: BuildOrCheck, cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], + examples: Vec, ) -> Vec> { + let backend = globals.backend(); + + match operation { + BuildOrCheck::Check => { + info!("Checking examples for backend {backend:?}"); + } + BuildOrCheck::Build => { + info!("Building for examples for backend {backend:?}"); + } + } + let runner = into_iter(examples).map(|example| { let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); @@ -270,13 +296,16 @@ pub fn cargo_clippy<'c>( globals: &Globals, cargoarg: &'c Option<&'c str>, package: &'c PackageOpt, - backend: Backends, + partial: bool, ) -> Vec> { + let backend = globals.backend(); + info!("Running clippy on backend: {backend:?}"); + let runner = package .packages() .flat_map(|package| { let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); + let features = package.features(target, backend, partial); into_iter(features).map(move |f| (package, target, f)) }) .map(move |(package, target, features)| { @@ -315,33 +344,14 @@ pub fn cargo_format<'c>( runner.run_and_coalesce() } -/// Run cargo doc -pub fn cargo_doc<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - arguments: &'c Option, -) -> Vec> { - let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - - let command = CargoCommand::Doc { - cargoarg, - features, - arguments: arguments.clone(), - deny_warnings: true, - }; - - vec![run_and_convert((globals, command, false))] -} - /// Run cargo test on the selected package or all packages /// /// If no package is specified, loop through all packages -pub fn cargo_test<'c>( - globals: &Globals, - package: &'c PackageOpt, - backend: Backends, -) -> Vec> { +pub fn cargo_test<'c>(globals: &Globals, package: &'c PackageOpt) -> Vec> { + let backend = globals.backend(); + + info!("Running cargo test on backend: {backend:?}"); + package .packages() .map(|p| { @@ -351,30 +361,19 @@ pub fn cargo_test<'c>( .run_and_coalesce() } -/// Use mdbook to build the book -pub fn cargo_book<'c>( - globals: &Globals, - arguments: &'c Option, -) -> Vec> { - vec![run_and_convert(( - globals, - CargoCommand::Book { - arguments: arguments.clone(), - }, - false, - ))] -} - /// Run examples /// /// Supports updating the expected output via the overwrite argument pub fn qemu_run_examples<'c>( globals: &Globals, cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], + examples: Vec, overwrite: bool, ) -> Vec> { + let backend = globals.backend(); + + info!("Running QEMU examples for backend: {backend:?}"); + let target = backend.to_target(); let features = Some(target.and_features(backend.to_rtic_feature())); @@ -385,7 +384,7 @@ pub fn qemu_run_examples<'c>( let cmd_build = CargoCommand::ExampleBuild { cargoarg: &None, - example, + example: example.clone(), target, features: features.clone(), mode: BuildMode::Release, @@ -413,10 +412,12 @@ pub fn qemu_run_examples<'c>( pub fn build_and_check_size<'c>( globals: &Globals, cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], + examples: Vec, arguments: &'c Option, ) -> Vec> { + let backend = globals.backend(); + info!("Measuring size for backend {backend:?}"); + let target = backend.to_target(); let features = Some(target.and_features(backend.to_rtic_feature())); @@ -427,7 +428,7 @@ pub fn build_and_check_size<'c>( // Make sure the requested example(s) are built let cmd_build = CargoCommand::ExampleBuild { cargoarg: &Some("--quiet"), - example, + example: example.clone(), target, features: features.clone(), mode: BuildMode::Release, @@ -465,7 +466,8 @@ fn run_command( process .args(command.args()) .stdout(Stdio::piped()) - .stderr(stderr_mode); + .stderr(stderr_mode) + .env_remove("RUST_LOG"); if let Some(dir) = command.chdir() { process.current_dir(dir.canonicalize()?); @@ -499,3 +501,168 @@ fn run_command( stderr, }) } + +fn check_all_api_links(globals: &Globals) -> Vec { + info!("Checking all API links"); + + #[cfg(feature = "rayon")] + let iter = Package::all().into_par_iter(); + + #[cfg(not(feature = "rayon"))] + let iter = Package::all().into_iter(); + + let runner = iter.map(|p| { + let name = p.name().to_string().replace('-', "_"); + let segments = ["target", "doc", name.as_str()]; + let path = PathBuf::from_iter(segments); + (globals, CargoCommand::Lychee { path }, true) + }); + + runner.run_and_coalesce() +} + +/// Use mdbook to build the book +pub fn cargo_book<'c>( + globals: &'c Globals, + check_book_links: bool, + check_api_links: bool, + output_dir: PathBuf, + api: Option, + arguments: &'c Option, +) -> Vec> { + if let Some(args) = arguments { + return vec![run_and_convert(( + &globals, + CargoCommand::Book { + output_path: None, + arguments: Some(args.clone()), + }, + true, + ))]; + } + + info!("Documenting all crates"); + let mut final_results = Vec::new(); + + let api_path = if let Some(api) = api { + if let Err(e) = std::fs::metadata(&api) { + return vec![FinalRunResult::OtherError(anyhow::anyhow!( + "Could not find API path: {e}" + ))]; + } + api + } else { + let features = globals.backend().to_rtic_feature().to_string(); + let features = format!("rp2040,cortex-m-systick,nrf52840,{features}"); + + let doc_command = CargoCommand::Doc { + cargoarg: &None, + features: Some(features), + arguments: None, + deny_warnings: true, + }; + + final_results.push(run_and_convert((globals, doc_command, true))); + if final_results.iter().any(|r| !r.is_success()) { + return final_results; + } + + if check_api_links { + let mut links = check_all_api_links(globals); + final_results.append(&mut links); + if final_results.iter().any(|r| !r.is_success()) { + return final_results; + } + } + + PathBuf::from_iter(["target", "doc"].into_iter()) + }; + + let construct_book = || -> anyhow::Result> { + use fs_extra::dir::CopyOptions; + + // ./book-target/ + let book_target = PathBuf::from(output_dir); + + if std::fs::metadata(&book_target) + .map(|m| !m.is_dir()) + .unwrap_or(false) + { + return Err(anyhow::anyhow!( + "Book target ({}) exists but is not a directory.", + book_target.display() + )); + } + + std::fs::remove_dir_all(&book_target).ok(); + + // ./book-target/book + let mut book_target_book = book_target.clone(); + book_target_book.push("book"); + + // ./book-target/book/en + let mut book_target_en = book_target_book.clone(); + book_target_en.push("en"); + std::fs::create_dir_all(&book_target_en)?; + let book_target_en = book_target_en.canonicalize()?; + + // ./book-target/api + let mut book_target_api = book_target.clone(); + book_target_api.push("api"); + + info!("Running mdbook"); + + let book = run_and_convert(( + globals, + CargoCommand::Book { + arguments: arguments.clone(), + output_path: Some(book_target_en.clone()), + }, + false, + )); + + if !book.is_success() { + return Ok(vec![book]); + } + + std::fs::create_dir_all(&book_target_book)?; + + debug!("Copying licenses"); + fs_extra::copy_items( + &["./LICENSE-APACHE", "./LICENSE-CC-BY-SA", "./LICENSE-MIT"], + &book_target_en, + &Default::default(), + )?; + + info!( + "Copying API docs from {} to {}", + api_path.display(), + book_target_api.display() + ); + fs_extra::copy_items( + &[api_path], + book_target_api, + &CopyOptions::default().overwrite(true).copy_inside(true), + )?; + + if check_book_links { + info!("Checking links in the book"); + + let last_command = CargoCommand::Lychee { + path: book_target_en, + }; + + Ok(vec![book, run_and_convert((globals, last_command, true))]) + } else { + Ok(vec![book]) + } + }; + + let mut construction_result = match construct_book() { + Ok(res) => res, + Err(other) => vec![FinalRunResult::OtherError(other)], + }; + + final_results.append(&mut construction_result); + final_results +} diff --git a/xtask/src/run/data.rs b/xtask/src/run/data.rs index eacd72cbad96..23219e74e6f7 100644 --- a/xtask/src/run/data.rs +++ b/xtask/src/run/data.rs @@ -34,6 +34,13 @@ pub enum FinalRunResult<'c> { Success(CargoCommand<'c>, RunResult), Failed(CargoCommand<'c>, RunResult), CommandError(CargoCommand<'c>, anyhow::Error), + OtherError(anyhow::Error), +} + +impl FinalRunResult<'_> { + pub fn is_success(&self) -> bool { + matches!(self, Self::Success(_, _)) + } } #[derive(Debug)] diff --git a/xtask/src/run/results.rs b/xtask/src/run/results.rs index b64e7b18fd2b..887ee383d819 100644 --- a/xtask/src/run/results.rs +++ b/xtask/src/run/results.rs @@ -31,6 +31,14 @@ pub fn handle_results(globals: &Globals, results: Vec) -> Result } }); + let external_errors = results.iter().filter_map(|r| { + if let FinalRunResult::OtherError(e) = r { + Some(e) + } else { + None + } + }); + let log_stdout_stderr = |level: Level| { move |(cmd, stdout, stderr): (&CargoCommand, &String, &String)| { let cmd = cmd.as_cmd_string(); @@ -89,7 +97,11 @@ pub fn handle_results(globals: &Globals, results: Vec) -> Result ) }); - let ecount = errors.count() + command_errors.count(); + external_errors + .clone() + .for_each(|e| error!(target: TARGET, "❌ Failed: {e}")); + + let ecount = errors.count() + command_errors.count() + external_errors.count(); if ecount != 0 { error!(target: TARGET, "{ecount} commands failed."); Err(())