Skip to content

Commit fa04c75

Browse files
committed
Update shim behavior on Windows to use scripts
Rather than using symlinks, which require administrator privileges or developer mode on Windows, we can instead use small scripts that call out to 'volta run' to actually handle the execution of 3rd party binaries. This also adds a migration to a new v4 layout, which allows Windows installs to migrate immediately to using the script-based shims.
1 parent 419c382 commit fa04c75

File tree

6 files changed

+389
-61
lines changed

6 files changed

+389
-61
lines changed

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)

crates/volta-layout/src/v4.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::path::PathBuf;
2+
3+
use volta_layout_macro::layout;
4+
5+
pub use crate::v1::VoltaInstall;
6+
7+
layout! {
8+
pub struct VoltaHome {
9+
"cache": cache_dir {
10+
"node": node_cache_dir {
11+
"index.json": node_index_file;
12+
"index.json.expires": node_index_expiry_file;
13+
}
14+
}
15+
"bin": shim_dir {}
16+
"log": log_dir {}
17+
"tools": tools_dir {
18+
"inventory": inventory_dir {
19+
"node": node_inventory_dir {}
20+
"npm": npm_inventory_dir {}
21+
"pnpm": pnpm_inventory_dir {}
22+
"yarn": yarn_inventory_dir {}
23+
}
24+
"image": image_dir {
25+
"node": node_image_root_dir {}
26+
"npm": npm_image_root_dir {}
27+
"pnpm": pnpm_image_root_dir {}
28+
"yarn": yarn_image_root_dir {}
29+
"packages": package_image_root_dir {}
30+
}
31+
"shared": shared_lib_root {}
32+
"user": default_toolchain_dir {
33+
"bins": default_bin_dir {}
34+
"packages": default_package_dir {}
35+
"platform.json": default_platform_file;
36+
}
37+
}
38+
"tmp": tmp_dir {}
39+
"hooks.json": default_hooks_file;
40+
"layout.v4": layout_file;
41+
}
42+
}
43+
44+
impl VoltaHome {
45+
pub fn node_image_dir(&self, node: &str) -> PathBuf {
46+
path_buf!(self.node_image_root_dir.clone(), node)
47+
}
48+
49+
pub fn npm_image_dir(&self, npm: &str) -> PathBuf {
50+
path_buf!(self.npm_image_root_dir.clone(), npm)
51+
}
52+
53+
pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf {
54+
path_buf!(self.npm_image_dir(npm), "bin")
55+
}
56+
57+
pub fn pnpm_image_dir(&self, version: &str) -> PathBuf {
58+
path_buf!(self.pnpm_image_root_dir.clone(), version)
59+
}
60+
61+
pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf {
62+
path_buf!(self.pnpm_image_dir(version), "bin")
63+
}
64+
65+
pub fn yarn_image_dir(&self, version: &str) -> PathBuf {
66+
path_buf!(self.yarn_image_root_dir.clone(), version)
67+
}
68+
69+
pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf {
70+
path_buf!(self.yarn_image_dir(version), "bin")
71+
}
72+
73+
pub fn package_image_dir(&self, name: &str) -> PathBuf {
74+
path_buf!(self.package_image_root_dir.clone(), name)
75+
}
76+
77+
pub fn default_package_config_file(&self, package_name: &str) -> PathBuf {
78+
path_buf!(
79+
self.default_package_dir.clone(),
80+
format!("{}.json", package_name)
81+
)
82+
}
83+
84+
pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf {
85+
path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name))
86+
}
87+
88+
pub fn node_npm_version_file(&self, version: &str) -> PathBuf {
89+
path_buf!(
90+
self.node_inventory_dir.clone(),
91+
format!("node-v{}-npm", version)
92+
)
93+
}
94+
95+
pub fn shim_file(&self, toolname: &str) -> PathBuf {
96+
// On Windows, shims are created as `<name>.cmd` since they
97+
// are thin scripts that use `volta run` to execute the command
98+
#[cfg(windows)]
99+
let toolname = format!("{}{}", toolname, ".cmd");
100+
101+
path_buf!(self.shim_dir.clone(), toolname)
102+
}
103+
104+
pub fn shared_lib_dir(&self, library: &str) -> PathBuf {
105+
path_buf!(self.shared_lib_root.clone(), library)
106+
}
107+
}
108+
109+
#[cfg(windows)]
110+
impl VoltaHome {
111+
pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf {
112+
path_buf!(self.shim_dir.clone(), toolname)
113+
}
114+
115+
pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
116+
self.node_image_dir(node)
117+
}
118+
}
119+
120+
#[cfg(unix)]
121+
impl VoltaHome {
122+
pub fn node_image_bin_dir(&self, node: &str) -> PathBuf {
123+
path_buf!(self.node_image_dir(node), "bin")
124+
}
125+
}

0 commit comments

Comments
 (0)