diff options
| -rw-r--r-- | crates/shirabe/src/util/filesystem.rs | 87 | ||||
| -rwxr-xr-x | scripts/bench/install.sh | 57 |
2 files changed, 114 insertions, 30 deletions
diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs index bca18771..05146fd3 100644 --- a/crates/shirabe/src/util/filesystem.rs +++ b/crates/shirabe/src/util/filesystem.rs @@ -116,9 +116,9 @@ impl Filesystem { /// Recursively remove a directory pub fn remove_directory(&mut self, directory: impl AsRef<Path>) -> anyhow::Result<bool> { // TODO(bytes): - // This path is matched against a regex (remove_edge_cases) and passed to an - // `rm -rf`/`rmdir` subprocess via the String-based ProcessExecutor, so it has to be - // representable as UTF-8. + // This path is matched against a regex (remove_edge_cases) and, on Windows, passed to an + // `rmdir` subprocess via the String-based ProcessExecutor, so it has to be representable as + // UTF-8. let directory = directory.as_ref(); let directory = directory.to_str().ok_or_else(|| { RuntimeException::new(format!( @@ -131,24 +131,25 @@ impl Filesystem { return Ok(r); } - let cmd: Vec<String> = if Platform::is_windows() { - vec![ + let result = if Platform::is_windows() { + // TODO(perf,windows): `std::fs::remove_dir_all` is expected to be faster here too, + // but how it differs from the `rmdir` command has to be investigated first. + let cmd: Vec<String> = vec![ "rmdir".to_string(), "/S".to_string(), "/Q".to_string(), Platform::realpath(directory), - ] + ]; + + let mut output = PhpMixed::Null; + self.get_process() + .execute(&cmd, &mut output, None) + .map(|n| n == 0) + .unwrap_or(false) } else { - vec!["rm".to_string(), "-rf".to_string(), directory.to_string()] + remove_directory_recursively(Path::new(directory)).is_ok() }; - let mut output = PhpMixed::Null; - let result = self - .get_process() - .execute(&cmd, &mut output, None) - .map(|n| n == 0) - .unwrap_or(false); - // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache2(false, ""); @@ -164,7 +165,7 @@ impl Filesystem { this: &std::rc::Rc<std::cell::RefCell<Filesystem>>, directory: &str, ) -> anyhow::Result<bool> { - let (process_executor, cmd) = { + { let mut fs = this.borrow_mut(); if let Some(mock) = fs.mock.as_mut() @@ -179,28 +180,33 @@ impl Filesystem { if let Some(r) = edge_case_result { return Ok(r); } + } - let cmd: Vec<String> = if Platform::is_windows() { - vec![ - "rmdir".to_string(), - "/S".to_string(), - "/Q".to_string(), - Platform::realpath(directory), - ] - } else { - vec!["rm".to_string(), "-rf".to_string(), directory.to_string()] - }; + let result = if Platform::is_windows() { + // TODO(perf,windows): `std::fs::remove_dir_all` is expected to be faster here too, + // but how it differs from the `rmdir` command has to be investigated first. + let cmd: Vec<String> = vec![ + "rmdir".to_string(), + "/S".to_string(), + "/Q".to_string(), + Platform::realpath(directory), + ]; - (fs.get_process_handle(), cmd) - }; + let process_executor = this.borrow_mut().get_process_handle(); + let process_future = process_executor.borrow_mut().execute_async(&cmd, None); + let mut process = process_future.await?; - let process_future = process_executor.borrow_mut().execute_async(&cmd, None); - let mut process = process_future.await?; + process.is_successful() + } else { + remove_directory_recursively_async(Path::new(directory)) + .await + .is_ok() + }; // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache2(false, ""); - if process.is_successful() && !is_dir(directory) { + if result && !is_dir(directory) { return Ok(true); } @@ -1084,3 +1090,24 @@ impl Filesystem { result } } + +/// Removes `path` and everything below it, standing in for the `rm -rf` that Composer shells out +/// to on non-Windows platforms. Symbolic links below `path` are unlinked rather than followed, and +/// a `path` that is already gone counts as removed. +/// +/// Unlike `rm -rf`, this stops at the first entry it cannot remove instead of removing the rest of +/// the tree first. Callers fall back to `Filesystem::remove_directory_php` on failure, which walks +/// the remainder and surfaces the error. +fn remove_directory_recursively(path: &Path) -> std::io::Result<()> { + match std::fs::remove_dir_all(path) { + Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e), + _ => Ok(()), + } +} + +async fn remove_directory_recursively_async(path: &Path) -> std::io::Result<()> { + match tokio::fs::remove_dir_all(path).await { + Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e), + _ => Ok(()), + } +} diff --git a/scripts/bench/install.sh b/scripts/bench/install.sh new file mode 100755 index 00000000..ef26d273 --- /dev/null +++ b/scripts/bench/install.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)" +BIN="$REPO_ROOT/target/release/shirabe" +COMPOSER_BIN="$REPO_ROOT/composer/bin/composer" + +source "$REPO_ROOT/scripts/bench/lib.sh" + +PACKAGE="laravel/laravel" +for arg in "$@"; do + case "$arg" in + --package=*) PACKAGE="${arg#*=}" ;; + *) echo "unknown option: $arg" >&2; exit 2 ;; + esac +done + +if ! command -v hyperfine >/dev/null 2>&1; then + echo "hyperfine not found; install it first (e.g. 'cargo install hyperfine')" >&2 + exit 1 +fi + +cargo build --manifest-path "$REPO_ROOT/Cargo.toml" --release --bin shirabe + +apply_http3_workaround + +OUTDIR="${TMPDIR:-/tmp}/shirabe-bench-install-$$" +mkdir -p "$OUTDIR" +cd "$OUTDIR" + +TARGET_DIR="project-under-test" +PACKAGE_SLUG="${PACKAGE//\//-}" + +# Resolve the dependencies once. What is measured is installing them, not solving them. +"$COMPOSER_BIN" create-project --no-plugins --no-scripts --no-audit --no-interaction \ + "$PACKAGE" "$OUTDIR/template" >/dev/null + +PREPARE_SCRIPT="$OUTDIR/prepare.sh" +cat > "$PREPARE_SCRIPT" <<EOF +for dir in '$TARGET_DIR-shirabe' '$TARGET_DIR-composer'; do + rm -rf "\$dir" + mkdir -p "\$dir" + cp '$OUTDIR/template/composer.json' '$OUTDIR/template/composer.lock' "\$dir/" +done +EOF + +hyperfine \ + --warmup 1 \ + --prepare "bash '$PREPARE_SCRIPT'" \ + --export-json "$OUTDIR/results-$PACKAGE_SLUG.json" \ + --export-markdown "$OUTDIR/results-$PACKAGE_SLUG.md" \ + --command-name Shirabe "RUST_BACKTRACE=1 '$BIN' install --no-plugins --no-scripts --no-interaction --working-dir='$TARGET_DIR-shirabe'" \ + --command-name Composer "'$COMPOSER_BIN' install --no-plugins --no-scripts --no-interaction --working-dir='$TARGET_DIR-composer'" + +echo ">> results: $OUTDIR/results-$PACKAGE_SLUG.json" >&2 +echo ">> $OUTDIR/results-$PACKAGE_SLUG.md" >&2 |
