Skip to content

Commit 9c6fbb8

Browse files
authored
Merge branch 'main' into feat/vp-shell-env
2 parents 9c9aec4 + de5f8d9 commit 9c6fbb8

7 files changed

Lines changed: 280 additions & 29 deletions

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ jobs:
145145
echo "Installer binaries:"
146146
ls -la installer-release/ || echo "No installer binaries found"
147147
148+
- name: Download release archives
149+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
150+
with:
151+
path: binary-release
152+
pattern: vp-release-archive-*
153+
merge-multiple: true
154+
148155
- name: 'Setup npm'
149156
run: npm install -g npm@latest
150157

@@ -203,6 +210,8 @@ jobs:
203210
target_commitish: ${{ github.sha }}
204211
files: |
205212
installer-release/vp-setup-*.exe
213+
binary-release/vp-*.tar.gz
214+
binary-release/vp-*.zip
206215
207216
- name: Publish GitHub Release
208217
env:

.github/workflows/reusable-release-build.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ jobs:
102102
./target/${{ matrix.settings.target }}/release/vp-shim.exe
103103
if-no-files-found: error
104104

105+
- name: Package release archive
106+
shell: bash
107+
working-directory: ./target/${{ matrix.settings.target }}/release
108+
run: |
109+
if [ -f vp.exe ]; then
110+
7z a "vp-${{ matrix.settings.target }}.zip" vp.exe vp-shim.exe
111+
else
112+
tar -czf "vp-${{ matrix.settings.target }}.tar.gz" vp
113+
fi
114+
115+
- name: Upload release archive
116+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
117+
with:
118+
name: vp-release-archive-${{ matrix.settings.target }}
119+
path: |
120+
./target/${{ matrix.settings.target }}/release/vp-*.tar.gz
121+
./target/${{ matrix.settings.target }}/release/vp-*.zip
122+
if-no-files-found: error
123+
105124
- name: Upload installer binary artifact (Windows only)
106125
if: contains(matrix.settings.target, 'windows')
107126
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 213 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//! - ~/.vite-plus/current/ - Contains the actual vp CLI binary
66
//!
77
//! On Unix:
8-
//! - bin/vp is a symlink to ../current/bin/vp
9-
//! - bin/node, bin/npm, bin/npx are symlinks to ../current/bin/vp
8+
//! - bin/vp is a symlink to the active vp binary
9+
//! - bin/node, bin/npm, bin/npx are symlinks to the active vp binary
1010
//! - Symlinks preserve argv[0], allowing tool detection via the symlink name
1111
//!
1212
//! On Windows:
@@ -88,7 +88,7 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
8888
.map_err(|e| Error::ConfigError(format!("Cannot find current executable: {e}").into()))?;
8989

9090
// Create wrapper script in bin/
91-
setup_vp_wrapper(&bin_dir, refresh).await?;
91+
setup_vp_wrapper(&current_exe, &bin_dir, refresh).await?;
9292

9393
// Create shims for node, npm, npx
9494
let mut created = Vec::new();
@@ -144,30 +144,44 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
144144
Ok(ExitStatus::default())
145145
}
146146

