diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs index cb914561..7c27588c 100644 --- a/crates/bin/src/args.rs +++ b/crates/bin/src/args.rs @@ -172,6 +172,21 @@ pub struct Args { #[clap(help_heading = "Options", long)] pub no_cleanup: bool, + /// By default, binstall keeps track of the installed packages with metadata files + /// stored in the installation root directory. + /// + /// This flag tells binstall not to use or create that file. + /// + /// With this flag, binstall will refuse to overwrite any existing files unless the + /// `--force` flag is used. + /// + /// This also disables binstall’s ability to protect against multiple concurrent + /// invocations of binstall installing at the same time. + /// + /// This flag will also be passed to `cargo-install` if it is invoked. + #[clap(help_heading = "Options", long)] + pub no_track: bool, + /// Install binaries in a custom location. /// /// By default, binaries are installed to the global location `$CARGO_HOME/bin`, and global diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs index 1eb7b61b..5818ae3c 100644 --- a/crates/bin/src/entry.rs +++ b/crates/bin/src/entry.rs @@ -59,7 +59,7 @@ pub fn install_crates( // Compute paths let cargo_root = args.root; let (install_path, mut manifests, temp_dir) = - compute_paths_and_load_manifests(cargo_root.clone(), args.install_path)?; + compute_paths_and_load_manifests(cargo_root.clone(), args.install_path, args.no_track)?; // Remove installed crates let mut crate_names = @@ -101,6 +101,7 @@ pub fn install_crates( force: args.force, quiet: args.log_level == Some(LevelFilter::Off), locked: args.locked, + no_track: args.no_track, version_req: args.version_req, manifest_path: args.manifest_path, @@ -234,6 +235,7 @@ fn read_root_certs(root_certificate_paths: Vec) -> impl Iterator, install_path: Option, + no_track: bool, ) -> Result<(PathBuf, Option, tempfile::TempDir)> { // Compute cargo_roots let cargo_roots = install_path::get_cargo_roots_path(roots).ok_or_else(|| { @@ -251,8 +253,10 @@ fn compute_paths_and_load_manifests( fs::create_dir_all(&install_path).map_err(BinstallError::Io)?; debug!("Using install path: {}", install_path.display()); + let no_manifests = no_track || custom_install_path; + // Load manifests - let manifests = if !custom_install_path { + let manifests = if !no_manifests { Some(Manifests::open_exclusive(&cargo_roots)?) } else { None diff --git a/crates/binstalk/src/bins.rs b/crates/binstalk/src/bins.rs index 0b95ef76..939afb96 100644 --- a/crates/binstalk/src/bins.rs +++ b/crates/binstalk/src/bins.rs @@ -11,7 +11,10 @@ use tracing::debug; use crate::{ errors::BinstallError, - fs::{atomic_install, atomic_symlink_file}, + fs::{ + atomic_install, atomic_install_noclobber, atomic_symlink_file, + atomic_symlink_file_noclobber, + }, helpers::download::ExtractedFiles, manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}, }; @@ -180,28 +183,48 @@ impl BinFile { } } - pub fn install_bin(&self) -> Result<(), BinstallError> { + fn pre_install_bin(&self) -> Result<(), BinstallError> { if !self.source.try_exists()? { return Err(BinstallError::BinFileNotFound(self.source.clone())); } - debug!( - "Atomically install file from '{}' to '{}'", - self.source.display(), - self.dest.display() - ); - #[cfg(unix)] std::fs::set_permissions( &self.source, std::os::unix::fs::PermissionsExt::from_mode(0o755), )?; + Ok(()) + } + + pub fn install_bin(&self) -> Result<(), BinstallError> { + self.pre_install_bin()?; + + debug!( + "Atomically install file from '{}' to '{}'", + self.source.display(), + self.dest.display() + ); + atomic_install(&self.source, &self.dest)?; Ok(()) } + pub fn install_bin_noclobber(&self) -> Result<(), BinstallError> { + self.pre_install_bin()?; + + debug!( + "Installing file from '{}' to '{}' only if dst not exists", + self.source.display(), + self.dest.display() + ); + + atomic_install_noclobber(&self.source, &self.dest)?; + + Ok(()) + } + pub fn install_link(&self) -> Result<(), BinstallError> { if let Some(link) = &self.link { let dest = self.link_dest(); @@ -216,6 +239,20 @@ impl BinFile { Ok(()) } + pub fn install_link_noclobber(&self) -> Result<(), BinstallError> { + if let Some(link) = &self.link { + let dest = self.link_dest(); + debug!( + "Create link '{}' pointing to '{}' only if dst not exists", + link.display(), + dest.display() + ); + atomic_symlink_file_noclobber(dest, link)?; + } + + Ok(()) + } + fn link_dest(&self) -> &Path { if cfg!(target_family = "unix") { Path::new(self.dest.file_name().unwrap()) diff --git a/crates/binstalk/src/fs.rs b/crates/binstalk/src/fs.rs index abf5c959..7681c569 100644 --- a/crates/binstalk/src/fs.rs +++ b/crates/binstalk/src/fs.rs @@ -3,6 +3,54 @@ use std::{fs, io, path::Path}; use tempfile::{NamedTempFile, TempPath}; use tracing::{debug, warn}; +fn copy_to_tempfile(src: &Path, dst: &Path) -> io::Result { + let mut src_file = fs::File::open(src)?; + + let parent = dst.parent().unwrap(); + debug!("Creating named tempfile at '{}'", parent.display()); + let mut tempfile = NamedTempFile::new_in(parent)?; + + debug!( + "Copying from '{}' to '{}'", + src.display(), + tempfile.path().display() + ); + io::copy(&mut src_file, tempfile.as_file_mut())?; + + debug!("Retrieving permissions of '{}'", src.display()); + let permissions = src_file.metadata()?.permissions(); + + debug!( + "Setting permissions of '{}' to '{permissions:#?}'", + tempfile.path().display() + ); + tempfile.as_file().set_permissions(permissions)?; + + Ok(tempfile) +} + +/// Install a file. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_install_noclobber(src: &Path, dst: &Path) -> io::Result<()> { + debug!( + "Attempting to rename from '{}' to '{}'.", + src.display(), + dst.display() + ); + + let tempfile = copy_to_tempfile(src, dst)?; + + debug!( + "Persisting '{}' to '{}', fail if dst already exists", + tempfile.path().display(), + dst.display() + ); + tempfile.persist_noclobber(dst)?; + + Ok(()) +} + /// Atomically install a file. /// /// This is a blocking function, must be called in `block_in_place` mode. @@ -33,29 +81,7 @@ pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> { // Fallback to creating NamedTempFile on the parent dir of // dst. - let mut src_file = fs::File::open(src)?; - - let parent = dst.parent().unwrap(); - debug!("Creating named tempfile at '{}'", parent.display()); - let mut tempfile = NamedTempFile::new_in(parent)?; - - debug!( - "Copying from '{}' to '{}'", - src.display(), - tempfile.path().display() - ); - io::copy(&mut src_file, tempfile.as_file_mut())?; - - debug!("Retrieving permissions of '{}'", src.display()); - let permissions = src_file.metadata()?.permissions(); - - debug!( - "Setting permissions of '{}' to '{permissions:#?}'", - tempfile.path().display() - ); - tempfile.as_file().set_permissions(permissions)?; - - persist(tempfile.into_temp_path(), dst)?; + persist(copy_to_tempfile(src, dst)?.into_temp_path(), dst)?; } else { debug!("Attempting at atomically succeeded."); } @@ -63,17 +89,30 @@ pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> { Ok(()) } -fn symlink_file(original: &Path, link: &Path) -> io::Result<()> { +fn symlink_file_inner(dest: &Path, link: &Path) -> io::Result<()> { #[cfg(target_family = "unix")] - std::os::unix::fs::symlink(original, link)?; + std::os::unix::fs::symlink(dest, link)?; - // Symlinks on Windows are disabled in some editions, so creating one is unreliable. #[cfg(target_family = "windows")] - std::os::windows::fs::symlink_file(original, link) - .or_else(|_| std::fs::copy(original, link).map(drop))?; + std::os::windows::fs::symlink_file(dest, link)?; + Ok(()) } +pub fn atomic_symlink_file_noclobber(dest: &Path, link: &Path) -> io::Result<()> { + match symlink_file_inner(dest, link) { + Ok(_) => Ok(()), + + #[cfg(target_family = "windows")] + // Symlinks on Windows are disabled in some editions, so creating one is unreliable. + // Fallback to copy if it fails. + Err(_) => atomic_install_noclobber(dest, link), + + #[cfg(not(target_family = "windows"))] + Err(err) => Err(err), + } +} + /// Atomically install symlink "link" to a file "dst". /// /// This is a blocking function, must be called in `block_in_place` mode. @@ -82,6 +121,8 @@ pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { debug!("Creating tempPath at '{}'", parent.display()); let temp_path = NamedTempFile::new_in(parent)?.into_temp_path(); + // Remove this file so that we can create a symlink + // with the name. fs::remove_file(&temp_path)?; debug!( @@ -89,9 +130,18 @@ pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { temp_path.display(), dest.display() ); - symlink_file(dest, &temp_path)?; - persist(temp_path, link) + match symlink_file_inner(dest, &temp_path) { + Ok(_) => persist(temp_path, link), + + #[cfg(target_family = "windows")] + // Symlinks on Windows are disabled in some editions, so creating one is unreliable. + // Fallback to copy if it fails. + Err(_) => atomic_install(dest, link), + + #[cfg(not(target_family = "windows"))] + Err(err) => Err(err), + } } fn persist(temp_path: TempPath, to: &Path) -> io::Result<()> { diff --git a/crates/binstalk/src/ops.rs b/crates/binstalk/src/ops.rs index 67b33a38..58affa67 100644 --- a/crates/binstalk/src/ops.rs +++ b/crates/binstalk/src/ops.rs @@ -25,6 +25,7 @@ pub struct Options { pub force: bool, pub quiet: bool, pub locked: bool, + pub no_track: bool, pub version_req: Option, pub manifest_path: Option, diff --git a/crates/binstalk/src/ops/resolve/resolution.rs b/crates/binstalk/src/ops/resolve/resolution.rs index 1a0b2ea6..968c7403 100644 --- a/crates/binstalk/src/ops/resolve/resolution.rs +++ b/crates/binstalk/src/ops/resolve/resolution.rs @@ -51,15 +51,26 @@ impl Resolution { impl ResolutionFetch { pub fn install(self, opts: &Options) -> Result { + type InstallFp = fn(&bins::BinFile) -> Result<(), BinstallError>; + + let (install_bin, install_link): (InstallFp, InstallFp) = match (opts.no_track, opts.force) + { + (true, true) | (false, _) => (bins::BinFile::install_bin, bins::BinFile::install_link), + (true, false) => ( + bins::BinFile::install_bin_noclobber, + bins::BinFile::install_link_noclobber, + ), + }; + info!("Installing binaries..."); for file in &self.bin_files { - file.install_bin()?; + install_bin(file)?; } // Generate symlinks if !opts.no_symlinks { for file in &self.bin_files { - file.install_link()?; + install_link(file)?; } } @@ -158,6 +169,10 @@ impl ResolutionSource { cmd.arg("--root").arg(cargo_root); } + if opts.no_track { + cmd.arg("--no-track"); + } + if !opts.dry_run { let mut child = opts .jobserver_client diff --git a/e2e-tests/no-track.sh b/e2e-tests/no-track.sh new file mode 100644 index 00000000..18cc91e2 --- /dev/null +++ b/e2e-tests/no-track.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -euxo pipefail + +unset CARGO_INSTALL_ROOT + +CARGO_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'cargo-home') +export CARGO_HOME +export PATH="$CARGO_HOME/bin:$PATH" + +"./$1" binstall -y cargo-binstall@0.20.1 +cargo-binstall --help >/dev/null + +set +e + +"./$1" binstall -y --no-track cargo-binstall@0.20.1 +exit_code="$?" + +set -e + +if [ "$exit_code" != 74 ]; then + echo "Expected exit code 74 Io Error, but actual exit code $exit_code" + exit 1 +fi + + +"./$1" binstall -y --no-track --force cargo-binstall@0.20.1 +cargo-binstall --help >/dev/null diff --git a/justfile b/justfile index 383ddf4f..3a1d3f06 100644 --- a/justfile +++ b/justfile @@ -202,6 +202,7 @@ e2e-test-version-syntax: (e2e-test "version-syntax") e2e-test-upgrade: (e2e-test "upgrade") e2e-test-self-upgrade-no-symlink: (e2e-test "self-upgrade-no-symlink") e2e-test-uninstall: (e2e-test "uninstall") +e2e-test-no-track: (e2e-test "no-track") # WinTLS (Windows in CI) does not have TLS 1.3 support [windows] @@ -210,7 +211,7 @@ e2e-test-tls: (e2e-test "tls" "1.2") [macos] e2e-test-tls: (e2e-test "tls" "1.2") (e2e-test "tls" "1.3") -e2e-tests: e2e-test-live e2e-test-manifest-path e2e-test-other-repos e2e-test-strategies e2e-test-version-syntax e2e-test-upgrade e2e-test-tls e2e-test-self-upgrade-no-symlink e2e-test-uninstall e2e-test-subcrate +e2e-tests: e2e-test-live e2e-test-manifest-path e2e-test-other-repos e2e-test-strategies e2e-test-version-syntax e2e-test-upgrade e2e-test-tls e2e-test-self-upgrade-no-symlink e2e-test-uninstall e2e-test-subcrate e2e-test-no-track unit-tests: print-env {{cargo-bin}} test {{cargo-build-args}}