about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAleksey Kladov <aleksey.kladov@gmail.com>2020-10-16 19:46:03 +0200
committerAleksey Kladov <aleksey.kladov@gmail.com>2020-10-17 02:42:42 +0200
commit49a90d4c31148a6533d9ee9a288f42b454b2f421 (patch)
treecf93fa6a4f3d18e8be27acf56ee85927fd6f66c7
parentf0412da4a2c06e50030d13e37002d0440fc7cded (diff)
downloadrust-49a90d4c31148a6533d9ee9a288f42b454b2f421.tar.gz
rust-49a90d4c31148a6533d9ee9a288f42b454b2f421.zip
Switch from not_bash to xshell
-rw-r--r--Cargo.lock23
-rw-r--r--xtask/Cargo.toml2
-rw-r--r--xtask/src/codegen.rs19
-rw-r--r--xtask/src/codegen/gen_features.rs10
-rw-r--r--xtask/src/dist.rs33
-rw-r--r--xtask/src/install.rs46
-rw-r--r--xtask/src/lib.rs52
-rw-r--r--xtask/src/main.rs4
-rw-r--r--xtask/src/metrics.rs39
-rw-r--r--xtask/src/not_bash.rs169
-rw-r--r--xtask/src/pre_cache.rs5
-rw-r--r--xtask/src/pre_commit.rs8
-rw-r--r--xtask/src/release.rs60
-rw-r--r--xtask/tests/tidy.rs36
14 files changed, 181 insertions, 325 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d470d84f2c8..7a77ed7222f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -422,12 +422,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "fs-err"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcd1163ae48bda72a20ae26d66a04d3094135cadab911cff418ae5e33f253431"
-
-[[package]]
 name = "fsevent"
 version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1921,16 +1915,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "xshell"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7f756f2faab73adb00db44db716598ab2c9e4bce4a875c053022291bd3cab4"
+dependencies = [
+ "xshell-macros",
+]
+
+[[package]]
+name = "xshell-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b020c2f3132b34067e2f6ebc58f0f210624898713a8186b8cdb75d3b8c3001"
+
+[[package]]
 name = "xtask"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "flate2",
- "fs-err",
  "pico-args",
  "proc-macro2",
  "quote",
  "ungrammar",
  "walkdir",
  "write-json",
+ "xshell",
 ]
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 01a83882538..2ef95648559 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -18,5 +18,5 @@ quote = "1.0.2"
 ungrammar = "1.1.3"
 walkdir = "2.3.1"
 write-json =  "0.1.0"
-fs-err = "2.3"
+xshell = "0.1"
 # Avoid adding more dependencies to this crate
diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs
index 1e789461742..3ee4c1adf42 100644
--- a/xtask/src/codegen.rs
+++ b/xtask/src/codegen.rs
@@ -15,12 +15,9 @@ use std::{
     fmt, mem,
     path::{Path, PathBuf},
 };
+use xshell::{cmd, pushenv, read_file, write_file};
 