147-
/// Create symlink in bin/ that points to current/bin/vp.
148-
async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> Result<(), Error> {
147+
/// Create symlink in bin/ that points to the active vp binary.
148+
async fn setup_vp_wrapper(
149+
current_exe: &std::path::Path,
150+
bin_dir: &vite_path::AbsolutePath,
151+
refresh: bool,
152+
) -> Result<(), Error> {
149153
#[cfg(unix)]
150154
{
151155
let bin_vp = bin_dir.join("vp");
152-
153-
// Create symlink bin/vp -> ../current/bin/vp
154-
let should_create_symlink = refresh
155-
|| !tokio::fs::try_exists(&bin_vp).await.unwrap_or(false)
156-
|| !is_symlink(&bin_vp).await; // Replace non-symlink with symlink
156+
let target = resolve_unix_vp_shim_target(current_exe, bin_dir).await?;
157+
let existing = tokio::fs::symlink_metadata(&bin_vp).await.ok();
158+
159+
let should_create_symlink = match existing.as_ref() {
160+
Some(metadata) if refresh || !metadata.file_type().is_symlink() => true,
161+
Some(_) => {
162+
let broken_symlink = !std::fs::exists(bin_vp.as_path()).unwrap_or(false);
163+
let wrong_target = tokio::fs::read_link(&bin_vp)
164+
.await
165+
.map(|existing_target| existing_target != target)
166+
.unwrap_or(true);
167+
broken_symlink || wrong_target
168+
}
169+
None => true,
170+
};
157171

158172
if should_create_symlink {
159173
// Remove existing if present (could be old wrapper script or file)
160-
if tokio::fs::try_exists(&bin_vp).await.unwrap_or(false) {
174+
if existing.is_some() {
161175
tokio::fs::remove_file(&bin_vp).await?;
162176
}
163-
// Create relative symlink
164-
tokio::fs::symlink("../current/bin/vp", &bin_vp).await?;
165-
tracing::debug!("Created symlink {:?} -> ../current/bin/vp", bin_vp);
177+
tokio::fs::symlink(&target, &bin_vp).await?;
178+
tracing::debug!("Created symlink {:?} -> {:?}", bin_vp, target);
166179
}
167180
}
168181

169182
#[cfg(windows)]
170183
{
184+
let _ = current_exe;
171185
let bin_vp_exe = bin_dir.join("vp.exe");
172186

173187
// Create trampoline bin/vp.exe that forwards to current\bin\vp.exe
@@ -195,13 +209,23 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R
195209
Ok(())
196210
}
197211

198-
/// Check if a path is a symlink.
199212
#[cfg(unix)]
200-
async fn is_symlink(path: &vite_path::AbsolutePath) -> bool {
201-
match tokio::fs::symlink_metadata(path).await {
202-
Ok(m) => m.file_type().is_symlink(),
203-
Err(_) => false,
213+
pub(crate) async fn resolve_unix_vp_shim_target(
214+
current_exe: &std::path::Path,
215+
bin_dir: &vite_path::AbsolutePath,
216+
) -> Result<std::path::PathBuf, Error> {
217+
if let Some(vite_plus_home) = bin_dir.parent() {
218+
let standalone_vp = vite_plus_home.join("current").join("bin").join("vp");
219+
if tokio::fs::try_exists(&standalone_vp).await.unwrap_or(false) {
220+
let standalone_vp = tokio::fs::canonicalize(&standalone_vp).await.ok();
221+
let current_exe = tokio::fs::canonicalize(current_exe).await.ok();
222+
if standalone_vp.is_some() && standalone_vp == current_exe {
223+
return Ok(std::path::PathBuf::from("../current/bin/vp"));
224+
}
225+
}
204226
}
227+
228+
Ok(current_exe.to_path_buf())
205229
}
206230

207231
/// Create a single shim for node/npm/npx.
@@ -215,9 +239,31 @@ async fn create_shim(
215239
) -> Result<bool, Error> {
216240
let shim_path = bin_dir.join(shim_filename(tool));
217241

218-
// Check if shim already exists
219-
if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) {
220-
if !refresh {
242+
#[cfg(unix)]
243+
let desired_target = resolve_unix_vp_shim_target(source, bin_dir).await?;
244+
245+
let existing = tokio::fs::symlink_metadata(&shim_path).await.ok();
246+
if existing.is_some() {
247+
let should_replace = if refresh {
248+
true
249+
} else {
250+
#[cfg(unix)]
251+
{
252+
existing.as_ref().is_some_and(|metadata| metadata.file_type().is_symlink())
253+
&& (!std::fs::exists(shim_path.as_path()).unwrap_or(false)
254+
|| tokio::fs::read_link(&shim_path)
255+
.await
256+
.map(|existing_target| existing_target != desired_target)
257+
.unwrap_or(true))
258+
}
259+
260+
#[cfg(windows)]
261+
{
262+
false
263+
}
264+
};
265+
266+
if !should_replace {
221267
return Ok(false);
222268
}
223269
#[cfg(windows)]
@@ -255,19 +301,22 @@ fn shim_filename(tool: &str) -> String {
255301
}
256302
}
257303

258-
/// Create a Unix shim using symlink to ../current/bin/vp.
304+
/// Create a Unix shim using symlink to the active vp binary.
259305
///
260306
/// Symlinks preserve argv[0], allowing the vp binary to detect which tool
261307
/// was invoked. This is the same pattern used by Volta.
262308
#[cfg(unix)]
263309
async fn create_unix_shim(
264-
_source: &std::path::Path,
310+
source: &std::path::Path,
265311
shim_path: &vite_path::AbsolutePath,
266-
_tool: &str,
312+
tool: &str,
267313
) -> Result<(), Error> {
268-
// Create symlink to ../current/bin/vp (relative path)
269-
tokio::fs::symlink("../current/bin/vp", shim_path).await?;
270-
tracing::debug!("Created symlink shim at {:?} -> ../current/bin/vp", shim_path);
314+
let bin_dir = shim_path.parent().ok_or_else(|| {
315+
Error::ConfigError(format!("Cannot find parent directory for {tool} shim").into())
316+
})?;
317+
let target = resolve_unix_vp_shim_target(source, bin_dir).await?;
318+
tokio::fs::symlink(&target, shim_path).await?;
319+
tracing::debug!("Created symlink shim at {:?} -> {:?}", shim_path, target);
271320

272321
Ok(())
273322
}
@@ -1079,6 +1128,142 @@ mod tests {
10791128
assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created");
10801129
}
10811130

1131+
#[tokio::test]
1132+
#[cfg(unix)]
1133+
async fn test_unix_vp_shim_target_prefers_standalone_layout_for_current_exe() {
1134+
let temp_dir = TempDir::new().unwrap();
1135+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1136+
let bin_dir = home.join("bin");
1137+
let standalone_vp = home.join("current").join("bin").join("vp");
1138+
1139+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1140+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1141+
tokio::fs::write(&standalone_vp, b"vp").await.unwrap();
1142+
1143+
let target = resolve_unix_vp_shim_target(standalone_vp.as_path(), &bin_dir).await.unwrap();
1144+
1145+
assert_eq!(target, std::path::Path::new("../current/bin/vp"));
1146+
}
1147+
1148+
#[tokio::test]
1149+
#[cfg(unix)]
1150+
async fn test_unix_vp_shim_target_uses_current_exe_when_standalone_is_stale() {
1151+
let temp_dir = TempDir::new().unwrap();
1152+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1153+
let bin_dir = home.join("bin");
1154+
let standalone_vp = home.join("current").join("bin").join("vp");
1155+
let external_vp = temp_dir.path().join("external-vp");
1156+
1157+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1158+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1159+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1160+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1161+
1162+
let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap();
1163+
1164+
assert_eq!(target, external_vp);
1165+
}
1166+
1167+
#[tokio::test]
1168+
#[cfg(unix)]
1169+
async fn test_unix_vp_shim_target_uses_current_exe_without_standalone_layout() {
1170+
let temp_dir = TempDir::new().unwrap();
1171+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1172+
let bin_dir = home.join("bin");
1173+
let external_vp = temp_dir.path().join("external-vp");
1174+
1175+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1176+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1177+
1178+
let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap();
1179+
1180+
assert_eq!(target, external_vp);
1181+
}
1182+
1183+
#[tokio::test]
1184+
#[cfg(unix)]
1185+
async fn test_create_shim_replaces_stale_unix_symlink_without_refresh() {
1186+
let temp_dir = TempDir::new().unwrap();
1187+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1188+
let bin_dir = home.join("bin");
1189+
let standalone_vp = home.join("current").join("bin").join("vp");
1190+
let external_vp = temp_dir.path().join("external-vp");
1191+
let node_shim = bin_dir.join("node");
1192+
1193+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1194+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1195+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1196+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1197+
tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap();
1198+
1199+
let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap();
1200+
let target = tokio::fs::read_link(&node_shim).await.unwrap();
1201+
1202+
assert!(created, "stale shims should be recreated");
1203+
assert_eq!(target, external_vp);
1204+
}
1205+
1206+
#[tokio::test]
1207+
#[cfg(unix)]
1208+
async fn test_create_shim_replaces_broken_unix_symlink_without_refresh() {
1209+
let temp_dir = TempDir::new().unwrap();
1210+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1211+
let bin_dir = home.join("bin");
1212+
let external_vp = temp_dir.path().join("external-vp");
1213+
let node_shim = bin_dir.join("node");
1214+
1215+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1216+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1217+
tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap();
1218+
1219+
let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap();
1220+
let target = tokio::fs::read_link(&node_shim).await.unwrap();
1221+
1222+
assert!(created, "broken shims should be recreated");
1223+
assert_eq!(target, external_vp);
1224+
}
1225+
1226+
#[tokio::test]
1227+
#[cfg(unix)]
1228+
async fn test_setup_vp_wrapper_replaces_stale_unix_symlink_without_refresh() {
1229+
let temp_dir = TempDir::new().unwrap();
1230+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1231+
let bin_dir = home.join("bin");
1232+
let standalone_vp = home.join("current").join("bin").join("vp");
1233+
let external_vp = temp_dir.path().join("external-vp");
1234+
let vp_shim = bin_dir.join("vp");
1235+
1236+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1237+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1238+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1239+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1240+
tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap();
1241+
1242+
setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap();
1243+
let target = tokio::fs::read_link(&vp_shim).await.unwrap();
1244+
1245+
assert_eq!(target, external_vp);
1246+
}
1247+
1248+
#[tokio::test]
1249+
#[cfg(unix)]
1250+
async fn test_setup_vp_wrapper_replaces_broken_unix_symlink_without_refresh() {
1251+
let temp_dir = TempDir::new().unwrap();
1252+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1253+
let bin_dir = home.join("bin");
1254+
let external_vp = temp_dir.path().join("external-vp");
1255+
let vp_shim = bin_dir.join("vp");
1256+
1257+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1258+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1259+
tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap();
1260+
1261+
setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap();
1262+
let target = tokio::fs::read_link(&vp_shim).await.unwrap();
1263+
1264+
assert_eq!(target, external_vp);
1265+
}
1266+
10821267
#[tokio::test]
10831268
async fn test_create_env_files_contains_dynamic_completion() {
10841269
let temp_dir = TempDir::new().unwrap();

crates/vite_pm_cli/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ vite_str = { workspace = true }
2121
vite_workspace = { workspace = true }
2222

2323
[lib]
24-
test = false
2524
doctest = false
2625

2726
[lints]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
const expected = path.resolve('external/vp');
5+
6+
for (const shim of ['vp', 'node', 'npm', 'npx', 'vpx', 'vpr']) {
7+
const shimPath = path.join('home', 'bin', shim);
8+
const target = fs.readlinkSync(shimPath);
9+
if (target !== expected) {
10+
throw new Error(`${shim} points to ${target}, expected ${expected}`);
11+
}
12+
}
13+
14+
console.log('all shims point to external vp');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
> mkdir -p external home # Prepare isolated external install and VP_HOME
2+
> cp "$VP_HOME/bin/vp" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME
3+
> printf '22.18.0\n' > .node-version # Project Node.js version
4+
> mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\necho vp-managed-node-22.18.0\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime
5+
> VP_HOME="$(pwd)/home" ./external/vp env setup # Setup shims from external vp
6+
> VP_BYPASS="$VP_HOME/bin" node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp
7+
all shims point to external vp
8+
9+
> VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" node -v # node shim uses the project version
10+
vp-managed-node-22.18.0

0 commit comments

Comments
 (0)