diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 440fc68a..4e902ddf 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -16,7 +16,7 @@ serde = { workspace = true } serde_json = { workspace = true } toml = { version = "0.8.2", default-features = false, features = ["parse"] } toml_edit = { version = "0.22.22", optional = true } -xshell = "0.1.17" +xshell = "0.2.7" [lints] workspace = true diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs index 755d6746..d70d0934 100644 --- a/xtask/src/cargo.rs +++ b/xtask/src/cargo.rs @@ -9,8 +9,10 @@ use serde::{de::IgnoredAny, Deserialize}; #[cfg(feature = "default")] use toml_edit::{value, DocumentMut}; #[cfg(feature = "default")] -use xshell::{cmd, pushd, read_file, write_file}; +use xshell::Shell; +#[cfg(feature = "default")] +use crate::cmd; use crate::{util::ask_yes_no, Metadata, Result}; const CRATESIO_API: &str = "https://crates.io/api/v1/crates"; @@ -74,15 +76,15 @@ impl Package { #[cfg(feature = "default")] impl Package { /// Update the version of this crate. - pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> { + pub fn update_version(&mut self, sh: &Shell, version: &Version, dry_run: bool) -> Result<()> { println!("Updating {} to version {version}…", self.name); if !dry_run { - let mut document = read_file(&self.manifest_path)?.parse::()?; + let mut document = sh.read_file(&self.manifest_path)?.parse::()?; document["package"]["version"] = value(version.to_string()); - write_file(&self.manifest_path, document.to_string())?; + sh.write_file(&self.manifest_path, document.to_string())?; } self.version = version.clone(); @@ -92,7 +94,12 @@ impl Package { /// Update the version of this crate in dependant crates' manifests, with the given version /// prefix. - pub(crate) fn update_dependants(&self, metadata: &Metadata, dry_run: bool) -> Result<()> { + pub(crate) fn update_dependants( + &self, + sh: &Shell, + metadata: &Metadata, + dry_run: bool, + ) -> Result<()> { if self.name == "ruma" { for package in metadata.packages.iter().filter(|p| { p.manifest_path.starts_with(&metadata.workspace_root) @@ -101,7 +108,8 @@ impl Package { println!("Updating dependency in {} crate…", package.name); if !dry_run { - let mut document = read_file(&package.manifest_path)?.parse::()?; + let mut document = + sh.read_file(&package.manifest_path)?.parse::()?; let version = if !self.version.pre.is_empty() { format!("={}", self.version) @@ -119,12 +127,12 @@ impl Package { document[kind][&self.name]["version"] = value(version.as_str()); } - write_file(&package.manifest_path, document.to_string())?; + sh.write_file(&package.manifest_path, document.to_string())?; } } } else { let workspace_manifest_path = metadata.workspace_root.join("Cargo.toml"); - let mut document = read_file(&workspace_manifest_path)?.parse::()?; + let mut document = sh.read_file(&workspace_manifest_path)?.parse::()?; let workspace_deps = &mut document["workspace"]["dependencies"]; println!("Updating workspace dependency…"); @@ -139,7 +147,7 @@ impl Package { workspace_deps[&self.name]["version"] = value(version.as_str()); - write_file(&workspace_manifest_path, document.to_string())?; + sh.write_file(&workspace_manifest_path, document.to_string())?; } } @@ -149,7 +157,7 @@ impl Package { /// Get the changes for the version. /// /// If `update` is `true`, update the changelog for the release of the given version. - pub fn changes(&self, update: bool) -> Result { + pub fn changes(&self, sh: &Shell, update: bool) -> Result { if self.name == "ruma-macros" { // ruma-macros doesn't have a changelog and won't create a tag. return Ok(String::new()); @@ -158,7 +166,7 @@ impl Package { let mut changelog_path = self.manifest_path.clone(); changelog_path.set_file_name("CHANGELOG.md"); - let changelog = read_file(&changelog_path)?; + let changelog = sh.read_file(&changelog_path)?; let version = Version { pre: semver::Prerelease::EMPTY, build: semver::BuildMetadata::EMPTY, @@ -196,7 +204,7 @@ impl Package { let rest = &changelog[changes_end..]; let changelog = format!("# [unreleased]\n\n# {}\n\n{changes}\n{rest}", self.version); - write_file(&changelog_path, changelog)?; + sh.write_file(&changelog_path, changelog)?; } Ok(changes.to_owned()) @@ -211,16 +219,16 @@ impl Package { } /// Publish this package on crates.io. - pub fn publish(&self, client: &Client, dry_run: bool) -> Result<()> { + pub fn publish(&self, sh: &Shell, client: &Client, dry_run: bool) -> Result<()> { println!("Publishing {} {} on crates.io…", self.name, self.version); - let _dir = pushd(self.manifest_path.parent().unwrap())?; + let _dir = sh.push_dir(self.manifest_path.parent().unwrap()); if self.is_published(client)? { if !ask_yes_no("This version is already published. Skip this step and continue?")? { return Err("Release interrupted by user.".into()); } } else if !dry_run { - cmd!("cargo publish").run()?; + cmd!(sh, "cargo publish").run()?; } Ok(()) diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index e9275d7d..3ef83aa1 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -4,7 +4,7 @@ use std::path::Path; use clap::{Args, Subcommand}; -use xshell::pushd; +use xshell::Shell; use crate::{cmd, Metadata, Result, NIGHTLY}; @@ -81,12 +81,16 @@ pub struct CiTask { /// The metadata of the workspace. project_metadata: Metadata, + + /// The shell API to use to run commands. + sh: Shell, } impl CiTask { pub(crate) fn new(cmd: Option) -> Result { - let project_metadata = Metadata::load()?; - Ok(Self { cmd, project_metadata }) + let sh = Shell::new()?; + let project_metadata = Metadata::load(&sh)?; + Ok(Self { cmd, sh, project_metadata }) } fn project_root(&self) -> &Path { @@ -94,7 +98,7 @@ impl CiTask { } pub(crate) fn run(self) -> Result<()> { - let _p = pushd(self.project_root())?; + let _p = self.sh.push_dir(self.project_root()); match self.cmd { Some(CiCmd::Msrv) => self.msrv()?, @@ -147,6 +151,7 @@ impl CiTask { /// * xtask (no real reason to enforce an MSRV for it) fn msrv_all(&self) -> Result<()> { cmd!( + &self.sh, "rustup run {MSRV} cargo check --workspace --all-features --exclude ruma --exclude ruma-macros @@ -159,7 +164,7 @@ impl CiTask { /// Check ruma crate with default features with the MSRV. fn msrv_ruma(&self) -> Result<()> { - cmd!("rustup run {MSRV} cargo check -p ruma").run().map_err(Into::into) + cmd!(&self.sh, "rustup run {MSRV} cargo check -p ruma").run().map_err(Into::into) } /// Run all the tasks that use the stable version. @@ -177,6 +182,7 @@ impl CiTask { // ruma-macros is pulled in as a dependency, but excluding it on the command line means its // features don't get activated. It has only a single feature, which is nightly-only. cmd!( + &self.sh, "rustup run stable cargo check --workspace --all-features --exclude ruma-macros" ) @@ -186,7 +192,7 @@ impl CiTask { /// Check ruma-client without default features with the stable version. fn stable_client(&self) -> Result<()> { - cmd!("rustup run stable cargo check -p ruma-client --no-default-features") + cmd!(&self.sh, "rustup run stable cargo check -p ruma-client --no-default-features") .run() .map_err(Into::into) } @@ -194,6 +200,7 @@ impl CiTask { /// Check ruma-common with onjy the required features with the stable version. fn stable_common(&self) -> Result<()> { cmd!( + &self.sh, "rustup run stable cargo check -p ruma-common --no-default-features --features client,server" ) @@ -203,20 +210,24 @@ impl CiTask { /// Run tests on all crates with almost all features with the stable version. fn test_all(&self) -> Result<()> { - cmd!("rustup run stable cargo test --tests --features __ci").run().map_err(Into::into) + cmd!(&self.sh, "rustup run stable cargo test --tests --features __ci") + .run() + .map_err(Into::into) } /// Run tests on all crates with almost all features and the compat features with the stable /// version. fn test_compat(&self) -> Result<()> { - cmd!("rustup run stable cargo test --tests --features __ci,compat") + cmd!(&self.sh, "rustup run stable cargo test --tests --features __ci,compat") .run() .map_err(Into::into) } /// Run doctests on all crates with almost all features with the stable version. fn test_doc(&self) -> Result<()> { - cmd!("rustup run stable cargo test --doc --features __ci").run().map_err(Into::into) + cmd!(&self.sh, "rustup run stable cargo test --doc --features __ci") + .run() + .map_err(Into::into) } /// Run all the tasks that use the nightly version. @@ -230,12 +241,14 @@ impl CiTask { /// Check the formatting with the nightly version. fn fmt(&self) -> Result<()> { - cmd!("rustup run {NIGHTLY} cargo fmt -- --check").run().map_err(Into::into) + cmd!(&self.sh, "rustup run {NIGHTLY} cargo fmt -- --check").run().map_err(Into::into) } /// Check ruma crate with full feature with the nightly version. fn nightly_full(&self) -> Result<()> { - cmd!("rustup run {NIGHTLY} cargo check -p ruma --features full").run().map_err(Into::into) + cmd!(&self.sh, "rustup run {NIGHTLY} cargo check -p ruma --features full") + .run() + .map_err(Into::into) } /// Check all crates with all features with the nightly version. @@ -243,6 +256,7 @@ impl CiTask { /// Also checks that all features that are used in the code exist. fn nightly_all(&self) -> Result<()> { cmd!( + &self.sh, " rustup run {NIGHTLY} cargo check --workspace --all-features -Z unstable-options @@ -259,7 +273,7 @@ impl CiTask { /// Check ruma-common with `ruma_identifiers_storage="Box"` fn msrv_owned_id_box(&self) -> Result<()> { - cmd!("rustup run {MSRV} cargo check -p ruma-common") + cmd!(&self.sh, "rustup run {MSRV} cargo check -p ruma-common") .env("RUSTFLAGS", "--cfg=ruma_identifiers_storage=\"Box\"") .run() .map_err(Into::into) @@ -267,7 +281,7 @@ impl CiTask { /// Check ruma-common with `ruma_identifiers_storage="Arc"` fn msrv_owned_id_arc(&self) -> Result<()> { - cmd!("rustup run {MSRV} cargo check -p ruma-common") + cmd!(&self.sh, "rustup run {MSRV} cargo check -p ruma-common") .env("RUSTFLAGS", "--cfg=ruma_identifiers_storage=\"Arc\"") .run() .map_err(Into::into) @@ -276,6 +290,7 @@ impl CiTask { /// Lint default features with clippy with the nightly version. fn clippy_default(&self) -> Result<()> { cmd!( + &self.sh, " rustup run {NIGHTLY} cargo clippy --workspace --all-targets --features=full -- -D warnings @@ -288,6 +303,7 @@ impl CiTask { /// Lint ruma with clippy with the nightly version and wasm target. fn clippy_wasm(&self) -> Result<()> { cmd!( + &self.sh, " rustup run {NIGHTLY} cargo clippy --target wasm32-unknown-unknown -p ruma --features __unstable-mscs,api,canonical-json,client-api,events,html-matrix,identity-service-api,js,markdown,rand,signatures,unstable-unspecified -- -D warnings @@ -301,6 +317,7 @@ impl CiTask { /// Lint almost all features with clippy with the nightly version. fn clippy_all(&self) -> Result<()> { cmd!( + &self.sh, " rustup run {NIGHTLY} cargo clippy --workspace --all-targets --features=__ci,compat -- -D warnings @@ -324,13 +341,14 @@ impl CiTask { /// Check the sorting of dependencies with the nightly version. fn dependencies(&self) -> Result<()> { - if cmd!("cargo sort --version").run().is_err() { + if cmd!(&self.sh, "cargo sort --version").run().is_err() { return Err( "Could not find cargo-sort. Install it by running `cargo install cargo-sort`" .into(), ); } cmd!( + &self.sh, " rustup run {NIGHTLY} cargo sort --workspace --grouped --check @@ -343,11 +361,11 @@ impl CiTask { /// Check the typos. fn typos(&self) -> Result<()> { - if cmd!("typos --version").run().is_err() { + if cmd!(&self.sh, "typos --version").run().is_err() { return Err( "Could not find typos. Install it by running `cargo install typos-cli`".into() ); } - cmd!("typos").run().map_err(Into::into) + cmd!(&self.sh, "typos").run().map_err(Into::into) } } diff --git a/xtask/src/doc.rs b/xtask/src/doc.rs index acc8328a..bab1bd6c 100644 --- a/xtask/src/doc.rs +++ b/xtask/src/doc.rs @@ -1,4 +1,5 @@ use clap::Args; +use xshell::Shell; use crate::{cmd, Result, NIGHTLY}; @@ -20,7 +21,8 @@ impl DocTask { rustdocflags += " -Dwarnings"; } - let mut cmd = cmd!("rustup run {NIGHTLY} cargo doc --all-features --no-deps") + let sh = Shell::new()?; + let mut cmd = cmd!(sh, "rustup run {NIGHTLY} cargo doc --all-features --no-deps") .env("RUSTDOCFLAGS", rustdocflags); if self.open { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 5c18a59b..0373514f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -30,6 +30,7 @@ use ci::{CiArgs, CiTask}; use doc::DocTask; #[cfg(feature = "default")] use release::{ReleaseArgs, ReleaseTask}; +use xshell::Shell; type Result = std::result::Result>; @@ -75,8 +76,8 @@ struct Metadata { impl Metadata { /// Load a new `Metadata` from the command line. - pub fn load() -> Result { - let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?; + pub fn load(sh: &Shell) -> Result { + let metadata_json = cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?; Ok(from_json_str(&metadata_json)?) } @@ -96,11 +97,11 @@ struct Config { #[cfg(feature = "default")] impl Config { /// Load a new `Config` from `config.toml`. - fn load() -> Result { + fn load(sh: &Shell) -> Result { use std::{env, path::Path}; let path = Path::new(&env!("CARGO_MANIFEST_DIR")).join("config.toml"); - let config = xshell::read_file(path)?; + let config = sh.read_file(path)?; Ok(toml::from_str(&config)?) } } @@ -117,7 +118,7 @@ struct GithubConfig { #[macro_export] macro_rules! cmd { - ($cmd:tt) => { - xshell::cmd!($cmd).echo_cmd(false) + ($sh: expr, $cmd:tt) => { + xshell::cmd!($sh, $cmd).quiet() }; } diff --git a/xtask/src/release.rs b/xtask/src/release.rs index a169ac3b..f079797d 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -4,6 +4,7 @@ use clap::Args; use reqwest::{blocking::Client, StatusCode}; use semver::Version; use serde_json::json; +use xshell::Shell; use crate::{cargo::Package, cmd, util::ask_yes_no, GithubConfig, Metadata, Result}; @@ -40,6 +41,9 @@ pub struct ReleaseTask { /// The github configuration required to publish a release. config: GithubConfig, + /// The shell API to use to run commands. + sh: Shell, + /// List the steps but don't actually change anything pub dry_run: bool, } @@ -47,7 +51,8 @@ pub struct ReleaseTask { impl ReleaseTask { /// Create a new `ReleaseTask` with the given `name` and `version`. pub(crate) fn new(name: String, version: Version, dry_run: bool) -> Result { - let metadata = Metadata::load()?; + let sh = Shell::new()?; + let metadata = Metadata::load(&sh)?; let package = metadata .packages @@ -56,11 +61,11 @@ impl ReleaseTask { .find(|p| p.name == name) .ok_or(format!("Package {name} not found in cargo metadata"))?; - let config = crate::Config::load()?.github; + let config = crate::Config::load(&sh)?.github; let http_client = Client::builder().user_agent("ruma xtask").build()?; - Ok(Self { metadata, package, version, http_client, config, dry_run }) + Ok(Self { metadata, package, version, http_client, config, sh, dry_run }) } /// Run the task to effectively create a release. @@ -81,10 +86,10 @@ impl ReleaseTask { return Err("This crate version is already released".into()); } - let remote = Self::git_remote()?; + let remote = self.git_remote()?; println!("Checking status of git repository…"); - if !cmd!("git status -s -uno").read()?.is_empty() + if !cmd!(&self.sh, "git status -s -uno").read()?.is_empty() && !ask_yes_no("This git repository contains uncommitted changes. Continue?")? { return Ok(()); @@ -101,8 +106,8 @@ impl ReleaseTask { } let create_commit = if self.package.version != self.version { - self.package.update_version(&self.version, self.dry_run)?; - self.package.update_dependants(&self.metadata, self.dry_run)?; + self.package.update_version(&self.sh, &self.version, self.dry_run)?; + self.package.update_dependants(&self.sh, &self.metadata, self.dry_run)?; true } else if !ask_yes_no(&format!( "Package is already version {}. Skip creating a commit and continue?", @@ -113,19 +118,19 @@ impl ReleaseTask { false }; - let changes = &self.package.changes(!prerelease && !self.dry_run)?; + let changes = &self.package.changes(&self.sh, !prerelease && !self.dry_run)?; if create_commit { self.commit()?; } - self.package.publish(&self.http_client, self.dry_run)?; + self.package.publish(&self.sh, &self.http_client, self.dry_run)?; - let branch = cmd!("git rev-parse --abbrev-ref HEAD").read()?; + let branch = cmd!(&self.sh, "git rev-parse --abbrev-ref HEAD").read()?; if publish_only { println!("Pushing to remote repository…"); if !self.dry_run { - cmd!("git push {remote} {branch}").run()?; + cmd!(&self.sh, "git push {remote} {branch}").run()?; } println!("Crate published successfully!"); @@ -135,18 +140,18 @@ impl ReleaseTask { let tag = &self.tag_name(); println!("Creating git tag '{tag}'…"); - if cmd!("git tag -l {tag}").read()?.is_empty() { + if cmd!(&self.sh, "git tag -l {tag}").read()?.is_empty() { if !self.dry_run { - cmd!("git tag -s {tag} -m {title} -m {changes}").read()?; + cmd!(&self.sh, "git tag -s {tag} -m {title} -m {changes}").read()?; } } else if !ask_yes_no("This tag already exists. Skip this step and continue?")? { return Ok(()); } println!("Pushing to remote repository…"); - if cmd!("git ls-remote --tags {remote} {tag}").read()?.is_empty() { + if cmd!(&self.sh, "git ls-remote --tags {remote} {tag}").read()?.is_empty() { if !self.dry_run { - cmd!("git push {remote} {branch} {tag}").run()?; + cmd!(&self.sh, "git push {remote} {branch} {tag}").run()?; } } else if !ask_yes_no("This tag has already been pushed. Skip this step and continue?")? { return Ok(()); @@ -192,9 +197,9 @@ impl ReleaseTask { } /// Load the GitHub config from the config file. - fn git_remote() -> Result { - let branch = cmd!("git rev-parse --abbrev-ref HEAD").read()?; - let remote = cmd!("git config branch.{branch}.remote").read()?; + fn git_remote(&self) -> Result { + let branch = cmd!(&self.sh, "git rev-parse --abbrev-ref HEAD").read()?; + let remote = cmd!(&self.sh, "git config branch.{branch}.remote").read()?; if remote.is_empty() { return Err("Could not get current git remote".into()); @@ -229,7 +234,7 @@ impl ReleaseTask { return Err("User aborted commit".into()); } "d" | "diff" => { - cmd!("git diff").run()?; + cmd!(&self.sh, "git diff").run()?; } _ => { println!("Unknown command."); @@ -247,7 +252,7 @@ impl ReleaseTask { println!("Creating commit with message '{message}'…"); if !self.dry_run { - cmd!("git commit -a -m {message}").read()?; + cmd!(&self.sh, "git commit -a -m {message}").read()?; } Ok(())