Skip to content

Commit 0e0ef61

Browse files
authored
Merge pull request #1755 from charlespierce/no_symlinks_windows
Replace Windows symlinks with `volta run` scripts
2 parents 8c4fd74 + 9f91be3 commit 0e0ef61

File tree

10 files changed

+418
-69
lines changed

10 files changed

+418
-69
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/volta-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ fs2 = "0.4.3"
5555

5656
[target.'cfg(windows)'.dependencies]
5757
winreg = "0.52.0"
58+
junction = "1.1.0"

crates/volta-core/src/fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ where
138138
D: AsRef<Path>,
139139
{
140140
#[cfg(windows)]
141-
return std::os::windows::fs::symlink_dir(src, dest);
141+
return junction::create(src, dest);
142142

143143
#[cfg(unix)]
144144
return std::os::unix::fs::symlink(src, dest);

crates/volta-core/src/layout/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::error::{Context, ErrorKind, Fallible};
55
use cfg_if::cfg_if;
66
use dunce::canonicalize;
77
use once_cell::sync::OnceCell;
8-
use volta_layout::v3::{VoltaHome, VoltaInstall};
8+
use volta_layout::v4::{VoltaHome, VoltaInstall};
99

1010
cfg_if! {
1111
if #[cfg(unix)] {

crates/volta-core/src/shim.rs

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
//! Provides utilities for modifying shims for 3rd-party executables
22
33
use std::collections::HashSet;
4-
use std::fs::{self, DirEntry, Metadata};
4+
use std::fs;
55
use std::io;
66
use std::path::Path;
77

88
use crate::error::{Context, ErrorKind, Fallible, VoltaError};
9-
use crate::fs::{read_dir_eager, symlink_file};
10-
use crate::layout::{volta_home, volta_install};
9+
use crate::fs::read_dir_eager;
10+
use crate::layout::volta_home;
1111
use crate::sync::VoltaLock;
1212
use log::debug;
1313

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

3133
#[cfg(unix)]
3234
{
33-
let mut shims: HashSet<String> = contents.filter_map(entry_to_shim_name).collect();
35+
let mut shims: HashSet<String> =
36+
contents.filter_map(platform::entry_to_shim_name).collect();
3437
shims.insert("node".into());
3538
shims.insert("npm".into());
3639
shims.insert("npx".into());
@@ -43,19 +46,7 @@ fn get_shim_list_deduped(dir: &Path) -> Fallible<HashSet<String>> {
4346
#[cfg(windows)]
4447
{
4548
// On Windows, the default shims are installed in Program Files, so we don't need to generate them here
46-
Ok(contents.filter_map(entry_to_shim_name).collect())
47-
}
48-
}
49-
50-
fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {
51-
if metadata.file_type().is_symlink() {
52-
entry
53-
.path()
54-
.file_stem()
55-
.and_then(|stem| stem.to_str())
56-
.map(|stem| stem.to_string())
57-
} else {
58-
None
49+
Ok(contents.filter_map(platform::entry_to_shim_name).collect())
5950
}
6051
}
6152

@@ -67,35 +58,11 @@ pub enum ShimResult {
6758
DoesntExist,
6859
}
6960

70-
pub fn create(shim_name: &str) -> Fallible<ShimResult> {
71-
let executable = volta_install()?.shim_executable();
72-
let shim = volta_home()?.shim_file(shim_name);
73-
74-
#[cfg(windows)]
75-
windows::create_git_bash_script(shim_name)?;
76-
77-
match symlink_file(executable, shim) {
78-
Ok(_) => Ok(ShimResult::Created),
79-
Err(err) => {
80-
if err.kind() == io::ErrorKind::AlreadyExists {
81-
Ok(ShimResult::AlreadyExists)
82-
} else {
83-
Err(VoltaError::from_source(
84-
err,
85-
ErrorKind::ShimCreateError {
86-
name: shim_name.to_string(),
87-
},
88-
))
89-
}
90-
}
91-
}
92-
}
93-
9461
pub fn delete(shim_name: &str) -> Fallible<ShimResult> {
9562
let shim = volta_home()?.shim_file(shim_name);
9663

9764
#[cfg(windows)]
98-
windows::delete_git_bash_script(shim_name)?;
65+
platform::delete_git_bash_script(shim_name)?;
9966

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

117-
/// These methods are a (hacky) workaround for an issue with Git Bash on Windows
118-
/// When executing the shim symlink, Git Bash resolves the symlink first and then calls shim.exe directly
119-
/// This results in the shim being unable to determine which tool is being executed
120-
/// However, both cmd.exe and PowerShell execute the symlink correctly
121-
/// To fix the issue specifically in Git Bash, we write a bash script in the shim dir, with the same name as the shim
122-
/// minus the '.exe' (e.g. we write `ember` next to the symlink `ember.exe`)
123-
/// Since the file doesn't have a file extension, it is ignored by cmd.exe and PowerShell, but is detected by Bash
124-
/// This bash script simply calls the shim using `cmd.exe`, so that it is resolved correctly
84+
#[cfg(unix)]
85+
mod platform {
86+
//! Unix-specific shim utilities
87+
//!
88+
//! On macOS and Linux, creating a shim involves creating a symlink to the `volta-shim`
89+
//! executable. Additionally, filtering the shims from directory entries means looking
90+
//! for symlinks and ignoring the actual binaries
91+
use std::ffi::OsStr;
92+
use std::fs::{DirEntry, Metadata};
93+
use std::io;
94+
95+
use super::ShimResult;
96+
use crate::error::{ErrorKind, Fallible, VoltaError};
97+
use crate::fs::symlink_file;
98+
use crate::layout::{volta_home, volta_install};
99+
100+
pub fn create(shim_name: &str) -> Fallible<ShimResult> {
101+
let executable = volta_install()?.shim_executable();
102+
let shim = volta_home()?.shim_file(shim_name);
103+
104+
match symlink_file(executable, shim) {
105+
Ok(_) => Ok(ShimResult::Created),
106+
Err(err) => {
107+
if err.kind() == io::ErrorKind::AlreadyExists {
108+
Ok(ShimResult::AlreadyExists)
109+
} else {
110+
Err(VoltaError::from_source(
111+
err,
112+
ErrorKind::ShimCreateError {
113+
name: shim_name.to_string(),
114+
},
115+
))
116+
}
117+
}
118+
}
119+
}
120+
121+
pub fn entry_to_shim_name((entry, metadata): (DirEntry, Metadata)) -> Option<String> {
122+
if metadata.file_type().is_symlink() {
123+
entry
124+
.path()
125+
.file_stem()
126+
.and_then(OsStr::to_str)
127+
.map(ToOwned::to_owned)
128+
} else {
129+
None
130+
}
131+
}
132+
}
133+
125134
#[cfg(windows)]
126-
mod windows {
135+
mod platform {
136+
//! Windows-specific shim utilities
137+
//!
138+
//! On Windows, creating a shim involves creating a small .cmd script, rather than a symlink.
139+
//! This allows us to create shims without requiring administrator privileges or developer
140+
//! mode. Also, to support Git Bash, we create a similar script with bash syntax that doesn't
141+
//! have a file extension. This allows Powershell and Cmd to ignore it, while Bash detects it
142+
//! as an executable script.
143+
//!
144+
//! Finally, filtering directory entries to find the shim files involves looking for the .cmd
145+
//! files.
146+
use std::ffi::OsStr;
147+
use std::fs::{write, DirEntry, Metadata};
148+
149+
use super::ShimResult;
127150
use crate::error::{Context, ErrorKind, Fallible};
128151
use crate::fs::remove_file_if_exists;
129152
use crate::layout::volta_home;
130-
use std::fs::write;
131153

132-
const BASH_SCRIPT: &str = r#"cmd //C $0 "$@""#;
154+
const SHIM_SCRIPT_CONTENTS: &str = r#"@echo off
155+
volta run %~n0 %*
156+
"#;
133157

134-
pub fn create_git_bash_script(shim_name: &str) -> Fallible<()> {
135-
let script_path = volta_home()?.shim_git_bash_script_file(shim_name);
136-
write(script_path, BASH_SCRIPT).with_context(|| ErrorKind::ShimCreateError {
137-
name: shim_name.to_string(),
138-
})
158+
const GIT_BASH_SCRIPT_CONTENTS: &str = r#"#!/bin/bash
159+
volta run "$(basename $0)" "$@""#;
160+
161+
pub fn create(shim_name: &str) -> Fallible<ShimResult> {
162+
let shim = volta_home()?.shim_file(shim_name);
163+
164+
write(shim, SHIM_SCRIPT_CONTENTS).with_context(|| ErrorKind::ShimCreateError {
165+
name: shim_name.to_owned(),
166+
})?;
167+
168+
let git_bash_script = volta_home()?.shim_git_bash_script_file(shim_name);
169+
170+
write(git_bash_script, GIT_BASH_SCRIPT_CONTENTS).with_context(|| {
171+
ErrorKind::ShimCreateError {
172+
name: shim_name.to_owned(),
173+
}
174+
})?;
175+
176+
Ok(ShimResult::Created)
177+
}
178+
179+
pub fn entry_to_shim_name((entry, _): (DirEntry, Metadata)) -> Option<String> {
180+
let path = entry.path();
181+
182+
if path.extension().is_some_and(|ext| ext == "cmd") {
183+
path.file_stem()
184+
.and_then(OsStr::to_str)
185+
.map(ToOwned::to_owned)
186+
} else {
187+
None
188+
}
139189
}
140190

141191
pub fn delete_git_bash_script(shim_name: &str) -> Fallible<()> {

crates/volta-layout/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod v0;
55
pub mod v1;
66
pub mod v2;
77
pub mod v3;
8+
pub mod v4;
89

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

0 commit comments

Comments
 (0)