From 698f8c2f01ea549d77d7dc3338a12e04c11057b9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 14:02:58 +0200 Subject: Adding upstream version 1.64.0+dfsg1. Signed-off-by: Daniel Baumann --- vendor/xshell/.cargo-checksum.json | 1 + vendor/xshell/CHANGELOG.md | 85 ++ vendor/xshell/Cargo.lock | 23 + vendor/xshell/Cargo.toml | 27 + vendor/xshell/LICENSE-APACHE | 201 +++++ vendor/xshell/LICENSE-MIT | 23 + vendor/xshell/README.md | 39 + vendor/xshell/examples/ci.rs | 112 +++ vendor/xshell/examples/clone_and_publish.rs | 28 + vendor/xshell/src/error.rs | 177 +++++ vendor/xshell/src/lib.rs | 1112 +++++++++++++++++++++++++++ vendor/xshell/tests/compile_time.rs | 41 + vendor/xshell/tests/data/xecho.rs | 85 ++ vendor/xshell/tests/it/compile_failures.rs | 128 +++ vendor/xshell/tests/it/env.rs | 130 ++++ vendor/xshell/tests/it/main.rs | 462 +++++++++++ vendor/xshell/tests/it/tidy.rs | 37 + 17 files changed, 2711 insertions(+) create mode 100644 vendor/xshell/.cargo-checksum.json create mode 100644 vendor/xshell/CHANGELOG.md create mode 100644 vendor/xshell/Cargo.lock create mode 100644 vendor/xshell/Cargo.toml create mode 100644 vendor/xshell/LICENSE-APACHE create mode 100644 vendor/xshell/LICENSE-MIT create mode 100644 vendor/xshell/README.md create mode 100644 vendor/xshell/examples/ci.rs create mode 100644 vendor/xshell/examples/clone_and_publish.rs create mode 100644 vendor/xshell/src/error.rs create mode 100644 vendor/xshell/src/lib.rs create mode 100644 vendor/xshell/tests/compile_time.rs create mode 100644 vendor/xshell/tests/data/xecho.rs create mode 100644 vendor/xshell/tests/it/compile_failures.rs create mode 100644 vendor/xshell/tests/it/env.rs create mode 100644 vendor/xshell/tests/it/main.rs create mode 100644 vendor/xshell/tests/it/tidy.rs (limited to 'vendor/xshell') diff --git a/vendor/xshell/.cargo-checksum.json b/vendor/xshell/.cargo-checksum.json new file mode 100644 index 000000000..dd079af53 --- /dev/null +++ b/vendor/xshell/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"dd08a5079cd2e437eb1269f7c60be9c05ce97db407dbb95a631d64c0f1c478a1","Cargo.lock":"f8f5019959ed5fcd772c3cf199220be41ce1e7b2b9a35b153c52f9c8f3a35171","Cargo.toml":"04a9cad4d6a6e7c756c35d3ba776334f0c47b895e54f7006a27617430c84b25f","LICENSE-APACHE":"a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","README.md":"a480b1b8b943c633c64ce9c215f8ecea24e58c7242fe4605d37e7a3be810ab9f","examples/ci.rs":"d5fbfc199469c08f3d459164c05a85c1a0a8f1bef625b347533ad7a43c1e97fb","examples/clone_and_publish.rs":"94568ef665e65527417bb5d50b0404bc60d6a72942d70260c8f3ce1a99820077","src/error.rs":"9222d0b21a889c9fbac1d285c6d43573be9c94f0ea5c02b4bd692bdc02753b49","src/lib.rs":"61d7c1dcd569e60188baba51521661c177bff5a27bcf5272bd83f06542c2304a","tests/compile_time.rs":"224f3476eff4070fbd62c1974e7e69996efd92263bc12f7ddbd59823d85484da","tests/data/xecho.rs":"7a82252daade541bc3843fffa617fc50bf2faf7eebc55e4442a0bc9bb59182fd","tests/it/compile_failures.rs":"c87a438583c9f4b4e45a7422df3ee7c6bd5e69150eba5468425c63aa70fa47d3","tests/it/env.rs":"e863965669378e603c36186e1c738914e3d2300cbe3b04288a9ed689edcf09fb","tests/it/main.rs":"1bb089455f92d6486bf80502ebbbd1f136248194b9c454949f21ed279ea58028","tests/it/tidy.rs":"f530cf51504d43716e849ac96fb64a3a6ef80bd3e56b6eb1bd7b5325dc2f2de9"},"package":"6d47097dc5c85234b1e41851b3422dd6d19b3befdd35b4ae5ce386724aeca981"} \ No newline at end of file diff --git a/vendor/xshell/CHANGELOG.md b/vendor/xshell/CHANGELOG.md new file mode 100644 index 000000000..cbef62a3e --- /dev/null +++ b/vendor/xshell/CHANGELOG.md @@ -0,0 +1,85 @@ +# Changelog + +## 0.2.2 + +- Add `Shell::path_exists`. + +## 0.2.1 + +- `Shell::remove_path` returns `Ok` if the path does not exist (ie the function + is now idempotent). + +## 0.2.0 + +A major release with significant changes to the API: + +- All global state is removed in favor of explicitly passing a `Shell` instance. +- Some methods are renamed to better match Rust naming conventions. +- New APIs for controlling working directory and environment. +- MSRV is raised to 1.59.0. +- Improved reliability across the board: the crate aims to become a dependable + 1.0 tool in the future (no ETA). +- This is expected to be the last *large* API reshuffle. + +## 0.1.17 + +- Allow panics to transparently pass through xshell calls. + This removes some internal lock poisoned errors. + +## 0.1.16 + +- Add `xshell::hard_link`. + +## 0.1.15 + +- Correctly handle multiple internal read guards. + +## 0.1.14 + +- Correctly handle commands name starting with quote. + +## 0.1.13 + +- Add `ignore_stdout`, `ignore_stderr` functions. + +## 0.1.12 + +- Add `env`, `env_revome`, `env_clear` functions. + +## 0.1.11 + +- `write_file` now creates the intervening directory path if it doesn't exit. + +## 0.1.10 + +- `echo_cmd` output goes to stderr, not stdout. + +## 0.1.9 + +- `mktemp_d` creates an (insecure, world readable) temporary directory. +- Fix cp docs. + +## 0.1.8 + +- Add option to not echo command at all. +- Add option to censor command contents when echoing. +- Add docs. + +## 0.1.7 + +- `cp(foo, bar)` copies `foo` _into_ `bar`, if `bar` is an existing directory. +- Tweak reading API. + +## 0.1.6 + +- `.read()` chomps `\r\n` on Windows. +- Prevent cwd/env races when using `.read()` or `.run()`. +- Better spans in error messages. + +## 0.1.5 + +- Improve proc-macro error messages. + +## 0.1.4 + +- No changelog until this point :( diff --git a/vendor/xshell/Cargo.lock b/vendor/xshell/Cargo.lock new file mode 100644 index 000000000..1ea0d8ff2 --- /dev/null +++ b/vendor/xshell/Cargo.lock @@ -0,0 +1,23 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "xshell" +version = "0.2.2" +dependencies = [ + "anyhow", + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88301b56c26dd9bf5c43d858538f82d6f3f7764767defbc5d34e59459901c41a" diff --git a/vendor/xshell/Cargo.toml b/vendor/xshell/Cargo.toml new file mode 100644 index 000000000..f4fb7cff2 --- /dev/null +++ b/vendor/xshell/Cargo.toml @@ -0,0 +1,27 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.59" +name = "xshell" +version = "0.2.2" +authors = ["Aleksey Kladov "] +exclude = [".github/", "bors.toml", "rustfmt.toml", "cbench", "mock_bin/"] +description = "Utilities for quick shell scripting in Rust" +categories = ["development-tools::build-utils", "filesystem"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/matklad/xshell" +resolver = "2" +[dependencies.xshell-macros] +version = "=0.2.2" +[dev-dependencies.anyhow] +version = "1.0.56" diff --git a/vendor/xshell/LICENSE-APACHE b/vendor/xshell/LICENSE-APACHE new file mode 100644 index 000000000..78173fa2e --- /dev/null +++ b/vendor/xshell/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/xshell/LICENSE-MIT b/vendor/xshell/LICENSE-MIT new file mode 100644 index 000000000..31aa79387 --- /dev/null +++ b/vendor/xshell/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/xshell/README.md b/vendor/xshell/README.md new file mode 100644 index 000000000..36026cb10 --- /dev/null +++ b/vendor/xshell/README.md @@ -0,0 +1,39 @@ +# xshell: Making Rust a Better Bash + +`xshell` provides a set of cross-platform utilities for writing cross-platform +and ergonomic "bash" scripts. + +## Example + +```rust +//! Clones a git repository and publishes it to crates.io. +use xshell::{cmd, Shell}; + +fn main() -> anyhow::Result<()> { + let sh = Shell::new()?; + + let user = "matklad"; + let repo = "xshell"; + cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; + sh.change_dir(repo); + + let test_args = ["-Zunstable-options", "--report-time"]; + cmd!(sh, "cargo test -- {test_args...}").run()?; + + let manifest = sh.read_file("Cargo.toml")?; + let version = manifest + .split_once("version = \"") + .and_then(|it| it.1.split_once('\"')) + .map(|it| it.0) + .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?; + + cmd!(sh, "git tag {version}").run()?; + + let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; + cmd!(sh, "cargo publish {dry_run...}").run()?; + + Ok(()) +} +``` + +See [the docs](https://docs.rs/xshell) for more. diff --git a/vendor/xshell/examples/ci.rs b/vendor/xshell/examples/ci.rs new file mode 100644 index 000000000..799b28824 --- /dev/null +++ b/vendor/xshell/examples/ci.rs @@ -0,0 +1,112 @@ +//! This CI script for `xshell`. +//! +//! It also serves as a real-world example, yay bootstrap! +use std::{env, process, thread, time::Duration, time::Instant}; + +use xshell::{cmd, Result, Shell}; + +fn main() { + if let Err(err) = try_main() { + eprintln!("{}", err); + process::exit(1); + } +} + +fn try_main() -> Result<()> { + let sh = Shell::new()?; + if env::args().nth(1).as_deref() == Some("publish") { + publish(&sh) + } else { + test(&sh) + } +} + +fn test(sh: &Shell) -> Result<()> { + // Can't delete oneself on Windows. + if !cfg!(windows) { + sh.remove_path("./target")?; + } + + { + let _s = Section::new("BUILD"); + cmd!(sh, "cargo test --workspace --no-run").run()?; + } + + { + let _s = Section::new("TEST"); + cmd!(sh, "cargo test --workspace").run()?; + } + Ok(()) +} + +fn publish(sh: &Shell) -> Result<()> { + let _s = Section::new("PUBLISH"); + let manifest = sh.read_file("./Cargo.toml")?; + + let version = manifest + .lines() + .find_map(|line| { + let words = line.split_ascii_whitespace().collect::>(); + match words.as_slice() { + [n, "=", v, ..] if n.trim() == "version" => { + assert!(v.starts_with('"') && v.ends_with('"')); + return Some(&v[1..v.len() - 1]); + } + _ => None, + } + }) + .unwrap(); + + let tag = format!("v{}", version); + let tags = cmd!(sh, "git tag --list").read()?; + let tag_exists = tags.split_ascii_whitespace().any(|it| it == &tag); + + let current_branch = cmd!(sh, "git branch --show-current").read()?; + + if current_branch == "master" && !tag_exists { + cmd!(sh, "git tag v{version}").run()?; + + let token = sh.var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string()); + { + let _p = sh.push_dir("xshell-macros"); + cmd!(sh, "cargo publish --token {token}").run()?; + for _ in 0..100 { + thread::sleep(Duration::from_secs(3)); + let err_msg = + cmd!(sh, "cargo install xshell-macros --version {version} --bin non-existing") + .ignore_status() + .read_stderr()?; + + let not_found = err_msg.contains("could not find "); + let tried_installing = err_msg.contains("Installing"); + assert!(not_found ^ tried_installing); + if tried_installing { + break; + } + } + } + cmd!(sh, "cargo publish --token {token}").run()?; + cmd!(sh, "git push --tags").run()?; + } + Ok(()) +} + +struct Section { + name: &'static str, + start: Instant, +} + +impl Section { + fn new(name: &'static str) -> Section { + println!("::group::{}", name); + let start = Instant::now(); + Section { name, start } + } +} + +impl Drop for Section { + fn drop(&mut self) { + println!("{}: {:.2?}", self.name, self.start.elapsed()); + println!("::endgroup::"); + } +} diff --git a/vendor/xshell/examples/clone_and_publish.rs b/vendor/xshell/examples/clone_and_publish.rs new file mode 100644 index 000000000..e18d9e200 --- /dev/null +++ b/vendor/xshell/examples/clone_and_publish.rs @@ -0,0 +1,28 @@ +//! Clones a git repository and publishes it to crates.io. +use xshell::{cmd, Shell}; + +fn main() -> anyhow::Result<()> { + let sh = Shell::new()?; + + let user = "matklad"; + let repo = "xshell"; + cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; + sh.change_dir(repo); + + let test_args = ["-Zunstable-options", "--report-time"]; + cmd!(sh, "cargo test -- {test_args...}").run()?; + + let manifest = sh.read_file("Cargo.toml")?; + let version = manifest + .split_once("version = \"") + .and_then(|it| it.1.split_once('\"')) + .map(|it| it.0) + .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?; + + cmd!(sh, "git tag {version}").run()?; + + let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; + cmd!(sh, "cargo publish {dry_run...}").run()?; + + Ok(()) +} diff --git a/vendor/xshell/src/error.rs b/vendor/xshell/src/error.rs new file mode 100644 index 000000000..3e0f5dc3f --- /dev/null +++ b/vendor/xshell/src/error.rs @@ -0,0 +1,177 @@ +use std::{env, ffi::OsString, fmt, io, path::PathBuf, process::ExitStatus, string::FromUtf8Error}; + +use crate::{Cmd, CmdData}; + +/// `Result` from std, with the error type defaulting to xshell's [`Error`]. +pub type Result = std::result::Result; + +/// An error returned by an `xshell` operation. +pub struct Error { + kind: Box, +} + +/// Note: this is intentionally not public. +enum ErrorKind { + CurrentDir { err: io::Error }, + Var { err: env::VarError, var: OsString }, + ReadFile { err: io::Error, path: PathBuf }, + ReadDir { err: io::Error, path: PathBuf }, + WriteFile { err: io::Error, path: PathBuf }, + CopyFile { err: io::Error, src: PathBuf, dst: PathBuf }, + HardLink { err: io::Error, src: PathBuf, dst: PathBuf }, + CreateDir { err: io::Error, path: PathBuf }, + RemovePath { err: io::Error, path: PathBuf }, + CmdStatus { cmd: CmdData, status: ExitStatus }, + CmdIo { err: io::Error, cmd: CmdData }, + CmdUtf8 { err: FromUtf8Error, cmd: CmdData }, + CmdStdin { err: io::Error, cmd: CmdData }, +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + let kind = Box::new(kind); + Error { kind } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &*self.kind { + ErrorKind::CurrentDir { err } => write!(f, "failed to get current directory: {err}"), + ErrorKind::Var { err, var } => { + let var = var.to_string_lossy(); + write!(f, "failed to get environment variable `{var}`: {err}") + } + ErrorKind::ReadFile { err, path } => { + let path = path.display(); + write!(f, "failed to read file `{path}`: {err}") + } + ErrorKind::ReadDir { err, path } => { + let path = path.display(); + write!(f, "failed read directory `{path}`: {err}") + } + ErrorKind::WriteFile { err, path } => { + let path = path.display(); + write!(f, "failed to write file `{path}`: {err}") + } + ErrorKind::CopyFile { err, src, dst } => { + let src = src.display(); + let dst = dst.display(); + write!(f, "failed to copy `{src}` to `{dst}`: {err}") + } + ErrorKind::HardLink { err, src, dst } => { + let src = src.display(); + let dst = dst.display(); + write!(f, "failed hard link `{src}` to `{dst}`: {err}") + } + ErrorKind::CreateDir { err, path } => { + let path = path.display(); + write!(f, "failed to create directory `{path}`: {err}") + } + ErrorKind::RemovePath { err, path } => { + let path = path.display(); + write!(f, "failed to remove path `{path}`: {err}") + } + ErrorKind::CmdStatus { cmd, status } => match status.code() { + Some(code) => write!(f, "command exited with non-zero code `{cmd}`: {code}"), + #[cfg(unix)] + None => { + use std::os::unix::process::ExitStatusExt; + match status.signal() { + Some(sig) => write!(f, "command was terminated by a signal `{cmd}`: {sig}"), + None => write!(f, "command was terminated by a signal `{cmd}`"), + } + } + #[cfg(not(unix))] + None => write!(f, "command was terminated by a signal `{cmd}`"), + }, + ErrorKind::CmdIo { err, cmd } => { + if err.kind() == io::ErrorKind::NotFound { + let prog = cmd.prog.display(); + write!(f, "command not found: `{prog}`") + } else { + write!(f, "io error when running command `{cmd}`: {err}") + } + } + ErrorKind::CmdUtf8 { err, cmd } => { + write!(f, "failed to decode output of command `{cmd}`: {err}") + } + ErrorKind::CmdStdin { err, cmd } => { + write!(f, "failed to write to stdin of command `{cmd}`: {err}") + } + }?; + Ok(()) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} +impl std::error::Error for Error {} + +/// `pub(crate)` constructors, visible only in this crate. +impl Error { + pub(crate) fn new_current_dir(err: io::Error) -> Error { + ErrorKind::CurrentDir { err }.into() + } + + pub(crate) fn new_var(err: env::VarError, var: OsString) -> Error { + ErrorKind::Var { err, var }.into() + } + + pub(crate) fn new_read_file(err: io::Error, path: PathBuf) -> Error { + ErrorKind::ReadFile { err, path }.into() + } + + pub(crate) fn new_read_dir(err: io::Error, path: PathBuf) -> Error { + ErrorKind::ReadDir { err, path }.into() + } + + pub(crate) fn new_write_file(err: io::Error, path: PathBuf) -> Error { + ErrorKind::WriteFile { err, path }.into() + } + + pub(crate) fn new_copy_file(err: io::Error, src: PathBuf, dst: PathBuf) -> Error { + ErrorKind::CopyFile { err, src, dst }.into() + } + + pub(crate) fn new_hard_link(err: io::Error, src: PathBuf, dst: PathBuf) -> Error { + ErrorKind::HardLink { err, src, dst }.into() + } + + pub(crate) fn new_create_dir(err: io::Error, path: PathBuf) -> Error { + ErrorKind::CreateDir { err, path }.into() + } + + pub(crate) fn new_remove_path(err: io::Error, path: PathBuf) -> Error { + ErrorKind::RemovePath { err, path }.into() + } + + pub(crate) fn new_cmd_status(cmd: &Cmd<'_>, status: ExitStatus) -> Error { + let cmd = cmd.data.clone(); + ErrorKind::CmdStatus { cmd, status }.into() + } + + pub(crate) fn new_cmd_io(cmd: &Cmd<'_>, err: io::Error) -> Error { + let cmd = cmd.data.clone(); + ErrorKind::CmdIo { err, cmd }.into() + } + + pub(crate) fn new_cmd_utf8(cmd: &Cmd<'_>, err: FromUtf8Error) -> Error { + let cmd = cmd.data.clone(); + ErrorKind::CmdUtf8 { err, cmd }.into() + } + + pub(crate) fn new_cmd_stdin(cmd: &Cmd<'_>, err: io::Error) -> Error { + let cmd = cmd.data.clone(); + ErrorKind::CmdStdin { err, cmd }.into() + } +} + +#[test] +fn error_send_sync() { + fn f() {} + f::(); +} diff --git a/vendor/xshell/src/lib.rs b/vendor/xshell/src/lib.rs new file mode 100644 index 000000000..6b980ba22 --- /dev/null +++ b/vendor/xshell/src/lib.rs @@ -0,0 +1,1112 @@ +//! xshell is a swiss-army knife for writing cross-platform "bash" scripts in +//! Rust. +//! +//! It doesn't use the shell directly, but rather re-implements parts of +//! scripting environment in Rust. The intended use-case is various bits of glue +//! code, which could be written in bash or python. The original motivation is +//! [`xtask`](https://github.com/matklad/cargo-xtask) development. +//! +//! Here's a quick example: +//! +//! ```no_run +//! use xshell::{Shell, cmd}; +//! +//! let sh = Shell::new()?; +//! let branch = "main"; +//! let commit_hash = cmd!(sh, "git rev-parse {branch}").read()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! **Goals:** +//! +//! * Ergonomics and DWIM ("do what I mean"): `cmd!` macro supports +//! interpolation, writing to a file automatically creates parent directories, +//! etc. +//! * Reliability: no [shell injection] by construction, good error messages +//! with file paths, non-zero exit status is an error, independence of the +//! host environment, etc. +//! * Frugality: fast compile times, few dependencies, low-tech API. +//! +//! # Guide +//! +//! For a short API overview, let's implement a script to clone a github +//! repository and publish it as a crates.io crate. The script will do the +//! following: +//! +//! 1. Clone the repository. +//! 2. `cd` into the repository's directory. +//! 3. Run the tests. +//! 4. Create a git tag using a version from `Cargo.toml`. +//! 5. Publish the crate with an optional `--dry-run`. +//! +//! Start with the following skeleton: +//! +//! ```no_run +//! use xshell::{cmd, Shell}; +//! +//! fn main() -> anyhow::Result<()> { +//! let sh = Shell::new()?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! Only two imports are needed -- the [`Shell`] struct the and [`cmd!`] macro. +//! By convention, an instance of a [`Shell`] is stored in a variable named +//! `sh`. All the API is available as methods, so a short name helps here. For +//! "scripts", the [`anyhow`](https://docs.rs/anyhow) crate is a great choice +//! for an error-handling library. +//! +//! Next, clone the repository: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! cmd!(sh, "git clone https://github.com/matklad/xshell.git").run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! The [`cmd!`] macro provides a convenient syntax for creating a command -- +//! the [`Cmd`] struct. The [`Cmd::run`] method runs the command as if you +//! typed it into the shell. The whole program outputs: +//! +//! ```console +//! $ git clone https://github.com/matklad/xshell.git +//! Cloning into 'xshell'... +//! remote: Enumerating objects: 676, done. +//! remote: Counting objects: 100% (220/220), done. +//! remote: Compressing objects: 100% (123/123), done. +//! remote: Total 676 (delta 106), reused 162 (delta 76), pack-reused 456 +//! Receiving objects: 100% (676/676), 136.80 KiB | 222.00 KiB/s, done. +//! Resolving deltas: 100% (327/327), done. +//! ``` +//! +//! Note that the command itself is echoed to stderr (the `$ git ...` bit in the +//! output). You can use [`Cmd::quiet`] to override this behavior: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! cmd!(sh, "git clone https://github.com/matklad/xshell.git") +//! .quiet() +//! .run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! To make the code more general, let's use command interpolation to extract +//! the username and the repository: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let user = "matklad"; +//! let repo = "xshell"; +//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! Note that the `cmd!` macro parses the command string at compile time, so you +//! don't have to worry about escaping the arguments. For example, the following +//! command "touches" a single file whose name is `contains a space`: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let file = "contains a space"; +//! cmd!(sh, "touch {file}").run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! Next, `cd` into the folder you have just cloned: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! # let repo = "xshell"; +//! sh.change_dir(repo); +//! ``` +//! +//! Each instance of [`Shell`] has a current directory, which is independent of +//! the process-wide [`std::env::current_dir`]. The same applies to the +//! environment. +//! +//! Next, run the tests: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let test_args = ["-Zunstable-options", "--report-time"]; +//! cmd!(sh, "cargo test -- {test_args...}").run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! Note how the so-called splat syntax (`...`) is used to interpolate an +//! iterable of arguments. +//! +//! Next, read the Cargo.toml so that we can fetch crate' declared version: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let manifest = sh.read_file("Cargo.toml")?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! [`Shell::read_file`] works like [`std::fs::read_to_string`], but paths are +//! relative to the current directory of the [`Shell`]. Unlike [`std::fs`], +//! error messages are much more useful. For example, if there isn't a +//! `Cargo.toml` in the repository, the error message is: +//! +//! ```text +//! Error: failed to read file `xshell/Cargo.toml`: no such file or directory (os error 2) +//! ``` +//! +//! `xshell` doesn't implement string processing utils like `grep`, `sed` or +//! `awk` -- there's no need to, built-in language features work fine, and it's +//! always possible to pull extra functionality from crates.io. +//! +//! To extract the `version` field from Cargo.toml, [`str::split_once`] is +//! enough: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let manifest = sh.read_file("Cargo.toml")?; +//! let version = manifest +//! .split_once("version = \"") +//! .and_then(|it| it.1.split_once('\"')) +//! .map(|it| it.0) +//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?; +//! +//! cmd!(sh, "git tag {version}").run()?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! The splat (`...`) syntax works with any iterable, and in Rust options are +//! iterable. This means that `...` can be used to implement optional arguments. +//! For example, here's how to pass `--dry-run` when *not* running in CI: +//! +//! ```no_run +//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap(); +//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; +//! cmd!(sh, "cargo publish {dry_run...}").run()?; +//! # Ok::<(), xshell::Error>(()) +//! ``` +//! +//! Putting everything altogether, here's the whole script: +//! +//! ```no_run +//! use xshell::{cmd, Shell}; +//! +//! fn main() -> anyhow::Result<()> { +//! let sh = Shell::new()?; +//! +//! let user = "matklad"; +//! let repo = "xshell"; +//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?; +//! sh.change_dir(repo); +//! +//! let test_args = ["-Zunstable-options", "--report-time"]; +//! cmd!(sh, "cargo test -- {test_args...}").run()?; +//! +//! let manifest = sh.read_file("Cargo.toml")?; +//! let version = manifest +//! .split_once("version = \"") +//! .and_then(|it| it.1.split_once('\"')) +//! .map(|it| it.0) +//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?; +//! +//! cmd!(sh, "git tag {version}").run()?; +//! +//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") }; +//! cmd!(sh, "cargo publish {dry_run...}").run()?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! `xshell` itself uses a similar script to automatically publish oneself to +//! crates.io when the version in Cargo.toml changes: +//! +//! +//! +//! # Maintenance +//! +//! Minimum Supported Rust Version: 1.59.0. MSRV bump is not considered semver +//! breaking. MSRV is updated conservatively. +//! +//! The crate isn't comprehensive yet, but this is a goal. You are hereby +//! encouraged to submit PRs with missing functionality! +//! +//! # Related Crates +//! +//! [`duct`] is a crate for heavy-duty process herding, with support for +//! pipelines. +//! +//! Most of what this crate provides can be open-coded using +//! [`std::process::Command`] and [`std::fs`]. If you only need to spawn a +//! single process, using `std` is probably better (but don't forget to check +//! the exit status!). +//! +//! [`duct`]: https://github.com/oconnor663/duct.rs +//! [shell injection]: +//! https://en.wikipedia.org/wiki/Code_injection#Shell_injection +//! +//! # Implementation Notes +//! +//! The design is heavily inspired by the Julia language: +//! +//! * [Shelling Out +//! Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/) +//! * [Put This In Your +//! Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/) +//! * [Running External +//! Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/) +//! * [Filesystem](https://docs.julialang.org/en/v1/base/file/) +//! +//! Smaller influences are the [`duct`] crate and Ruby's +//! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html) +//! module. +//! +//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on +//! helper libraries, so the fixed-cost impact on compile times is moderate. +//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second. +//! Equivalent program using only `std::process::Command` compiles in 0.25 +//! seconds. +//! +//! To make IDEs infer correct types without expanding proc-macro, it is wrapped +//! into a declarative macro which supplies type hints. + +#![deny(missing_debug_implementations)] +#![deny(missing_docs)] +#![deny(rust_2018_idioms)] + +mod error; + +use std::{ + cell::RefCell, + collections::HashMap, + env::{self, current_dir, VarError}, + ffi::{OsStr, OsString}, + fmt, fs, + io::{self, ErrorKind, Write}, + mem, + path::{Path, PathBuf}, + process::{Command, ExitStatus, Output, Stdio}, + sync::atomic::{AtomicUsize, Ordering}, +}; + +pub use crate::error::{Error, Result}; +#[doc(hidden)] +pub use xshell_macros::__cmd; + +/// Constructs a [`Cmd`] from the given string. +/// +/// # Examples +/// +/// Basic: +/// +/// ```no_run +/// # use xshell::{cmd, Shell}; +/// let sh = Shell::new()?; +/// cmd!(sh, "echo hello world").run()?; +/// # Ok::<(), xshell::Error>(()) +/// ``` +/// +/// Interpolation: +/// +/// ``` +/// # use xshell::{cmd, Shell}; let sh = Shell::new()?; +/// let greeting = "hello world"; +/// let c = cmd!(sh, "echo {greeting}"); +/// assert_eq!(c.to_string(), r#"echo "hello world""#); +/// +/// let c = cmd!(sh, "echo '{greeting}'"); +/// assert_eq!(c.to_string(), r#"echo {greeting}"#); +/// +/// let c = cmd!(sh, "echo {greeting}!"); +/// assert_eq!(c.to_string(), r#"echo "hello world!""#); +/// +/// let c = cmd!(sh, "echo 'spaces '{greeting}' around'"); +/// assert_eq!(c.to_string(), r#"echo "spaces hello world around""#); +/// +/// # Ok::<(), xshell::Error>(()) +/// ``` +/// +/// Splat interpolation: +/// +/// ``` +/// # use xshell::{cmd, Shell}; let sh = Shell::new()?; +/// let args = ["hello", "world"]; +/// let c = cmd!(sh, "echo {args...}"); +/// assert_eq!(c.to_string(), r#"echo hello world"#); +/// +/// let arg1: Option<&str> = Some("hello"); +/// let arg2: Option<&str> = None; +/// let c = cmd!(sh, "echo {arg1...} {arg2...}"); +/// assert_eq!(c.to_string(), r#"echo hello"#); +/// # Ok::<(), xshell::Error>(()) +/// ``` +#[macro_export] +macro_rules! cmd { + ($sh:expr, $cmd:literal) => {{ + #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)] + format_args!($cmd); + let f = |prog| $sh.cmd(prog); + let cmd: $crate::Cmd = $crate::__cmd!(f $cmd); + cmd + }}; +} + +/// A `Shell` is the main API entry point. +/// +/// Almost all of the crate's functionality is available as methods of the +/// `Shell` object. +/// +/// `Shell` is a stateful object. It maintains a logical working directory and +/// an environment map. They are independent from process's +/// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and +/// commands passed to the [`Shell`]. +/// +/// +/// By convention, variable holding the shell is named `sh`. +/// +/// # Example +/// +/// ```no_run +/// use xshell::{cmd, Shell}; +/// +/// let sh = Shell::new()?; +/// let _d = sh.push_dir("./target"); +/// let cwd = sh.current_dir(); +/// cmd!(sh, "echo current dir is {cwd}").run()?; +/// +/// let process_cwd = std::env::current_dir().unwrap(); +/// assert_eq!(cwd, process_cwd.join("./target")); +/// # Ok::<(), xshell::Error>(()) +/// ``` +#[derive(Debug)] +pub struct Shell { + cwd: RefCell, + env: RefCell>, +} + +impl std::panic::UnwindSafe for Shell {} +impl std::panic::RefUnwindSafe for Shell {} + +impl Shell { + /// Creates a new [`Shell`]. + /// + /// Fails if [`std::env::current_dir`] returns an error. + pub fn new() -> Result { + let cwd = current_dir().map_err(Error::new_current_dir)?; + let cwd = RefCell::new(cwd); + let env = RefCell::new(HashMap::new()); + Ok(Shell { cwd, env }) + } + + // region:env + /// Returns the working directory for this [`Shell`]. + /// + /// All relative paths are interpreted relative to this directory, rather + /// than [`std::env::current_dir`]. + #[doc(alias = "pwd")] + pub fn current_dir(&self) -> PathBuf { + self.cwd.borrow().clone() + } + + /// Changes the working directory for this [`Shell`]. + /// + /// Note that this doesn't affect [`std::env::current_dir`]. + #[doc(alias = "pwd")] + pub fn change_dir>(&self, dir: P) { + self._change_dir(dir.as_ref()) + } + fn _change_dir(&self, dir: &Path) { + let dir = self.path(dir); + *self.cwd.borrow_mut() = dir; + } + + /// Temporary changes the working directory of this [`Shell`]. + /// + /// Returns a RAII guard which reverts the working directory to the old + /// value when dropped. + /// + /// Note that this doesn't affect [`std::env::current_dir`]. + #[doc(alias = "pushd")] + pub fn push_dir>(&self, path: P) -> PushDir<'_> { + self._push_dir(path.as_ref()) + } + fn _push_dir(&self, path: &Path) -> PushDir<'_> { + let path = self.path(path); + PushDir::new(self, path) + } + + /// Fetches the environmental variable `key` for this [`Shell`]. + /// + /// Returns an error if the variable is not set, or set to a non-utf8 value. + /// + /// Environment of the [`Shell`] affects all commands spawned via this + /// shell. + pub fn var>(&self, key: K) -> Result { + self._var(key.as_ref()) + } + fn _var(&self, key: &OsStr) -> Result { + match self._var_os(key) { + Some(it) => it.into_string().map_err(VarError::NotUnicode), + None => Err(VarError::NotPresent), + } + .map_err(|err| Error::new_var(err, key.to_os_string())) + } + + /// Fetches the environmental variable `key` for this [`Shell`] as + /// [`OsString`] Returns [`None`] if the variable is not set. + /// + /// Environment of the [`Shell`] affects all commands spawned via this + /// shell. + pub fn var_os>(&self, key: K) -> Option { + self._var_os(key.as_ref()) + } + fn _var_os(&self, key: &OsStr) -> Option { + self.env.borrow().get(key).cloned().or_else(|| env::var_os(key)) + } + + /// Sets the value of `key` environment variable for this [`Shell`] to + /// `val`. + /// + /// Note that this doesn't affect [`std::env::var`]. + pub fn set_var, V: AsRef>(&self, key: K, val: V) { + self._set_var(key.as_ref(), val.as_ref()) + } + fn _set_var(&self, key: &OsStr, val: &OsStr) { + self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string()); + } + + /// Temporary sets the value of `key` environment variable for this + /// [`Shell`] to `val`. + /// + /// Returns a RAII guard which restores the old environment when dropped. + /// + /// Note that this doesn't affect [`std::env::var`]. + pub fn push_env, V: AsRef>(&self, key: K, val: V) -> PushEnv<'_> { + self._push_env(key.as_ref(), val.as_ref()) + } + fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> { + PushEnv::new(self, key.to_os_string(), val.to_os_string()) + } + // endregion:env + + // region:fs + /// Read the entire contents of a file into a string. + #[doc(alias = "cat")] + pub fn read_file>(&self, path: P) -> Result { + self._read_file(path.as_ref()) + } + fn _read_file(&self, path: &Path) -> Result { + let path = self.path(path); + fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path)) + } + + /// Read the entire contents of a file into a vector of bytes. + pub fn read_binary_file>(&self, path: P) -> Result> { + self._read_binary_file(path.as_ref()) + } + fn _read_binary_file(&self, path: &Path) -> Result> { + let path = self.path(path); + fs::read(&path).map_err(|err| Error::new_read_file(err, path)) + } + + /// Returns a sorted list of paths directly contained in the directory at + /// `path`. + #[doc(alias = "ls")] + pub fn read_dir>(&self, path: P) -> Result> { + self._read_dir(path.as_ref()) + } + fn _read_dir(&self, path: &Path) -> Result> { + let path = self.path(path); + let mut res = Vec::new(); + || -> _ { + for entry in fs::read_dir(&path)? { + let entry = entry?; + res.push(entry.path()) + } + Ok(()) + }() + .map_err(|err| Error::new_read_dir(err, path))?; + res.sort(); + Ok(res) + } + + /// Write a slice as the entire contents of a file. + /// + /// This function will create the file and all intermediate directories if + /// they don't exist. + // TODO: probably want to make this an atomic rename write? + pub fn write_file, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> { + self._write_file(path.as_ref(), contents.as_ref()) + } + fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> { + let path = self.path(path); + if let Some(p) = path.parent() { + self.create_dir(p)?; + } + fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path)) + } + + /// Copies `src` into `dst`. + /// + /// `src` must be a file, but `dst` need not be. If `dst` is an existing + /// directory, `src` will be copied into a file in the `dst` directory whose + /// name is same as that of `src`. + /// + /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into + /// it. + #[doc(alias = "cp")] + pub fn copy_file, D: AsRef>(&self, src: S, dst: D) -> Result<()> { + self._copy_file(src.as_ref(), dst.as_ref()) + } + fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> { + let src = self.path(src); + let dst = self.path(dst); + let dst = dst.as_path(); + let mut _tmp; + let mut dst = dst; + if dst.is_dir() { + if let Some(file_name) = src.file_name() { + _tmp = dst.join(file_name); + dst = &_tmp; + } + } + std::fs::copy(&src, dst) + .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?; + Ok(()) + } + + /// Hardlinks `src` to `dst`. + #[doc(alias = "ln")] + pub fn hard_link, D: AsRef>(&self, src: S, dst: D) -> Result<()> { + self._hard_link(src.as_ref(), dst.as_ref()) + } + fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> { + let src = self.path(src); + let dst = self.path(dst); + fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst)) + } + + /// Creates the specified directory. + /// + /// All intermediate directories will also be created. + #[doc(alias("mkdir_p", "mkdir"))] + pub fn create_dir>(&self, path: P) -> Result { + self._create_dir(path.as_ref()) + } + fn _create_dir(&self, path: &Path) -> Result { + let path = self.path(path); + match fs::create_dir_all(&path) { + Ok(()) => Ok(path), + Err(err) => Err(Error::new_create_dir(err, path)), + } + } + + /// Creates an empty named world-readable temporary directory. + /// + /// Returns a [`TempDir`] RAII guard with the path to the directory. When + /// dropped, the temporary directory and all of its contents will be + /// removed. + /// + /// Note that this is an **insecure method** -- any other process on the + /// system will be able to read the data. + #[doc(alias = "mktemp")] + pub fn create_temp_dir(&self) -> Result { + let base = std::env::temp_dir(); + self.create_dir(&base)?; + + static CNT: AtomicUsize = AtomicUsize::new(0); + + let mut n_try = 0u32; + loop { + let cnt = CNT.fetch_add(1, Ordering::Relaxed); + let path = base.join(format!("xshell-tmp-dir-{}", cnt)); + match fs::create_dir(&path) { + Ok(()) => return Ok(TempDir { path }), + Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)), + Err(_) => n_try += 1, + } + } + } + + /// Removes the file or directory at the given path. + #[doc(alias("rm_rf", "rm"))] + pub fn remove_path>(&self, path: P) -> Result<()> { + self._remove_path(path.as_ref()) + } + fn _remove_path(&self, path: &Path) -> Result<(), Error> { + let path = self.path(path); + match path.metadata() { + Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) } + .map_err(|err| Error::new_remove_path(err, path)), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), + Err(err) => Err(Error::new_remove_path(err, path)), + } + } + + /// Returns whether a file or directory exists at the given path. + #[doc(alias("stat"))] + pub fn path_exists>(&self, path: P) -> bool { + self.path(path.as_ref()).exists() + } + // endregion:fs + + /// Creates a new [`Cmd`] that executes the given `program`. + pub fn cmd>(&self, program: P) -> Cmd<'_> { + // TODO: path lookup? + Cmd::new(self, program.as_ref()) + } + + fn path(&self, p: &Path) -> PathBuf { + let cd = self.cwd.borrow(); + cd.join(p) + } +} + +/// RAII guard returned from [`Shell::push_dir`]. +/// +/// Dropping `PushDir` restores the working directory of the [`Shell`] to the +/// old value. +#[derive(Debug)] +#[must_use] +pub struct PushDir<'a> { + old_cwd: PathBuf, + shell: &'a Shell, +} + +impl<'a> PushDir<'a> { + fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> { + PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), path), shell } + } +} + +impl Drop for PushDir<'_> { + fn drop(&mut self) { + mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd) + } +} + +/// RAII guard returned from [`Shell::push_env`]. +/// +/// Dropping `PushEnv` restores the old value of the environmental variable. +#[derive(Debug)] +#[must_use] +pub struct PushEnv<'a> { + key: OsString, + old_value: Option, + shell: &'a Shell, +} + +impl<'a> PushEnv<'a> { + fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> { + let old_value = shell.env.borrow_mut().insert(key.clone(), val); + PushEnv { shell, key, old_value } + } +} + +impl Drop for PushEnv<'_> { + fn drop(&mut self) { + let mut env = self.shell.env.borrow_mut(); + let key = mem::take(&mut self.key); + match self.old_value.take() { + Some(value) => { + env.insert(key, value); + } + None => { + env.remove(&key); + } + } + } +} + +/// A builder object for constructing a subprocess. +/// +/// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists +/// within a context of a [`Shell`] and uses its working directory and +/// environment. +/// +/// # Example +/// +/// ```no_run +/// use xshell::{Shell, cmd}; +/// +/// let sh = Shell::new()?; +/// +/// let branch = "main"; +/// let cmd = cmd!(sh, "git switch {branch}").quiet().run()?; +/// # Ok::<(), xshell::Error>(()) +/// ``` +#[derive(Debug)] +#[must_use] +pub struct Cmd<'a> { + shell: &'a Shell, + data: CmdData, +} + +#[derive(Debug, Default, Clone)] +struct CmdData { + prog: PathBuf, + args: Vec, + env_changes: Vec, + ignore_status: bool, + quiet: bool, + secret: bool, + stdin_contents: Option>, + ignore_stdout: bool, + ignore_stderr: bool, +} + +// We just store a list of functions to call on the `Command` — the alternative +// would require mirroring the logic that `std::process::Command` (or rather +// `sys_common::CommandEnvs`) uses, which is moderately complex, involves +// special-casing `PATH`, and plausibly could change. +#[derive(Debug, Clone)] +enum EnvChange { + Set(OsString, OsString), + Remove(OsString), + Clear, +} + +impl fmt::Display for Cmd<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.data, f) + } +} + +impl fmt::Display for CmdData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.secret { + return write!(f, ""); + } + + write!(f, "{}", self.prog.display())?; + for arg in &self.args { + // TODO: this is potentially not copy-paste safe. + let arg = arg.to_string_lossy(); + if arg.chars().any(|it| it.is_ascii_whitespace()) { + write!(f, " \"{}\"", arg.escape_default())? + } else { + write!(f, " {}", arg)? + }; + } + Ok(()) + } +} + +impl From> for Command { + fn from(cmd: Cmd<'_>) -> Command { + cmd.to_command() + } +} + +impl<'a> Cmd<'a> { + fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> { + let mut data = CmdData::default(); + data.prog = prog.to_path_buf(); + Cmd { shell, data } + } + + // region:builder + /// Adds an argument to this commands. + pub fn arg>(mut self, arg: P) -> Cmd<'a> { + self._arg(arg.as_ref()); + self + } + fn _arg(&mut self, arg: &OsStr) { + self.data.args.push(arg.to_owned()) + } + + /// Adds all of the arguments to this command. + pub fn args(mut self, args: I) -> Cmd<'a> + where + I: IntoIterator, + I::Item: AsRef, + { + args.into_iter().for_each(|it| self._arg(it.as_ref())); + self + } + + #[doc(hidden)] + pub fn __extend_arg>(mut self, arg_fragment: P) -> Cmd<'a> { + self.___extend_arg(arg_fragment.as_ref()); + self + } + fn ___extend_arg(&mut self, arg_fragment: &OsStr) { + match self.data.args.last_mut() { + Some(last_arg) => last_arg.push(arg_fragment), + None => { + let mut prog = mem::take(&mut self.data.prog).into_os_string(); + prog.push(arg_fragment); + self.data.prog = prog.into(); + } + } + } + + /// Overrides the value of the environmental variable for this command. + pub fn env, V: AsRef>(mut self, key: K, val: V) -> Cmd<'a> { + self._env_set(key.as_ref(), val.as_ref()); + self + } + + fn _env_set(&mut self, key: &OsStr, val: &OsStr) { + self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned())); + } + + /// Overrides the values of specified environmental variables for this + /// command. + pub fn envs(mut self, vars: I) -> Cmd<'a> + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref())); + self + } + + /// Removes the environment variable from this command. + pub fn env_remove>(mut self, key: K) -> Cmd<'a> { + self._env_remove(key.as_ref()); + self + } + fn _env_remove(&mut self, key: &OsStr) { + self.data.env_changes.push(EnvChange::Remove(key.to_owned())); + } + + /// Removes all of the environment variables from this command. + pub fn env_clear(mut self) -> Cmd<'a> { + self.data.env_changes.push(EnvChange::Clear); + self + } + + /// Don't return an error if command the command exits with non-zero status. + /// + /// By default, non-zero exit status is considered an error. + pub fn ignore_status(mut self) -> Cmd<'a> { + self.set_ignore_status(true); + self + } + /// Controls whether non-zero exit status is considered an error. + pub fn set_ignore_status(&mut self, yes: bool) { + self.data.ignore_status = yes; + } + + /// Don't echo the command itself to stderr. + /// + /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`]. + pub fn quiet(mut self) -> Cmd<'a> { + self.set_quiet(true); + self + } + /// Controls whether the command itself is printed to stderr. + pub fn set_quiet(&mut self, yes: bool) { + self.data.quiet = yes; + } + + /// Marks the command as secret. + /// + /// If a command is secret, it echoes `` instead of the program and + /// its arguments, even in error messages. + pub fn secret(mut self) -> Cmd<'a> { + self.set_secret(true); + self + } + /// Controls whether the command is secret. + pub fn set_secret(&mut self, yes: bool) { + self.data.secret = yes; + } + + /// Pass the given slice to the standard input of the spawned process. + pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> { + self._stdin(stdin.as_ref()); + self + } + fn _stdin(&mut self, stdin: &[u8]) { + self.data.stdin_contents = Some(stdin.to_vec()); + } + + /// Ignores the standard output stream of the process. + /// + /// This is equivalent to redirecting stdout to `/dev/null`. By default, the + /// stdout is inherited or captured. + pub fn ignore_stdout(mut self) -> Cmd<'a> { + self.set_ignore_stdout(true); + self + } + /// Controls whether the standard output is ignored. + pub fn set_ignore_stdout(&mut self, yes: bool) { + self.data.ignore_stdout = yes; + } + + /// Ignores the standard output stream of the process. + /// + /// This is equivalent redirecting stderr to `/dev/null`. By default, the + /// stderr is inherited or captured. + pub fn ignore_stderr(mut self) -> Cmd<'a> { + self.set_ignore_stderr(true); + self + } + /// Controls whether the standard error is ignored. + pub fn set_ignore_stderr(&mut self, yes: bool) { + self.data.ignore_stderr = yes; + } + // endregion:builder + + // region:running + /// Runs the command. + /// + /// By default the command itself is echoed to stderr, its standard streams + /// are inherited, and non-zero return code is considered an error. These + /// behaviors can be overridden by using various builder methods of the [`Cmd`]. + pub fn run(&self) -> Result<()> { + if !self.data.quiet { + eprintln!("$ {}", self); + } + let mut command = self.to_command(); + let status = command.status().map_err(|err| Error::new_cmd_io(self, err))?; + self.check_status(status)?; + Ok(()) + } + + /// Run the command and return its stdout as a string. + pub fn read(&self) -> Result { + self.read_stream(false) + } + + /// Run the command and return its stderr as a string. + pub fn read_stderr(&self) -> Result { + self.read_stream(true) + } + + /// Run the command and return its output. + pub fn output(&self) -> Result { + self.output_impl(true, true) + } + // endregion:running + + fn read_stream(&self, read_stderr: bool) -> Result { + let read_stdout = !read_stderr; + let output = self.output_impl(read_stdout, read_stderr)?; + self.check_status(output.status)?; + + let stream = if read_stderr { output.stderr } else { output.stdout }; + let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?; + + if stream.ends_with('\n') { + stream.pop(); + } + if stream.ends_with('\r') { + stream.pop(); + } + + Ok(stream) + } + + fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result { + let mut child = { + let mut command = self.to_command(); + + if !self.data.ignore_stdout { + command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() }); + } + if !self.data.ignore_stderr { + command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() }); + } + + command.stdin(match &self.data.stdin_contents { + Some(_) => Stdio::piped(), + None => Stdio::null(), + }); + + command.spawn().map_err(|err| Error::new_cmd_io(self, err))? + }; + + let mut io_thread = None; + if let Some(stdin_contents) = self.data.stdin_contents.clone() { + let mut stdin = child.stdin.take().unwrap(); + io_thread = Some(std::thread::spawn(move || { + stdin.write_all(&stdin_contents)?; + stdin.flush() + })); + } + let out_res = child.wait_with_output(); + let err_res = io_thread.map(|it| it.join().unwrap()); + let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?; + if let Some(err_res) = err_res { + err_res.map_err(|err| Error::new_cmd_stdin(self, err))?; + } + self.check_status(output.status)?; + Ok(output) + } + + fn to_command(&self) -> Command { + let mut res = Command::new(&self.data.prog); + res.current_dir(self.shell.current_dir()); + res.args(&self.data.args); + + for (key, val) in &*self.shell.env.borrow() { + res.env(key, val); + } + for change in &self.data.env_changes { + match change { + EnvChange::Clear => res.env_clear(), + EnvChange::Remove(key) => res.env_remove(key), + EnvChange::Set(key, val) => res.env(key, val), + }; + } + + if self.data.ignore_stdout { + res.stdout(Stdio::null()); + } + + if self.data.ignore_stderr { + res.stderr(Stdio::null()); + } + + res + } + + fn check_status(&self, status: ExitStatus) -> Result<()> { + if status.success() || self.data.ignore_status { + return Ok(()); + } + Err(Error::new_cmd_status(self, status)) + } +} + +/// A temporary directory. +/// +/// This is a RAII object which will remove the underlying temporary directory +/// when dropped. +#[derive(Debug)] +#[must_use] +pub struct TempDir { + path: PathBuf, +} + +impl TempDir { + /// Returns the path to the underlying temporary directory. + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = remove_dir_all(&self.path); + } +} + +#[cfg(not(windows))] +fn remove_dir_all(path: &Path) -> io::Result<()> { + std::fs::remove_dir_all(path) +} + +#[cfg(windows)] +fn remove_dir_all(path: &Path) -> io::Result<()> { + for _ in 0..99 { + if fs::remove_dir_all(path).is_ok() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(10)) + } + fs::remove_dir_all(path) +} diff --git a/vendor/xshell/tests/compile_time.rs b/vendor/xshell/tests/compile_time.rs new file mode 100644 index 000000000..ca35eefe8 --- /dev/null +++ b/vendor/xshell/tests/compile_time.rs @@ -0,0 +1,41 @@ +use std::time::{Duration, Instant}; + +use xshell::{cmd, Shell}; + +#[test] +fn fixed_cost_compile_times() { + let sh = Shell::new().unwrap(); + + let _p = sh.push_dir("tests/data"); + let baseline = compile_bench(&sh, "baseline"); + let _ducted = compile_bench(&sh, "ducted"); + let xshelled = compile_bench(&sh, "xshelled"); + let ratio = (xshelled.as_millis() as f64) / (baseline.as_millis() as f64); + assert!(1.0 < ratio && ratio < 10.0); + + fn compile_bench(sh: &Shell, name: &str) -> Duration { + let _p = sh.push_dir(name); + let cargo_build = cmd!(sh, "cargo build -q"); + cargo_build.read().unwrap(); + + let n = 5; + let mut times = Vec::new(); + for _ in 0..n { + sh.remove_path("./target").unwrap(); + let start = Instant::now(); + cargo_build.read().unwrap(); + let elapsed = start.elapsed(); + times.push(elapsed); + } + + times.sort(); + times.remove(0); + times.pop(); + let total = times.iter().sum::(); + let average = total / (times.len() as u32); + + eprintln!("compiling {name}: {average:?}"); + + total + } +} diff --git a/vendor/xshell/tests/data/xecho.rs b/vendor/xshell/tests/data/xecho.rs new file mode 100644 index 000000000..ac86433c3 --- /dev/null +++ b/vendor/xshell/tests/data/xecho.rs @@ -0,0 +1,85 @@ +use std::io::{self, Write}; + +fn main() { + if let Err(err) = try_main() { + eprintln!("{err}"); + std::process::exit(1); + } +} + +fn try_main() -> io::Result<()> { + let mut tee_stderr = false; + let mut echo_stdin = false; + let mut echo_env = false; + let mut fail = false; + let mut suicide = false; + + let mut args = std::env::args().skip(1).peekable(); + while let Some(arg) = args.peek() { + match arg.as_str() { + "-e" => tee_stderr = true, + "-i" => echo_stdin = true, + "-$" => echo_env = true, + "-f" => fail = true, + "-s" => suicide = true, + _ => break, + } + args.next(); + } + + let stdin = io::stdin(); + let stdout = io::stdout(); + let stderr = io::stderr(); + let mut stdin = stdin.lock(); + let mut stdout = stdout.lock(); + let mut stderr = stderr.lock(); + macro_rules! w { + ($($tt:tt)*) => { + write!(stdout, $($tt)*)?; + if tee_stderr { + write!(stderr, $($tt)*)?; + } + } + } + + if echo_stdin { + io::copy(&mut stdin, &mut stdout)?; + } else if echo_env { + for key in args { + if let Some(v) = std::env::var_os(&key) { + w!("{}={}\n", key, v.to_string_lossy()); + } + } + } else { + let mut space = ""; + for arg in args { + w!("{}{}", space, arg); + space = " "; + } + w!("\n"); + } + + if fail { + return Err(io::ErrorKind::Other.into()); + } + if suicide { + #[cfg(unix)] + unsafe { + let pid = signals::getpid(); + if pid > 0 { + signals::kill(pid, 9); + } + } + } + + Ok(()) +} + +#[cfg(unix)] +mod signals { + use std::os::raw::c_int; + extern "C" { + pub fn kill(pid: c_int, sig: c_int) -> c_int; + pub fn getpid() -> c_int; + } +} diff --git a/vendor/xshell/tests/it/compile_failures.rs b/vendor/xshell/tests/it/compile_failures.rs new file mode 100644 index 000000000..611af5acb --- /dev/null +++ b/vendor/xshell/tests/it/compile_failures.rs @@ -0,0 +1,128 @@ +use xshell::{cmd, Shell}; + +#[track_caller] +fn check(code: &str, err_msg: &str) { + let sh = Shell::new().unwrap(); + let xshell_dir = sh.current_dir(); + let temp_dir = sh.create_temp_dir().unwrap(); + sh.change_dir(temp_dir.path()); + + let manifest = format!( + r#" +[package] +name = "cftest" +version = "0.0.0" +edition = "2018" +[workspace] + +[lib] +path = "main.rs" + +[dependencies] +xshell = {{ path = {xshell_dir:?} }} +"#, + ); + + let snip = format!( + " +use xshell::*; +pub fn f() {{ + let sh = Shell::new().unwrap(); + {code}; +}} +" + ); + + sh.write_file("Cargo.toml", manifest).unwrap(); + sh.write_file("main.rs", snip).unwrap(); + + let stderr = cmd!(sh, "cargo build").ignore_status().read_stderr().unwrap(); + assert!( + stderr.contains(err_msg), + "\n\nCompile fail fail!\n\nExpected:\n{}\n\nActual:\n{}\n", + err_msg, + stderr + ); +} + +#[test] +fn not_a_string_literal() { + check("cmd!(sh, 92)", "expected a plain string literal"); +} + +#[test] +fn not_raw_string_literal() { + check(r#"cmd!(sh, r"raw")"#, "expected a plain string literal"); +} + +#[test] +fn interpolate_complex_expression() { + check( + r#"cmd!(sh, "{echo.as_str()}")"#, + "error: can only interpolate simple variables, got this expression instead: `echo.as_str()`", + ); +} + +#[test] +fn interpolate_splat_concat_prefix() { + check( + r#"cmd!(sh, "echo a{args...}")"#, + "error: can't combine splat with concatenation, add spaces around `{args...}`", + ); +} + +#[test] +fn interpolate_splat_concat_suffix() { + check( + r#"cmd!(sh, "echo {args...}b")"#, + "error: can't combine splat with concatenation, add spaces around `{args...}`", + ); +} + +#[test] +fn interpolate_splat_concat_mixfix() { + check( + r#"cmd!(sh, "echo a{args...}b")"#, + "error: can't combine splat with concatenation, add spaces around `{args...}`", + ); +} + +#[test] +fn empty_command() { + check(r#"cmd!(sh, "")"#, "error: command can't be empty"); +} + +#[test] +fn spalt_program() { + check(r#"cmd!(sh, "{cmd...}")"#, "error: can't splat program name"); +} + +#[test] +fn unclosed_quote() { + check(r#"cmd!(sh, "echo 'hello world")"#, "error: unclosed `'` in command"); +} + +#[test] +fn unclosed_curly() { + check(r#"cmd!(sh, "echo {hello world")"#, "error: unclosed `{` in command"); +} + +#[test] +fn interpolate_integer() { + check( + r#" + let x = 92; + cmd!(sh, "make -j {x}")"#, + r#"is not implemented"#, + ); +} + +#[test] +fn splat_fn_pointer() { + check( + r#" + let dry_run: fn() -> Option<&'static str> = || None; + cmd!(sh, "make -j {dry_run...}")"#, + r#"is not implemented"#, + ); +} diff --git a/vendor/xshell/tests/it/env.rs b/vendor/xshell/tests/it/env.rs new file mode 100644 index 000000000..949869467 --- /dev/null +++ b/vendor/xshell/tests/it/env.rs @@ -0,0 +1,130 @@ +use std::collections::BTreeMap; + +use xshell::cmd; + +use crate::setup; + +#[test] +fn test_env() { + let sh = setup(); + + let v1 = "xshell_test_123"; + let v2 = "xshell_test_456"; + + assert_env(cmd!(sh, "xecho -$ {v1}").env(v1, "123"), &[(v1, Some("123"))]); + + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}").envs([(v1, "123"), (v2, "456")].iter().copied()), + &[(v1, Some("123")), (v2, Some("456"))], + ); + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}") + .envs([(v1, "123"), (v2, "456")].iter().copied()) + .env_remove(v2), + &[(v1, Some("123")), (v2, None)], + ); + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}") + .envs([(v1, "123"), (v2, "456")].iter().copied()) + .env_remove("nothing"), + &[(v1, Some("123")), (v2, Some("456"))], + ); + + let _g1 = sh.push_env(v1, "foobar"); + let _g2 = sh.push_env(v2, "quark"); + + assert_env(cmd!(sh, "xecho -$ {v1} {v2}"), &[(v1, Some("foobar")), (v2, Some("quark"))]); + + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}").env(v1, "wombo"), + &[(v1, Some("wombo")), (v2, Some("quark"))], + ); + + assert_env(cmd!(sh, "xecho -$ {v1} {v2}").env_remove(v1), &[(v1, None), (v2, Some("quark"))]); + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}").env_remove(v1).env(v1, "baz"), + &[(v1, Some("baz")), (v2, Some("quark"))], + ); + assert_env( + cmd!(sh, "xecho -$ {v1} {v2}").env(v1, "baz").env_remove(v1), + &[(v1, None), (v2, Some("quark"))], + ); +} + +#[test] +fn test_env_clear() { + let sh = setup(); + + let v1 = "xshell_test_123"; + let v2 = "xshell_test_456"; + + let xecho = format!("./target/xecho{}", std::env::consts::EXE_SUFFIX); + + assert_env( + cmd!(sh, "{xecho} -$ {v1} {v2}") + .envs([(v1, "123"), (v2, "456")].iter().copied()) + .env_clear(), + &[(v1, None), (v2, None)], + ); + assert_env( + cmd!(sh, "{xecho} -$ {v1} {v2}") + .envs([(v1, "123"), (v2, "456")].iter().copied()) + .env_clear() + .env(v1, "789"), + &[(v1, Some("789")), (v2, None)], + ); + + let _g1 = sh.push_env(v1, "foobar"); + let _g2 = sh.push_env(v2, "quark"); + + assert_env(cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear(), &[(v1, None), (v2, None)]); + assert_env( + cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear().env(v1, "baz"), + &[(v1, Some("baz")), (v2, None)], + ); + assert_env( + cmd!(sh, "{xecho} -$ {v1} {v2}").env(v1, "baz").env_clear(), + &[(v1, None), (v2, None)], + ); +} + +#[track_caller] +fn assert_env(xecho_env_cmd: xshell::Cmd, want_env: &[(&str, Option<&str>)]) { + let output = xecho_env_cmd.output().unwrap(); + let env = String::from_utf8_lossy(&output.stdout) + .lines() + .filter(|line| !line.is_empty()) + .map(|line| { + let (key, val) = line.split_once('=').unwrap_or_else(|| { + panic!("failed to parse line from `xecho -$` output: {:?}", line) + }); + (key.to_owned(), val.to_owned()) + }) + .collect::>(); + check_env(&env, want_env); +} + +#[track_caller] +fn check_env(env: &BTreeMap, wanted_env: &[(&str, Option<&str>)]) { + let mut failed = false; + let mut seen = env.clone(); + for &(k, val) in wanted_env { + match (seen.remove(k), val) { + (Some(env_v), Some(want_v)) if env_v == want_v => {} + (None, None) => {} + (have, want) => { + eprintln!("mismatch on env var {:?}: have `{:?}`, want `{:?}` ", k, have, want); + failed = true; + } + } + } + for (k, v) in seen { + eprintln!("Unexpected env key {:?} (value: {:?})", k, v); + failed = true; + } + assert!( + !failed, + "env didn't match (see stderr for cleaner output):\nsaw: {:?}\n\nwanted: {:?}", + env, wanted_env, + ); +} diff --git a/vendor/xshell/tests/it/main.rs b/vendor/xshell/tests/it/main.rs new file mode 100644 index 000000000..6ac8fe074 --- /dev/null +++ b/vendor/xshell/tests/it/main.rs @@ -0,0 +1,462 @@ +mod tidy; +mod env; +mod compile_failures; + +use std::{ffi::OsStr, path::Path}; + +use xshell::{cmd, Shell}; + +fn setup() -> Shell { + static ONCE: std::sync::Once = std::sync::Once::new(); + + let sh = Shell::new().unwrap(); + let xecho_src = sh.current_dir().join("./tests/data/xecho.rs"); + let target_dir = sh.current_dir().join("./target/"); + + ONCE.call_once(|| { + cmd!(sh, "rustc {xecho_src} --out-dir {target_dir}") + .quiet() + .run() + .unwrap_or_else(|err| panic!("failed to install binaries from mock_bin: {}", err)) + }); + + sh.set_var("PATH", target_dir); + sh +} + +#[test] +fn smoke() { + let sh = setup(); + + let pwd = "lol"; + let cmd = cmd!(sh, "xecho 'hello '{pwd}"); + println!("{}", cmd); +} + +#[test] +fn into_command() { + let sh = setup(); + let _: std::process::Command = cmd!(sh, "git branch").into(); +} + +#[test] +fn multiline() { + let sh = setup(); + + let output = cmd!( + sh, + " + xecho hello + " + ) + .read() + .unwrap(); + assert_eq!(output, "hello"); +} + +#[test] +fn interpolation() { + let sh = setup(); + + let hello = "hello"; + let output = cmd!(sh, "xecho {hello}").read().unwrap(); + assert_eq!(output, "hello"); +} + +#[test] +fn program_interpolation() { + let sh = setup(); + + let echo = "xecho"; + let output = cmd!(sh, "{echo} hello").read().unwrap(); + assert_eq!(output, "hello"); +} + +#[test] +fn interpolation_concatenation() { + let sh = setup(); + + let hello = "hello"; + let world = "world"; + let output = cmd!(sh, "xecho {hello}-{world}").read().unwrap(); + assert_eq!(output, "hello-world"); +} + +#[test] +fn program_concatenation() { + let sh = setup(); + + let ho = "ho"; + let output = cmd!(sh, "xec{ho} hello").read().unwrap(); + assert_eq!(output, "hello"); +} + +#[test] +fn interpolation_move() { + let sh = setup(); + + let hello = "hello".to_string(); + let output1 = cmd!(sh, "xecho {hello}").read().unwrap(); + let output2 = cmd!(sh, "xecho {hello}").read().unwrap(); + assert_eq!(output1, output2) +} + +#[test] +fn interpolation_spat() { + let sh = setup(); + + let a = &["hello", "world"]; + let b: &[&OsStr] = &[]; + let c = &["!".to_string()]; + let output = cmd!(sh, "xecho {a...} {b...} {c...}").read().unwrap(); + assert_eq!(output, "hello world !") +} + +#[test] +fn splat_option() { + let sh = setup(); + + let a: Option<&OsStr> = None; + let b = Some("hello"); + let output = cmd!(sh, "xecho {a...} {b...}").read().unwrap(); + assert_eq!(output, "hello") +} + +#[test] +fn splat_idiom() { + let sh = setup(); + + let check = if true { &["--", "--check"][..] } else { &[] }; + let cmd = cmd!(sh, "cargo fmt {check...}"); + assert_eq!(cmd.to_string(), "cargo fmt -- --check"); + + let dry_run = if true { Some("--dry-run") } else { None }; + let cmd = cmd!(sh, "cargo publish {dry_run...}"); + assert_eq!(cmd.to_string(), "cargo publish --dry-run"); +} + +#[test] +fn exit_status() { + let sh = setup(); + + let err = cmd!(sh, "xecho -f").read().unwrap_err(); + assert_eq!(err.to_string(), "command exited with non-zero code `xecho -f`: 1"); +} + +#[test] +#[cfg_attr(not(unix), ignore)] +fn exit_status_signal() { + let sh = setup(); + + let err = cmd!(sh, "xecho -s").read().unwrap_err(); + assert_eq!(err.to_string(), "command was terminated by a signal `xecho -s`: 9"); +} + +#[test] +fn ignore_status() { + let sh = setup(); + + let output = cmd!(sh, "xecho -f").ignore_status().read().unwrap(); + assert_eq!(output, ""); +} + +#[test] +fn ignore_status_no_such_command() { + let sh = setup(); + + let err = cmd!(sh, "xecho-f").ignore_status().read().unwrap_err(); + assert_eq!(err.to_string(), "command not found: `xecho-f`"); +} + +#[test] +#[cfg_attr(not(unix), ignore)] +fn ignore_status_signal() { + let sh = setup(); + + let output = cmd!(sh, "xecho -s dead").ignore_status().read().unwrap(); + assert_eq!(output, "dead"); +} + +#[test] +fn read_stderr() { + let sh = setup(); + + let output = cmd!(sh, "xecho -f -e snafu").ignore_status().read_stderr().unwrap(); + assert!(output.contains("snafu")); +} + +#[test] +fn unknown_command() { + let sh = setup(); + + let err = cmd!(sh, "nope no way").read().unwrap_err(); + assert_eq!(err.to_string(), "command not found: `nope`"); +} + +#[test] +fn args_with_spaces() { + let sh = setup(); + + let hello_world = "hello world"; + let cmd = cmd!(sh, "xecho {hello_world} 'hello world' hello world"); + assert_eq!(cmd.to_string(), r#"xecho "hello world" "hello world" hello world"#) +} + +#[test] +fn escape() { + let sh = setup(); + + let output = cmd!(sh, "xecho \\hello\\ '\\world\\'").read().unwrap(); + assert_eq!(output, r#"\hello\ \world\"#) +} + +#[test] +fn stdin_redirection() { + let sh = setup(); + + let lines = "\ +foo +baz +bar +"; + let output = cmd!(sh, "xecho -i").stdin(lines).read().unwrap().replace("\r\n", "\n"); + assert_eq!( + output, + "\ +foo +baz +bar" + ) +} + +#[test] +fn no_deadlock() { + let sh = setup(); + + let mut data = "All the work and now paly made Jack a dull boy.\n".repeat(1 << 20); + data.pop(); + let res = cmd!(sh, "xecho -i").stdin(&data).read().unwrap(); + assert_eq!(data, res); +} + +#[test] +fn test_push_dir() { + let sh = setup(); + + let d1 = sh.current_dir(); + { + let _p = sh.push_dir("xshell-macros"); + let d2 = sh.current_dir(); + assert_eq!(d2, d1.join("xshell-macros")); + { + let _p = sh.push_dir("src"); + let d3 = sh.current_dir(); + assert_eq!(d3, d1.join("xshell-macros/src")); + } + let d4 = sh.current_dir(); + assert_eq!(d4, d1.join("xshell-macros")); + } + let d5 = sh.current_dir(); + assert_eq!(d5, d1); +} + +#[test] +fn test_push_and_change_dir() { + let sh = setup(); + + let d1 = sh.current_dir(); + { + let _p = sh.push_dir("xshell-macros"); + let d2 = sh.current_dir(); + assert_eq!(d2, d1.join("xshell-macros")); + sh.change_dir("src"); + let d3 = sh.current_dir(); + assert_eq!(d3, d1.join("xshell-macros/src")); + } + let d5 = sh.current_dir(); + assert_eq!(d5, d1); +} + +#[test] +fn push_dir_parent_dir() { + let sh = setup(); + + let current = sh.current_dir(); + let dirname = current.file_name().unwrap(); + let _d = sh.push_dir(".."); + let _d = sh.push_dir(dirname); + assert_eq!(sh.current_dir().canonicalize().unwrap(), current.canonicalize().unwrap()); +} + +const VAR: &str = "SPICA"; + +#[test] +fn test_push_env() { + let sh = setup(); + + let e1 = sh.var_os(VAR); + { + let _e = sh.push_env(VAR, "1"); + let e2 = sh.var_os(VAR); + assert_eq!(e2, Some("1".into())); + { + let _e = sh.push_env(VAR, "2"); + let e3 = sh.var_os(VAR); + assert_eq!(e3, Some("2".into())); + } + let e4 = sh.var_os(VAR); + assert_eq!(e4, e2); + } + let e5 = sh.var_os(VAR); + assert_eq!(e5, e1); +} + +#[test] +fn test_push_env_and_set_var() { + let sh = setup(); + + let e1 = sh.var_os(VAR); + { + let _e = sh.push_env(VAR, "1"); + let e2 = sh.var_os(VAR); + assert_eq!(e2, Some("1".into())); + let _e = sh.set_var(VAR, "2"); + let e3 = sh.var_os(VAR); + assert_eq!(e3, Some("2".into())); + } + let e5 = sh.var_os(VAR); + assert_eq!(e5, e1); +} + +#[test] +fn output_with_ignore() { + let sh = setup(); + + let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().output().unwrap(); + assert_eq!(output.stderr, b"hello world!\n"); + assert_eq!(output.stdout, b""); + + let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().output().unwrap(); + assert_eq!(output.stdout, b"hello world!\n"); + assert_eq!(output.stderr, b""); + + let output = + cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().ignore_stderr().output().unwrap(); + assert_eq!(output.stdout, b""); + assert_eq!(output.stderr, b""); +} + +#[test] +fn test_read_with_ignore() { + let sh = setup(); + + let stdout = cmd!(sh, "xecho -e 'hello world'").ignore_stdout().read().unwrap(); + assert!(stdout.is_empty()); + + let stderr = cmd!(sh, "xecho -e 'hello world'").ignore_stderr().read_stderr().unwrap(); + assert!(stderr.is_empty()); + + let stdout = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().read().unwrap(); + assert_eq!(stdout, "hello world!"); + + let stderr = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().read_stderr().unwrap(); + assert_eq!(stderr, "hello world!"); +} + +#[test] +fn test_copy_file() { + let sh = setup(); + + let path; + { + let tempdir = sh.create_temp_dir().unwrap(); + path = tempdir.path().to_path_buf(); + let foo = tempdir.path().join("foo.txt"); + let bar = tempdir.path().join("bar.txt"); + let dir = tempdir.path().join("dir"); + sh.write_file(&foo, "hello world").unwrap(); + sh.create_dir(&dir).unwrap(); + + sh.copy_file(&foo, &bar).unwrap(); + assert_eq!(sh.read_file(&bar).unwrap(), "hello world"); + + sh.copy_file(&foo, &dir).unwrap(); + assert_eq!(sh.read_file(&dir.join("foo.txt")).unwrap(), "hello world"); + assert!(path.exists()); + } + assert!(!path.exists()); +} + +#[test] +fn test_exists() { + let sh = setup(); + let tmp = sh.create_temp_dir().unwrap(); + let _d = sh.change_dir(tmp.path()); + assert!(!sh.path_exists("foo.txt")); + sh.write_file("foo.txt", "foo").unwrap(); + assert!(sh.path_exists("foo.txt")); + assert!(!sh.path_exists("bar")); + sh.create_dir("bar").unwrap(); + assert!(sh.path_exists("bar")); + let _d = sh.change_dir("bar"); + assert!(!sh.path_exists("quz.rs")); + sh.write_file("quz.rs", "fn main () {}").unwrap(); + assert!(sh.path_exists("quz.rs")); + sh.remove_path("quz.rs").unwrap(); + assert!(!sh.path_exists("quz.rs")); +} + +#[test] +fn write_makes_directory() { + let sh = setup(); + + let tempdir = sh.create_temp_dir().unwrap(); + let folder = tempdir.path().join("some/nested/folder/structure"); + sh.write_file(folder.join(".gitinclude"), "").unwrap(); + assert!(folder.exists()); +} + +#[test] +fn test_remove_path() { + let sh = setup(); + + let tempdir = sh.create_temp_dir().unwrap(); + sh.change_dir(tempdir.path()); + sh.write_file(Path::new("a/b/c.rs"), "fn main() {}").unwrap(); + assert!(tempdir.path().join("a/b/c.rs").exists()); + sh.remove_path("./a").unwrap(); + assert!(!tempdir.path().join("a/b/c.rs").exists()); + sh.remove_path("./a").unwrap(); +} + +#[test] +fn recovers_from_panics() { + let sh = setup(); + + let tempdir = sh.create_temp_dir().unwrap(); + let tempdir = tempdir.path().canonicalize().unwrap(); + + let orig = sh.current_dir(); + + std::panic::catch_unwind(|| { + let _p = sh.push_dir(&tempdir); + assert_eq!(sh.current_dir(), tempdir); + std::panic::resume_unwind(Box::new(())); + }) + .unwrap_err(); + + assert_eq!(sh.current_dir(), orig); + { + let _p = sh.push_dir(&tempdir); + assert_eq!(sh.current_dir(), tempdir); + } +} + +#[test] +fn string_escapes() { + let sh = setup(); + + assert_eq!(cmd!(sh, "\"hello\"").to_string(), "\"hello\""); + assert_eq!(cmd!(sh, "\"\"\"asdf\"\"\"").to_string(), r##""""asdf""""##); + assert_eq!(cmd!(sh, "\\\\").to_string(), r#"\\"#); +} diff --git a/vendor/xshell/tests/it/tidy.rs b/vendor/xshell/tests/it/tidy.rs new file mode 100644 index 000000000..2b55b2907 --- /dev/null +++ b/vendor/xshell/tests/it/tidy.rs @@ -0,0 +1,37 @@ +use xshell::{cmd, Shell}; + +#[test] +fn versions_match() { + let sh = Shell::new().unwrap(); + + let read_version = |path: &str| { + let text = sh.read_file(path).unwrap(); + let vers = text.lines().find(|it| it.starts_with("version =")).unwrap(); + let vers = vers.splitn(2, '#').next().unwrap(); + vers.trim_start_matches("version =").trim().trim_matches('"').to_string() + }; + + let v1 = read_version("./Cargo.toml"); + let v2 = read_version("./xshell-macros/Cargo.toml"); + assert_eq!(v1, v2); + + let cargo_toml = sh.read_file("./Cargo.toml").unwrap(); + let dep = format!("xshell-macros = {{ version = \"={}\",", v1); + assert!(cargo_toml.contains(&dep)); +} + +#[test] +fn formatting() { + let sh = Shell::new().unwrap(); + + cmd!(sh, "cargo fmt --all -- --check").run().unwrap() +} + +#[test] +fn current_version_in_changelog() { + let sh = Shell::new().unwrap(); + let _p = sh.push_dir(env!("CARGO_MANIFEST_DIR")); + let changelog = sh.read_file("CHANGELOG.md").unwrap(); + let current_version_header = format!("## {}", env!("CARGO_PKG_VERSION")); + assert_eq!(changelog.lines().filter(|&line| line == current_version_header).count(), 1); +} -- cgit v1.2.3