11//! Provides utilities for modifying shims for 3rd-party executables
22
33use std:: collections:: HashSet ;
4- use std:: fs:: { self , DirEntry , Metadata } ;
4+ use std:: fs;
55use std:: io;
66use std:: path:: Path ;
77
88use 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;
1111use crate :: sync:: VoltaLock ;
1212use log:: debug;
1313
14+ pub use platform:: create;
15+
1416pub 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-
9461pub 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 < ( ) > {
0 commit comments