-use crate::{
-    ensure_rustfmt,
-    not_bash::{fs2, pushenv, run},
-    project_root, Result,
-};
+use crate::{ensure_rustfmt, project_root, Result};
 
 pub use self::{
     gen_assists_docs::{generate_assists_docs, generate_assists_tests},
@@ -57,7 +54,7 @@ impl CodegenCmd {
 /// A helper to update file on disk if it has changed.
 /// With verify = false,
 fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> {
-    match fs2::read_to_string(path) {
+    match read_file(path) {
         Ok(old_contents) if normalize(&old_contents) == normalize(contents) => {
             return Ok(());
         }
@@ -67,7 +64,7 @@ fn update(path: &Path, contents: &str, mode: Mode) -> Result<()> {
         anyhow::bail!("`{}` is not up-to-date", path.display());
     }
     eprintln!("updating {}", path.display());
-    fs2::write(path, contents)?;
+    write_file(path, contents)?;
     return Ok(());
 
     fn normalize(s: &str) -> String {
@@ -80,10 +77,10 @@ const PREAMBLE: &str = "Generated file, do not edit by hand, see `xtask/src/code
 fn reformat(text: &str) -> Result<String> {
     let _e = pushenv("RUSTUP_TOOLCHAIN", "stable");
     ensure_rustfmt()?;
-    let stdout = run!(
-        "rustfmt --config-path {} --config fn_single_line=true", project_root().join("rustfmt.toml").display();
-        <text.as_bytes()
-    )?;
+    let rustfmt_toml = project_root().join("rustfmt.toml");
+    let stdout = cmd!("rustfmt --config-path {rustfmt_toml} --config fn_single_line=true")
+        .stdin(text)
+        .read()?;
     Ok(format!("//! {}\n\n{}\n", PREAMBLE, stdout))
 }
 
diff --git a/xtask/src/codegen/gen_features.rs b/xtask/src/codegen/gen_features.rs
index b58c4a0c99c..3cf15ce0217 100644
--- a/xtask/src/codegen/gen_features.rs
+++ b/xtask/src/codegen/gen_features.rs
@@ -3,15 +3,13 @@ use std::path::{Path, PathBuf};
 
 use quote::quote;
 use walkdir::WalkDir;
+use xshell::{cmd, read_file};
 
-use crate::{
-    codegen::{project_root, reformat, update, Mode, Result},
-    not_bash::{fs2, run},
-};
+use crate::codegen::{project_root, reformat, update, Mode, Result};
 
 pub fn generate_features(mode: Mode) -> Result<()> {
     if !Path::new("./target/rust").exists() {
-        run!("git clone https://github.com/rust-lang/rust ./target/rust")?;
+        cmd!("git clone https://github.com/rust-lang/rust ./target/rust").run()?;
     }
 
     let contents = generate_descriptor("./target/rust/src/doc/unstable-book/src".into())?;
@@ -34,7 +32,7 @@ fn generate_descriptor(src_dir: PathBuf) -> Result<String> {
         .map(|entry| {
             let path = entry.path();
             let feature_ident = path.file_stem().unwrap().to_str().unwrap().replace("-", "_");
-            let doc = fs2::read_to_string(path).unwrap();
+            let doc = read_file(path).unwrap();
 
             quote! { LintCompletion { label: #feature_ident, description: #doc } }
         });
diff --git a/xtask/src/dist.rs b/xtask/src/dist.rs
index aa7d949677d..9e15a5a4c9f 100644
--- a/xtask/src/dist.rs
+++ b/xtask/src/dist.rs
@@ -1,4 +1,3 @@
-use flate2::{write::GzEncoder, Compression};
 use std::{
     env,
     fs::File,
@@ -7,11 +6,10 @@ use std::{
 };
 
 use anyhow::Result;
+use flate2::{write::GzEncoder, Compression};
+use xshell::{cmd, cp, mkdir_p, pushd, read_file, rm_rf, write_file};
 
-use crate::{
-    not_bash::{date_iso, fs2, pushd, rm_rf, run},
-    project_root,
-};
+use crate::{date_iso, project_root};
 
 pub struct DistCmd {
     pub nightly: bool,
@@ -22,7 +20,7 @@ impl DistCmd {
     pub fn run(self) -> Result<()> {
         let dist = project_root().join("dist");
         rm_rf(&dist)?;
-        fs2::create_dir_all(&dist)?;
+        mkdir_p(&dist)?;
 
         if let Some(version) = self.client_version {
             let release_tag = if self.nightly { "nightly".to_string() } else { date_iso()? };
@@ -34,7 +32,7 @@ impl DistCmd {
 }
 
 fn dist_client(version: &str, release_tag: &str) -> Result<()> {
-    let _d = pushd("./editors/code");
+    let _d = pushd("./editors/code")?;
     let nightly = release_tag == "nightly";
 
     let mut patch = Patch::new("./package.json")?;
@@ -54,20 +52,16 @@ fn dist_client(version: &str, release_tag: &str) -> Result<()> {
     }
     patch.commit()?;
 
-    run!("npm ci")?;
-    run!("npx vsce package -o ../../dist/rust-analyzer.vsix")?;
+    cmd!("npm ci").run()?;
+    cmd!("npx vsce package -o ../../dist/rust-analyzer.vsix").run()?;
     Ok(())
 }
 
 fn dist_server() -> Result<()> {
     if cfg!(target_os = "linux") {
         env::set_var("CC", "clang");
-        run!(
-            "cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release"
-        )?;
-    } else {
-        run!("cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release")?;
     }
+    cmd!("cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --release").run()?;
 
     let (src, dst) = if cfg!(target_os = "linux") {
         ("./target/release/rust-analyzer", "./dist/rust-analyzer-linux")
@@ -82,7 +76,7 @@ fn dist_server() -> Result<()> {
     let src = Path::new(src);
     let dst = Path::new(dst);
 
-    fs2::copy(&src, &dst)?;
+    cp(&src, &dst)?;
     gzip(&src, &dst.with_extension("gz"))?;
 
     Ok(())
@@ -105,7 +99,7 @@ struct Patch {
 impl Patch {
     fn new(path: impl Into<PathBuf>) -> Result<Patch> {
         let path = path.into();
-        let contents = fs2::read_to_string(&path)?;
+        let contents = read_file(&path)?;
         Ok(Patch { path, original_contents: contents.clone(), contents })
     }
 
@@ -115,13 +109,14 @@ impl Patch {
         self
     }
 
-    fn commit(&self) -> io::Result<()> {
-        fs2::write(&self.path, &self.contents)
+    fn commit(&self) -> Result<()> {
+        write_file(&self.path, &self.contents)?;
+        Ok(())
     }
 }
 
 impl Drop for Patch {
     fn drop(&mut self) {
-        fs2::write(&self.path, &self.original_contents).unwrap();
+        write_file(&self.path, &self.original_contents).unwrap();
     }
 }
diff --git a/xtask/src/install.rs b/xtask/src/install.rs
index fcc4f05e4ca..789e9f27bfa 100644
--- a/xtask/src/install.rs
+++ b/xtask/src/install.rs
@@ -3,8 +3,7 @@
 use std::{env, path::PathBuf, str};
 
 use anyhow::{bail, format_err, Context, Result};
-
-use crate::not_bash::{pushd, run};
+use xshell::{cmd, pushd};
 
 // Latest stable, feel free to send a PR if this lags behind.
 const REQUIRED_RUST_VERSION: u32 = 47;
@@ -76,7 +75,7 @@ fn fix_path_for_mac() -> Result<()> {
 }
 
 fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> {
-    let _dir = pushd("./editors/code");
+    let _dir = pushd("./editors/code")?;
 
     let find_code = |f: fn(&str) -> bool| -> Result<&'static str> {
         ["code", "code-insiders", "codium", "code-oss"]
@@ -89,24 +88,25 @@ fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> {
     };
 
     let installed_extensions = if cfg!(unix) {
-        run!("npm --version").context("`npm` is required to build the VS Code plugin")?;
-        run!("npm install")?;
+        cmd!("npm --version").run().context("`npm` is required to build the VS Code plugin")?;
+        cmd!("npm install").run()?;
 
-        run!("npm run package --scripts-prepend-node-path")?;
+        cmd!("npm run package --scripts-prepend-node-path").run()?;
 
-        let code = find_code(|bin| run!("{} --version", bin).is_ok())?;
-        run!("{} --install-extension rust-analyzer.vsix --force", code)?;
-        run!("{} --list-extensions", code; echo = false)?
+        let code = find_code(|bin| cmd!("{bin} --version").read().is_ok())?;
+        cmd!("{code} --install-extension rust-analyzer.vsix --force").run()?;
+        cmd!("{code} --list-extensions").read()?
     } else {
-        run!("cmd.exe /c npm --version")
+        cmd!("cmd.exe /c npm --version")
+            .run()
             .context("`npm` is required to build the VS Code plugin")?;
-        run!("cmd.exe /c npm install")?;
+        cmd!("cmd.exe /c npm install").run()?;
 
-        run!("cmd.exe /c npm run package")?;
+        cmd!("cmd.exe /c npm run package").run()?;
 
-        let code = find_code(|bin| run!("cmd.exe /c {}.cmd --version", bin).is_ok())?;
-        run!(r"cmd.exe /c {}.cmd --install-extension rust-analyzer.vsix --force", code)?;
-        run!("cmd.exe /c {}.cmd --list-extensions", code; echo = false)?
+        let code = find_code(|bin| cmd!("cmd.exe /c {bin}.cmd --version").read().is_ok())?;
+        cmd!("cmd.exe /c {code}.cmd --install-extension rust-analyzer.vsix --force").run()?;
+        cmd!("cmd.exe /c {code}.cmd --list-extensions").read()?
     };
 
     if !installed_extensions.contains("rust-analyzer") {
@@ -122,7 +122,7 @@ fn install_client(ClientOpt::VsCode: ClientOpt) -> Result<()> {
 
 fn install_server(opts: ServerOpt) -> Result<()> {
     let mut old_rust = false;
-    if let Ok(stdout) = run!("cargo --version") {
+    if let Ok(stdout) = cmd!("cargo --version").read() {
         if !check_version(&stdout, REQUIRED_RUST_VERSION) {
             old_rust = true;
         }
@@ -134,12 +134,13 @@ fn install_server(opts: ServerOpt) -> Result<()> {
             REQUIRED_RUST_VERSION,
         )
     }
-
-    let malloc_feature = match opts.malloc {
-        Malloc::System => "",
-        Malloc::Mimalloc => "--features mimalloc",
+    let features = match opts.malloc {
+        Malloc::System => &[][..],
+        Malloc::Mimalloc => &["--features", "mimalloc"],
     };
-    let res = run!("cargo install --path crates/rust-analyzer --locked --force {}", malloc_feature);
+
+    let cmd = cmd!("cargo install --path crates/rust-analyzer --locked --force {features...}");
+    let res = cmd.run();
 
     if res.is_err() && old_rust {
         eprintln!(
@@ -148,7 +149,8 @@ fn install_server(opts: ServerOpt) -> Result<()> {
         );
     }
 
-    res.map(drop)
+    res?;
+    Ok(())
 }
 
 fn check_version(version_output: &str, min_minor_version: u32) -> bool {
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs
index e790d995fb3..babec2dbd00 100644
--- a/xtask/src/lib.rs
+++ b/xtask/src/lib.rs
@@ -2,7 +2,6 @@
 //!
 //! See https://github.com/matklad/cargo-xtask/
 
-pub mod not_bash;
 pub mod codegen;
 mod ast_src;
 
@@ -19,11 +18,9 @@ use std::{
 };
 
 use walkdir::{DirEntry, WalkDir};
+use xshell::{cmd, pushd, pushenv};
 
-use crate::{
-    codegen::Mode,
-    not_bash::{pushd, pushenv},
-};
+use crate::codegen::Mode;
 
 pub use anyhow::{bail, Context as _, Result};
 
@@ -53,18 +50,19 @@ pub fn rust_files(path: &Path) -> impl Iterator<Item = PathBuf> {
 }
 
 pub fn run_rustfmt(mode: Mode) -> Result<()> {
-    let _dir = pushd(project_root());
+    let _dir = pushd(project_root())?;
     let _e = pushenv("RUSTUP_TOOLCHAIN", "stable");
     ensure_rustfmt()?;
-    match mode {
-        Mode::Overwrite => run!("cargo fmt"),
-        Mode::Verify => run!("cargo fmt -- --check"),
-    }?;
+    let check = match mode {
+        Mode::Overwrite => &[][..],
+        Mode::Verify => &["--", "--check"],
+    };
+    cmd!("cargo fmt {check...}").run()?;
     Ok(())
 }
 
 fn ensure_rustfmt() -> Result<()> {
-    let out = run!("rustfmt --version")?;
+    let out = cmd!("rustfmt --version").read()?;
     if !out.contains("stable") {
         bail!(
             "Failed to run rustfmt from toolchain 'stable'. \
@@ -75,40 +73,46 @@ fn ensure_rustfmt() -> Result<()> {
 }
 
 pub fn run_clippy() -> Result<()> {
-    if run!("cargo clippy --version").is_err() {
+    if cmd!("cargo clippy --version").read().is_err() {
         bail!(
             "Failed run cargo clippy. \
             Please run `rustup component add clippy` to install it.",
         )
     }
 
-    let allowed_lints = [
-        "clippy::collapsible_if",
-        "clippy::needless_pass_by_value",
-        "clippy::nonminimal_bool",
-        "clippy::redundant_pattern_matching",
-    ];
-    run!("cargo clippy --all-features --all-targets -- -A {}", allowed_lints.join(" -A "))?;
+    let allowed_lints = "
+        -A clippy::collapsible_if
+        -A clippy::needless_pass_by_value
+        -A clippy::nonminimal_bool
+        -A clippy::redundant_pattern_matching
+    "
+    .split_ascii_whitespace();
+    cmd!("cargo clippy --all-features --all-targets -- {allowed_lints...}").run()?;
     Ok(())
 }
 
 pub fn run_fuzzer() -> Result<()> {
-    let _d = pushd("./crates/syntax");
+    let _d = pushd("./crates/syntax")?;
     let _e = pushenv("RUSTUP_TOOLCHAIN", "nightly");
-    if run!("cargo fuzz --help").is_err() {
-        run!("cargo install cargo-fuzz")?;
+    if cmd!("cargo fuzz --help").read().is_err() {
+        cmd!("cargo install cargo-fuzz").run()?;
     };
 
     // Expecting nightly rustc
-    let out = run!("rustc --version")?;
+    let out = cmd!("rustc --version").read()?;
     if !out.contains("nightly") {
         bail!("fuzz tests require nightly rustc")
     }
 
-    run!("cargo fuzz run parser")?;
+    cmd!("cargo fuzz run parser").run()?;
     Ok(())
 }
 
+fn date_iso() -> Result<String> {
+    let res = cmd!("date --iso --utc").read()?;
+    Ok(res)
+}
+
 fn is_release_tag(tag: &str) -> bool {
     tag.len() == "2020-02-24".len() && tag.starts_with(|c: char| c.is_ascii_digit())
 }
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 3f4aa5497dc..97e5dcd4e5b 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -12,12 +12,12 @@ use std::env;
 
 use codegen::CodegenCmd;
 use pico_args::Arguments;
+use xshell::pushd;
 use xtask::{
     codegen::{self, Mode},
     dist::DistCmd,
     install::{ClientOpt, InstallCmd, Malloc, ServerOpt},
     metrics::MetricsCmd,
-    not_bash::pushd,
     pre_cache::PreCacheCmd,
     pre_commit, project_root,
     release::{PromoteCmd, ReleaseCmd},
@@ -29,7 +29,7 @@ fn main() -> Result<()> {
         return pre_commit::run_hook();
     }
 
-    let _d = pushd(project_root());
+    let _d = pushd(project_root())?;
 
     let mut args = Arguments::from_env();
     let subcommand = args.subcommand()?.unwrap_or_default();
diff --git a/xtask/src/metrics.rs b/xtask/src/metrics.rs
index 4bade2c7e27..e0d1aaf97de 100644
--- a/xtask/src/metrics.rs
+++ b/xtask/src/metrics.rs
@@ -7,8 +7,7 @@ use std::{
 };
 
 use anyhow::{bail, format_err, Result};
-
-use crate::not_bash::{fs2, pushd, pushenv, rm_rf, run};
+use xshell::{cmd, mkdir_p, pushd, pushenv, read_file, rm_rf};
 
 type Unit = String;
 
@@ -23,12 +22,13 @@ impl MetricsCmd {
             rm_rf("./target/release")?;
         }
         if !Path::new("./target/rustc-perf").exists() {
-            fs2::create_dir_all("./target/rustc-perf")?;
-            run!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")?;
+            mkdir_p("./target/rustc-perf")?;
+            cmd!("git clone https://github.com/rust-lang/rustc-perf.git ./target/rustc-perf")
+                .run()?;
         }
         {
-            let _d = pushd("./target/rustc-perf");
-            run!("git reset --hard 1d9288b0da7febf2599917da1b57dc241a1af033")?;
+            let _d = pushd("./target/rustc-perf")?;
+            cmd!("git reset --hard 1d9288b0da7febf2599917da1b57dc241a1af033").run()?;
         }
 
         let _env = pushenv("RA_METRICS", "1");
@@ -39,17 +39,20 @@ impl MetricsCmd {
         metrics.measure_analysis_stats("webrender")?;
 
         if !self.dry_run {
-            let _d = pushd("target");
+            let _d = pushd("target")?;
             let metrics_token = env::var("METRICS_TOKEN").unwrap();
-            let repo = format!("https://{}@github.com/rust-analyzer/metrics.git", metrics_token);
-            run!("git clone --depth 1 {}", repo)?;
-            let _d = pushd("metrics");
+            cmd!(
+                "git clone --depth 1 https://{metrics_token}@github.com/rust-analyzer/metrics.git"
+            )
+            .run()?;
+            let _d = pushd("metrics")?;
 
             let mut file = std::fs::OpenOptions::new().append(true).open("metrics.json")?;
             writeln!(file, "{}", metrics.json())?;
-            run!("git add .")?;
-            run!("git -c user.name=Bot -c user.email=dummy@example.com commit --message 📈")?;
-            run!("git push origin master")?;
+            cmd!("git add .").run()?;
+            cmd!("git -c user.name=Bot -c user.email=dummy@example.com commit --message 📈")
+                .run()?;
+            cmd!("git push origin master").run()?;
         }
         eprintln!("{:#?}", metrics);
         Ok(())
@@ -59,10 +62,10 @@ impl MetricsCmd {
 impl Metrics {
     fn measure_build(&mut self) -> Result<()> {
         eprintln!("\nMeasuring build");
-        run!("cargo fetch")?;
+        cmd!("cargo fetch").run()?;
 
         let time = Instant::now();
-        run!("cargo build --release --package rust-analyzer --bin rust-analyzer")?;
+        cmd!("cargo build --release --package rust-analyzer --bin rust-analyzer").run()?;
         let time = time.elapsed();
         self.report("build", time.as_millis() as u64, "ms".into());
         Ok(())
@@ -78,7 +81,7 @@ impl Metrics {
     }
     fn measure_analysis_stats_path(&mut self, name: &str, path: &str) -> Result<()> {
         eprintln!("\nMeasuring analysis-stats/{}", name);
-        let output = run!("./target/release/rust-analyzer analysis-stats --quiet {}", path)?;
+        let output = cmd!("./target/release/rust-analyzer analysis-stats --quiet {path}").read()?;
         for (metric, value, unit) in parse_metrics(&output) {
             self.report(&format!("analysis-stats/{}/{}", name, metric), value, unit.into());
         }
@@ -118,7 +121,7 @@ impl Metrics {
     fn new() -> Result<Metrics> {
         let host = Host::new()?;
         let timestamp = SystemTime::now();
-        let revision = run!("git rev-parse HEAD")?;
+        let revision = cmd!("git rev-parse HEAD").read()?;
         Ok(Metrics { host, timestamp, revision, metrics: BTreeMap::new() })
     }
 
@@ -160,7 +163,7 @@ impl Host {
         return Ok(Host { os, cpu, mem });
 
         fn read_field<'a>(path: &str, field: &str) -> Result<String> {
-            let text = fs2::read_to_string(path)?;
+            let text = read_file(path)?;
 
             let line = text
                 .lines()
diff --git a/xtask/src/not_bash.rs b/xtask/src/not_bash.rs
deleted file mode 100644
index 038898993ac..00000000000
--- a/xtask/src/not_bash.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-//! A bad shell -- small cross platform module for writing glue code
-
-use std::{
-    cell::RefCell,
-    env,
-    ffi::OsString,
-    io::{self, Write},
-    path::{Path, PathBuf},
-    process::{Command, Stdio},
-};
-
-use anyhow::{bail, Context, Result};
-
-pub use fs_err as fs2;
-
-#[macro_export]
-macro_rules! run {
-    ($($expr:expr),*) => {
-        run!($($expr),*; echo = true)
-    };
-    ($($expr:expr),* ; echo = $echo:expr) => {
-        $crate::not_bash::run_process(format!($($expr),*), $echo, None)
-    };
-    ($($expr:expr),* ;  <$stdin:expr) => {
-        $crate::not_bash::run_process(format!($($expr),*), false, Some($stdin))
-    };
-}
-pub use crate::run;
-
-pub struct Pushd {
-    _p: (),
-}
-
-pub fn pushd(path: impl Into<PathBuf>) -> Pushd {
-    Env::with(|env| env.pushd(path.into()));
-    Pushd { _p: () }
-}
-
-impl Drop for Pushd {
-    fn drop(&mut self) {
-        Env::with(|env| env.popd())
-    }
-}
-
-pub struct Pushenv {
-    _p: (),
-}
-
-pub fn pushenv(var: &str, value: &str) -> Pushenv {
-    Env::with(|env| env.pushenv(var.into(), value.into()));
-    Pushenv { _p: () }
-}
-
-impl Drop for Pushenv {
-    fn drop(&mut self) {
-        Env::with(|env| env.popenv())
-    }
-}
-
-pub fn rm_rf(path: impl AsRef<Path>) -> io::Result<()> {
-    let path = path.as_ref();
-    if !path.exists() {
-        return Ok(());
-    }
-    if path.is_file() {
-        fs2::remove_file(path)
-    } else {
-        fs2::remove_dir_all(path)
-    }
-}
-
-#[doc(hidden)]
-pub fn run_process(cmd: String, echo: bool, stdin: Option<&[u8]>) -> Result<String> {
-    run_process_inner(&cmd, echo, stdin).with_context(|| format!("process `{}` failed", cmd))
-}
-
-pub fn date_iso() -> Result<String> {
-    run!("date --iso --utc")
-}
-
-fn run_process_inner(cmd: &str, echo: bool, stdin: Option<&[u8]>) -> Result<String> {
-    let mut args = shelx(cmd);
-    let binary = args.remove(0);
-    let current_dir = Env::with(|it| it.cwd().to_path_buf());
-
-    if echo {
-        println!("> {}", cmd)
-    }
-
-    let mut command = Command::new(binary);
-    command.args(args).current_dir(current_dir).stderr(Stdio::inherit());
-    let output = match stdin {
-        None => command.stdin(Stdio::null()).output(),
-        Some(stdin) => {
-            command.stdin(Stdio::piped()).stdout(Stdio::piped());
-            let mut process = command.spawn()?;
-            process.stdin.take().unwrap().write_all(stdin)?;
-            process.wait_with_output()
-        }
-    }?;
-    let stdout = String::from_utf8(output.stdout)?;
-
-    if echo {
-        print!("{}", stdout)
-    }
-
-    if !output.status.success() {
-        bail!("{}", output.status)
-    }
-
-    Ok(stdout.trim().to_string())
-}
-
-// FIXME: some real shell lexing here
-fn shelx(cmd: &str) -> Vec<String> {
-    let mut res = Vec::new();
-    for (string_piece, in_quotes) in cmd.split('\'').zip([false, true].iter().copied().cycle()) {
-        if in_quotes {
-            res.push(string_piece.to_string())
-        } else {
-            if !string_piece.is_empty() {
-                res.extend(string_piece.split_ascii_whitespace().map(|it| it.to_string()))
-            }
-        }
-    }
-    res
-}
-
-struct Env {
-    pushd_stack: Vec<PathBuf>,
-    pushenv_stack: Vec<(OsString, Option<OsString>)>,
-}
-
-impl Env {
-    fn with<F: FnOnce(&mut Env) -> T, T>(f: F) -> T {
-        thread_local! {
-            static ENV: RefCell<Env> = RefCell::new(Env {
-                pushd_stack: vec![env::current_dir().unwrap()],
-                pushenv_stack: vec![],
-            });
-        }
-        ENV.with(|it| f(&mut *it.borrow_mut()))
-    }
-
-    fn pushd(&mut self, dir: PathBuf) {
-        let dir = self.cwd().join(dir);
-        self.pushd_stack.push(dir);
-        env::set_current_dir(self.cwd())
-            .unwrap_or_else(|err| panic!("Failed to set cwd to {}: {}", self.cwd().display(), err));
-    }
-    fn popd(&mut self) {
-        self.pushd_stack.pop().unwrap();
-        env::set_current_dir(self.cwd()).unwrap();
-    }
-    fn pushenv(&mut self, var: OsString, value: OsString) {
-        self.pushenv_stack.push((var.clone(), env::var_os(&var)));
-        env::set_var(var, value)
-    }
-    fn popenv(&mut self) {
-        let (var, value) = self.pushenv_stack.pop().unwrap();
-        match value {
-            None => env::remove_var(var),
-            Some(value) => env::set_var(var, value),
-        }
-    }
-    fn cwd(&self) -> &Path {
-        self.pushd_stack.last().unwrap()
-    }
-}
diff --git a/xtask/src/pre_cache.rs b/xtask/src/pre_cache.rs
index 47ba6ba246c..569f88f68f0 100644
--- a/xtask/src/pre_cache.rs
+++ b/xtask/src/pre_cache.rs
@@ -4,8 +4,7 @@ use std::{
 };
 
 use anyhow::Result;
-
-use crate::not_bash::{fs2, rm_rf};
+use xshell::rm_rf;
 
 pub struct PreCacheCmd;
 
@@ -26,7 +25,7 @@ impl PreCacheCmd {
             }
         }
 
-        fs2::remove_file("./target/.rustc_info.json")?;
+        rm_rf("./target/.rustc_info.json")?;
 
         let to_delete = read_dir("./crates", FileType::is_dir)?
             .into_iter()
diff --git a/xtask/src/pre_commit.rs b/xtask/src/pre_commit.rs
index 056f34acfb8..8f2dbea19a0 100644
--- a/xtask/src/pre_commit.rs
+++ b/xtask/src/pre_commit.rs
@@ -3,19 +3,21 @@
 use std::{fs, path::PathBuf};
 
 use anyhow::{bail, Result};
+use xshell::cmd;
 
-use crate::{not_bash::run, project_root, run_rustfmt, Mode};
+use crate::{project_root, run_rustfmt, Mode};
 
 // FIXME: if there are changed `.ts` files, also reformat TypeScript (by
 // shelling out to `npm fmt`).
 pub fn run_hook() -> Result<()> {
     run_rustfmt(Mode::Overwrite)?;
 
-    let diff = run!("git diff --diff-filter=MAR --name-only --cached")?;
+    let diff = cmd!("git diff --diff-filter=MAR --name-only --cached").read()?;
 
     let root = project_root();
     for line in diff.lines() {
-        run!("git update-index --add {}", root.join(line).display())?;
+        let file = root.join(line);
+        cmd!("git update-index --add {file}").run()?;
     }
 
     Ok(())
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index 3aab29801d5..14fc1f0ddf4 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -1,8 +1,6 @@
-use crate::{
-    codegen, is_release_tag,
-    not_bash::{date_iso, fs2, pushd, run},
-    project_root, Mode, Result,
-};
+use xshell::{cmd, cp, pushd, read_dir, write_file};
+
+use crate::{codegen, date_iso, is_release_tag, project_root, Mode, Result};
 
 pub struct ReleaseCmd {
     pub dry_run: bool,
@@ -11,10 +9,10 @@ pub struct ReleaseCmd {
 impl ReleaseCmd {
     pub fn run(self) -> Result<()> {
         if !self.dry_run {
-            run!("git switch release")?;
-            run!("git fetch upstream --tags --force")?;
-            run!("git reset --hard tags/nightly")?;
-            run!("git push")?;
+            cmd!("git switch release").run()?;
+            cmd!("git fetch upstream --tags --force").run()?;
+            cmd!("git reset --hard tags/nightly").run()?;
+            cmd!("git push").run()?;
         }
         codegen::generate_assists_docs(Mode::Overwrite)?;
         codegen::generate_feature_docs(Mode::Overwrite)?;
@@ -23,8 +21,8 @@ impl ReleaseCmd {
         let changelog_dir = website_root.join("./thisweek/_posts");
 
         let today = date_iso()?;
-        let commit = run!("git rev-parse HEAD")?;
-        let changelog_n = fs2::read_dir(changelog_dir.as_path())?.count();
+        let commit = cmd!("git rev-parse HEAD").read()?;
+        let changelog_n = read_dir(changelog_dir.as_path())?.len();
 
         let contents = format!(
             "\
@@ -52,20 +50,20 @@ https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
         );
 
         let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n));
-        fs2::write(&path, &contents)?;
+        write_file(&path, &contents)?;
 
         for &adoc in ["manual.adoc", "generated_features.adoc", "generated_assists.adoc"].iter() {
             let src = project_root().join("./docs/user/").join(adoc);
             let dst = website_root.join(adoc);
-            fs2::copy(src, dst)?;
+            cp(src, dst)?;
         }
 
-        let tags = run!("git tag --list"; echo = false)?;
+        let tags = cmd!("git tag --list").read()?;
         let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap();
 
-        let git_log = run!("git log {}..HEAD --merges --reverse", prev_tag; echo = false)?;
+        let git_log = cmd!("git log {prev_tag}..HEAD --merges --reverse").read()?;
         let git_log_dst = website_root.join("git.log");
-        fs2::write(git_log_dst, &git_log)?;
+        write_file(git_log_dst, &git_log)?;
 
         Ok(())
     }
@@ -77,27 +75,25 @@ pub struct PromoteCmd {
 
 impl PromoteCmd {
     pub fn run(self) -> Result<()> {
-        let _dir = pushd("../rust-rust-analyzer");
-        run!("git switch master")?;
-        run!("git fetch upstream")?;
-        run!("git reset --hard upstream/master")?;
-        run!("git submodule update --recursive")?;
+        let _dir = pushd("../rust-rust-analyzer")?;
+        cmd!("git switch master").run()?;
+        cmd!("git fetch upstream").run()?;
+        cmd!("git reset --hard upstream/master").run()?;
+        cmd!("git submodule update --recursive").run()?;
 
         let branch = format!("rust-analyzer-{}", date_iso()?);
-        run!("git switch -c {}", branch)?;
+        cmd!("git switch -c {branch}").run()?;
         {
-            let _dir = pushd("src/tools/rust-analyzer");
-            run!("git fetch origin")?;
-            run!("git reset --hard origin/release")?;
+            let _dir = pushd("src/tools/rust-analyzer")?;
+            cmd!("git fetch origin").run()?;
+            cmd!("git reset --hard origin/release").run()?;
         }
-        run!("git add src/tools/rust-analyzer")?;
-        run!("git commit -m':arrow_up: rust-analyzer'")?;
+        cmd!("git add src/tools/rust-analyzer").run()?;
+        cmd!("git commit -m':arrow_up: rust-analyzer'").run()?;
         if !self.dry_run {
-            run!("git push")?;
-            run!(
-                "xdg-open https://github.com/matklad/rust/pull/new/{}?body=r%3F%20%40ghost",
-                branch
-            )?;
+            cmd!("git push").run()?;
+            cmd!("xdg-open https://github.com/matklad/rust/pull/new/{branch}?body=r%3F%20%40ghost")
+                .run()?;
         }
         Ok(())
     }
diff --git a/xtask/tests/tidy.rs b/xtask/tests/tidy.rs
index b3bb9d543a1..d335adb72b3 100644
--- a/xtask/tests/tidy.rs
+++ b/xtask/tests/tidy.rs
@@ -3,9 +3,9 @@ use std::{
     path::{Path, PathBuf},
 };
 
+use xshell::{cmd, read_file};
 use xtask::{
     codegen::{self, Mode},
-    not_bash::{fs2, run},
     project_root, run_rustfmt, rust_files,
 };
 
@@ -48,14 +48,13 @@ fn smoke_test_docs_generation() {
 fn check_lsp_extensions_docs() {
     let expected_hash = {
         let lsp_ext_rs =
-            fs2::read_to_string(project_root().join("crates/rust-analyzer/src/lsp_ext.rs"))
-                .unwrap();
+            read_file(project_root().join("crates/rust-analyzer/src/lsp_ext.rs")).unwrap();
         stable_hash(lsp_ext_rs.as_str())
     };
 
     let actual_hash = {
         let lsp_extensions_md =
-            fs2::read_to_string(project_root().join("docs/dev/lsp-extensions.md")).unwrap();
+            read_file(project_root().join("docs/dev/lsp-extensions.md")).unwrap();
         let text = lsp_extensions_md
             .lines()
             .find_map(|line| line.strip_prefix("lsp_ext.rs hash:"))
@@ -83,7 +82,7 @@ Please adjust docs/dev/lsp-extensions.md.
 fn rust_files_are_tidy() {
     let mut tidy_docs = TidyDocs::default();
     for path in rust_files(&project_root().join("crates")) {
-        let text = fs2::read_to_string(&path).unwrap();
+        let text = read_file(&path).unwrap();
         check_todo(&path, &text);
         check_trailing_ws(&path, &text);
         deny_clippy(&path, &text);
@@ -94,8 +93,10 @@ fn rust_files_are_tidy() {
 
 #[test]
 fn check_merge_commits() {
-    let stdout = run!("git rev-list --merges --invert-grep --author 'bors\\[bot\\]' HEAD~19.."; echo = false)
-        .unwrap();
+    let stdout =
+        dbg!(cmd!("git rev-list --merges --invert-grep --author 'bors\\[bot\\]' HEAD~19.."))
+            .read()
+            .unwrap();
     if !stdout.is_empty() {
         panic!(
             "
@@ -168,7 +169,7 @@ Zlib OR Apache-2.0 OR MIT
     .filter(|it| !it.is_empty())
     .collect::<Vec<_>>();
 
-    let meta = run!("cargo metadata --format-version 1"; echo = false).unwrap();
+    let meta = cmd!("cargo metadata --format-version 1").read().unwrap();
     let mut licenses = meta
         .split(|c| c == ',' || c == '{' || c == '}')
         .filter(|it| it.contains(r#""license""#))
@@ -177,6 +178,25 @@ Zlib OR Apache-2.0 OR MIT
         .collect::<Vec<_>>();
     licenses.sort();
     licenses.dedup();
+    if licenses != expected {
+        let mut diff = String::new();
+
+        diff += &format!("New Licenses:\n");
+        for &l in licenses.iter() {
+            if !expected.contains(&l) {
+                diff += &format!("  {}\n", l)
+            }
+        }
+
+        diff += &format!("\nMissing Licenses:\n");
+        for &l in expected.iter() {
+            if !licenses.contains(&l) {
+                diff += &format!("  {}\n", l)
+            }
+        }
+
+        panic!("different set of licenses!\n{}", diff);
+    }
     assert_eq!(licenses, expected);
 }