Skip to content

Commit

Permalink
Merge pull request #1755 from charlespierce/no_symlinks_windows
Browse files Browse the repository at this point in the history
Replace Windows symlinks with `volta run` scripts
  • Loading branch information
chriskrycho authored Jul 9, 2024
2 parents 8c4fd74 + 9f91be3 commit 0e0ef61
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 69 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/volta-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ fs2 = "0.4.3"

[target.'cfg(windows)'.dependencies]
winreg = "0.52.0"
junction = "1.1.0"
2 changes: 1 addition & 1 deletion crates/volta-core/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ where
D: AsRef<Path>,
{
#[cfg(windows)]
return std::os::windows::fs::symlink_dir(src, dest);
return junction::create(src, dest);

#[cfg(unix)]
return std::os::unix::fs::symlink(src, dest);
Expand Down
2 changes: 1 addition & 1 deletion crates/volta-core/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::error::{Context, ErrorKind, Fallible};
use cfg_if::cfg_if;
use dunce::canonicalize;
use once_cell::sync::OnceCell;
use volta_layout::v3::{VoltaHome, VoltaInstall};
use volta_layout::v4::{VoltaHome, VoltaInstall};

cfg_if! {
if #[cfg(unix)] {
Expand Down
166 changes: 108 additions & 58 deletions crates/volta-core/src/shim.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
//! Provides utilities for modifying shims for 3rd-party executables
use std::collections::HashSet;
use std::fs::{self, DirEntry, Metadata};
use std::fs;
use std::io;
use std::path::Path;

use crate::error::{Context, ErrorKind, Fallible, VoltaError};
use crate::fs::{read_dir_eager, symlink_file};
use crate::layout::{volta_home, volta_install};
use crate::fs::read_dir_eager;
use crate::layout::volta_home;
use crate::sync::VoltaLock;
use log::debug;

pub use platform::create;

pub fn regenerate_shims_for_dir(dir: &Path) -> Fallible<()> {
// Acquire a lock on the Volta directory, if possible, to prevent concurrent changes
let _lock = VoltaLock::acquire();
Expand All @@ -30,7 +32,8 @@ fn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>> {

#[cfg(unix)]
{
let mut shims: HashSet<String> = contents.filter_map(entry_to_shim_name).collect();
let mut shims: HashSet<String> =
contents.filter_map(platform::entry_to_shim_name).collect();
shims.insert("node".into());
shims.insert("npm".into());
shims.insert("npx".into());
Expand All @@ -43,19 +46,7 @@ fn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>> {
#[cfg(windows)]
{
// On Windows, the default shims are installed in Program Files, so we don't need to generate them here
Ok(contents.filter_map(entry_to_shim_name).collect())
}
}

fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {
if metadata.file_type().is_symlink() {
entry
.path()
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| stem.to_string())
} else {
None
Ok(contents.filter_map(platform::entry_to_shim_name).collect())
}
}

Expand All @@ -67,35 +58,11 @@ pub enum ShimResult {
DoesntExist,
}

pub fn create(shim_name: &str) -> Fallible<ShimResult> {
let executable = volta_install()?.shim_executable();
let shim = volta_home()?.shim_file(shim_name);

#[cfg(windows)]
windows::create_git_bash_script(shim_name)?;

match symlink_file(executable, shim) {
Ok(_) => Ok(ShimResult::Created),
Err(err) => {
if err.kind() == io::ErrorKind::AlreadyExists {
Ok(ShimResult::AlreadyExists)
} else {
Err(VoltaError::from_source(
err,
ErrorKind::ShimCreateError {
name: shim_name.to_string(),
},
))
}
}
}
}

pub fn delete(shim_name: &str) -> Fallible<ShimResult> {
let shim = volta_home()?.shim_file(shim_name);

#[cfg(windows)]
windows::delete_git_bash_script(shim_name)?;
platform::delete_git_bash_script(shim_name)?;

match fs::remove_file(shim) {
Ok(_) => Ok(ShimResult::Deleted),
Expand All @@ -114,28 +81,111 @@ pub fn delete(shim_name: &str) -> Fallible<ShimResult> {
}
}

/// These methods are a (hacky) workaround for an issue with Git Bash on Windows
/// When executing the shim symlink, Git Bash resolves the symlink first and then calls shim.exe directly
/// This results in the shim being unable to determine which tool is being executed
/// However, both cmd.exe and PowerShell execute the symlink correctly
/// To fix the issue specifically in Git Bash, we write a bash script in the shim dir, with the same name as the shim
/// minus the '.exe' (e.g. we write `ember` next to the symlink `ember.exe`)
/// Since the file doesn't have a file extension, it is ignored by cmd.exe and PowerShell, but is detected by Bash
/// This bash script simply calls the shim using `cmd.exe`, so that it is resolved correctly
#[cfg(unix)]
mod platform {
//! Unix-specific shim utilities
//!
//! On macOS and Linux, creating a shim involves creating a symlink to the `volta-shim`
//! executable. Additionally, filtering the shims from directory entries means looking
//! for symlinks and ignoring the actual binaries
use std::ffi::OsStr;
use std::fs::{DirEntry, Metadata};
use std::io;

use super::ShimResult;
use crate::error::{ErrorKind, Fallible, VoltaError};
use crate::fs::symlink_file;
use crate::layout::{volta_home, volta_install};

pub fn create(shim_name: &str) -> Fallible<ShimResult> {
let executable = volta_install()?.shim_executable();
let shim = volta_home()?.shim_file(shim_name);

match symlink_file(executable, shim) {
Ok(_) => Ok(ShimResult::Created),
Err(err) => {
if err.kind() == io::ErrorKind::AlreadyExists {
Ok(ShimResult::AlreadyExists)
} else {
Err(VoltaError::from_source(
err,
ErrorKind::ShimCreateError {
name: shim_name.to_string(),
},
))
}
}
}
}

pub fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {
if metadata.file_type().is_symlink() {
entry
.path()
.file_stem()
.and_then(OsStr::to_str)
.map(ToOwned::to_owned)
} else {
None
}
}
}

#[cfg(windows)]
mod windows {
mod platform {
//! Windows-specific shim utilities
//!
//! On Windows, creating a shim involves creating a small .cmd script, rather than a symlink.
//! This allows us to create shims without requiring administrator privileges or developer
//! mode. Also, to support Git Bash, we create a similar script with bash syntax that doesn't
//! have a file extension. This allows Powershell and Cmd to ignore it, while Bash detects it
//! as an executable script.
//!
//! Finally, filtering directory entries to find the shim files involves looking for the .cmd
//! files.
use std::ffi::OsStr;
use std::fs::{write, DirEntry, Metadata};

use super::ShimResult;
use crate::error::{Context, ErrorKind, Fallible};
use crate::fs::remove_file_if_exists;
use crate::layout::volta_home;
use std::fs::write;

const BASH_SCRIPT: &str = r#"cmd //C $0 "$@""#;
const SHIM_SCRIPT_CONTENTS: &str = r#"@echo off
volta run %~n0 %*
"#;

pub fn create_git_bash_script(shim_name: &str) -> Fallible<()> {
let script_path = volta_home()?.shim_git_bash_script_file(shim_name);
write(script_path, BASH_SCRIPT).with_context(|| ErrorKind::ShimCreateError {
name: shim_name.to_string(),
})
const GIT_BASH_SCRIPT_CONTENTS: &str = r#"#!/bin/bash
volta run "$(basename $0)" "$@""#;

pub fn create(shim_name: &str) -> Fallible<ShimResult> {
let shim = volta_home()?.shim_file(shim_name);

write(shim, SHIM_SCRIPT_CONTENTS).with_context(|| ErrorKind::ShimCreateError {
name: shim_name.to_owned(),
})?;

let git_bash_script = volta_home()?.shim_git_bash_script_file(shim_name);

write(git_bash_script, GIT_BASH_SCRIPT_CONTENTS).with_context(|| {
ErrorKind::ShimCreateError {
name: shim_name.to_owned(),
}
})?;

Ok(ShimResult::Created)
}

pub fn entry_to_shim_name((entry, _): (DirEntry, Metadata)) -> Option<String> {
let path = entry.path();

if path.extension().is_some_and(|ext| ext == "cmd") {
path.file_stem()
.and_then(OsStr::to_str)
.map(ToOwned::to_owned)
} else {
None
}
}

pub fn delete_git_bash_script(shim_name: &str) -> Fallible<()> {
Expand Down
1 change: 1 addition & 0 deletions crates/volta-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod v0;
pub mod v1;
pub mod v2;
pub mod v3;
pub mod v4;

fn executable(name: &str) -> String {
format!("{}{}", name, std::env::consts::EXE_SUFFIX)
Expand Down
Loading

0 comments on commit 0e0ef61

Please sign in to comment.