Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Windows symlinks with volta run scripts #1755

Merged
merged 3 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading