Skip to content

Commit d3f9560

Browse files
committed
fix(installer): require split-layout releases
1 parent c4dcff0 commit d3f9560

10 files changed

Lines changed: 183 additions & 364 deletions

File tree

.github/workflows/test-standalone-install.yml

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- 'packages/cli/install.sh'
1010
- 'packages/cli/install.ps1'
1111
- 'packages/tools/src/install-global-cli.ts'
12+
- 'packages/tools/src/local-npm-registry.ts'
1213
- 'crates/vp_installer/**'
1314
- 'crates/vp_trampoline/**'
1415
- 'crates/vp_global_cli/**'
@@ -1257,40 +1258,33 @@ jobs:
12571258
# exit code after the assertions so the PowerShell step succeeds.
12581259
$global:LASTEXITCODE = 0
12591260
1260-
- name: vp-setup.exe pre-split fallback leaves no split roots
1261+
- name: vp-setup.exe rejects releases before 0.3.0
12611262
shell: pwsh
12621263
run: |
12631264
$ErrorActionPreference = "Stop"
1265+
$PSNativeCommandUseErrorActionPreference = $false
12641266
$installer = Join-Path $env:DEV_DRIVE "target/release/vp-setup.exe"
1265-
$profile = Join-Path $env:RUNNER_TEMP "vp-setup-0.2.9-profile"
1266-
$localSplitRoot = Join-Path $env:LOCALAPPDATA "vite-plus"
1267-
$roamingSplitRoot = Join-Path $env:APPDATA "vite-plus"
1268-
$splitRoots = @($localSplitRoot, $roamingSplitRoot)
1269-
Remove-Item -Recurse -Force $profile -ErrorAction SilentlyContinue
1270-
foreach ($splitRoot in $splitRoots) {
1271-
if (Test-Path $splitRoot) {
1272-
throw "fresh-install test requires an absent split root: $splitRoot"
1273-
}
1267+
$installRoot = Join-Path $env:RUNNER_TEMP "vp-setup-unsupported-version"
1268+
Remove-Item -Recurse -Force $installRoot -ErrorAction SilentlyContinue
1269+
$env:VP_HOME = $installRoot
1270+
Remove-Item Env:VP_BIN_DIR, Env:VP_DATA_DIR, Env:VP_CACHE_DIR -ErrorAction SilentlyContinue
1271+
1272+
$output = (& $installer --yes --quiet --version 0.2.9 --no-node-manager --no-modify-path 2>&1) | Out-String
1273+
$exitCode = $LASTEXITCODE
1274+
Write-Host $output
1275+
if ($exitCode -ne 1) {
1276+
throw "vp-setup.exe exited with $exitCode, expected 1"
12741277
}
1275-
New-Item -ItemType Directory -Force -Path $profile | Out-Null
1276-
1277-
Remove-Item Env:VP_HOME, Env:VP_BIN_DIR, Env:VP_DATA_DIR, Env:VP_CACHE_DIR -ErrorAction SilentlyContinue
1278-
$env:USERPROFILE = $profile
1279-
$env:HOME = $profile
1280-
& $installer --yes --version 0.2.9 --no-node-manager --no-modify-path
1281-
if ($LASTEXITCODE -ne 0) {
1282-
throw "vp-setup.exe exited with $LASTEXITCODE"
1278+
if (-not $output.Contains("Install vite-plus 0.3.0 or later")) {
1279+
throw "vp-setup.exe did not report its minimum supported version"
12831280
}
1284-
1285-
$legacyRoot = Join-Path $profile ".vite-plus"
1286-
if (-not (Test-Path (Join-Path $legacyRoot "current/bin/vp.exe"))) {
1287-
throw "pre-split payload missing from $legacyRoot"
1281+
if ($output -match "(?i)preview") {
1282+
throw "vp-setup.exe exposed preview-build guidance"
12881283
}
1289-
foreach ($splitRoot in $splitRoots) {
1290-
if (Test-Path $splitRoot) {
1291-
throw "vp-setup.exe left split root $splitRoot"
1292-
}
1284+
if (Test-Path $installRoot) {
1285+
throw "vp-setup.exe created unsupported-version root $installRoot"
12931286
}
1287+
$global:LASTEXITCODE = 0
12941288
12951289
- name: Incomplete directory overrides are rejected in PowerShell
12961290
shell: pwsh
@@ -1569,9 +1563,58 @@ jobs:
15691563
}
15701564
& $vp --version
15711565
1572-
- name: Install via vp-setup.exe (silent)
1566+
- name: Start local preview registry for vp-setup.exe
1567+
shell: bash
1568+
run: |
1569+
test_version="0.0.0-commit.${GITHUB_SHA}"
1570+
registry_root="$RUNNER_TEMP/vp-setup-local-registry"
1571+
packages_dir="$registry_root/packages"
1572+
main_dir="$registry_root/vite-plus"
1573+
platform_dir="$registry_root/vite-plus-cli-win32-x64-msvc"
1574+
rm -rf "$registry_root"
1575+
mkdir -p "$packages_dir" "$main_dir" "$platform_dir"
1576+
1577+
node -e '
1578+
const fs = require("node:fs");
1579+
const path = require("node:path");
1580+
const [dir, version] = process.argv.slice(1);
1581+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "vite-plus", version }));
1582+
' "$main_dir" "$test_version"
1583+
node -e '
1584+
const fs = require("node:fs");
1585+
const path = require("node:path");
1586+
const [dir, version] = process.argv.slice(1);
1587+
const manifest = {
1588+
name: "@voidzero-dev/vite-plus-cli-win32-x64-msvc",
1589+
version,
1590+
files: ["vp.exe", "vp-shim.exe"],
1591+
};
1592+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(manifest));
1593+
' "$platform_dir" "$test_version"
1594+
cp "$DEV_DRIVE/target/release/vp.exe" "$DEV_DRIVE/target/release/vp-shim.exe" "$platform_dir/"
1595+
npm pack "$main_dir" --pack-destination "$packages_dir"
1596+
npm pack "$platform_dir" --pack-destination "$packages_dir"
1597+
1598+
registry_log="$registry_root/registry.out"
1599+
node "$GITHUB_WORKSPACE/packages/tools/src/local-npm-registry.ts" --serve --packages-dir "$packages_dir" > "$registry_log" 2>&1 &
1600+
server_pid=$!
1601+
until grep -q '"registry"' "$registry_log" 2>/dev/null; do
1602+
if ! kill -0 "$server_pid" 2>/dev/null; then
1603+
cat "$registry_log"
1604+
exit 1
1605+
fi
1606+
sleep 0.2
1607+
done
1608+
handshake=$(grep -m1 '"registry"' "$registry_log")
1609+
registry=$(echo "$handshake" | node -e 'process.stdin.on("data", data => process.stdout.write(JSON.parse(data).registry))')
1610+
echo "VP_SETUP_TEST_REGISTRY=$registry" >> "$GITHUB_ENV"
1611+
echo "VP_SETUP_TEST_VERSION=$test_version" >> "$GITHUB_ENV"
1612+
1613+
- name: Install local preview via vp-setup.exe (silent)
15731614
shell: pwsh
1574-
run: ${{ format('{0}/target/release/vp-setup.exe', env.DEV_DRIVE) }}
1615+
run: |
1616+
$installer = Join-Path $env:DEV_DRIVE "target/release/vp-setup.exe"
1617+
& $installer --version $env:VP_SETUP_TEST_VERSION --registry $env:VP_SETUP_TEST_REGISTRY
15751618
15761619
- name: Set PATH
15771620
shell: bash

crates/vp_global_cli/src/commands/upgrade/mod.rs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub async fn execute(options: UpgradeOptions) -> Result<ExitStatus, Error> {
101101
// binary. A monolithic install accepts each release. This includes a
102102
// VP_HOME pin and an existing ~/.vite-plus install.
103103
let legacy = vp_shared::VpDirs::legacy_single_root(&config.user_home);
104-
if legacy.data != config.dirs.data && !supports_split_layout(&resolved.version) {
104+
if legacy.data != config.dirs.data && !vp_setup::supports_split_layout(&resolved.version) {
105105
return Err(Error::Upgrade(
106106
format!(
107107
"vite-plus {} does not support this split directory layout. \
@@ -280,35 +280,3 @@ async fn execute_rollback(
280280

281281
Ok(ExitStatus::default())
282282
}
283-
284-
/// Return `true` if `version` supports the split layout.
285-
///
286-
/// Version 0.3.0 and later support it, including prereleases. Preview builds
287-
/// (`0.0.0-commit.<sha>`) also support it because they track the current branch.
288-
fn supports_split_layout(version: &str) -> bool {
289-
let Ok(version) = node_semver::Version::parse(version) else {
290-
return false;
291-
};
292-
if version.major == 0 && version.minor == 0 && version.patch == 0 {
293-
return !version.pre_release.is_empty() || !version.build.is_empty();
294-
}
295-
version.major > 0 || version.minor >= 3
296-
}
297-
298-
#[cfg(test)]
299-
mod tests {
300-
use super::supports_split_layout;
301-
302-
#[test]
303-
fn split_layout_support_by_version() {
304-
assert!(supports_split_layout("0.3.0"));
305-
assert!(supports_split_layout("0.3.0-alpha.1"));
306-
assert!(supports_split_layout("0.4.2"));
307-
assert!(supports_split_layout("1.0.0"));
308-
assert!(supports_split_layout("0.0.0-commit.0123abc"));
309-
assert!(!supports_split_layout("0.2.9"));
310-
assert!(!supports_split_layout("0.2.0"));
311-
assert!(!supports_split_layout("0.1.14-alpha.1"));
312-
assert!(!supports_split_layout("not-a-version"));
313-
}
314-
}

crates/vp_installer/src/main.rs

Lines changed: 17 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,6 @@ use vp_setup::{VP_BINARY_NAME, install, integrity, platform, registry};
3131
use vp_shared::VpDirs;
3232
use vt_path::AbsolutePathBuf;
3333

34-
#[derive(Debug)]
35-
struct AbandonedSplitData {
36-
data: AbsolutePathBuf,
37-
parent_to_remove: Option<std::path::PathBuf>,
38-
}
39-
40-
impl AbandonedSplitData {
41-
async fn capture(data: AbsolutePathBuf) -> Option<Self> {
42-
if tokio::fs::try_exists(&data).await.unwrap_or(true) {
43-
return None;
44-
}
45-
46-
let parent_to_remove = if let Some(parent) = data.as_path().parent()
47-
&& !tokio::fs::try_exists(parent).await.unwrap_or(true)
48-
{
49-
Some(parent.to_path_buf())
50-
} else {
51-
None
52-
};
53-
Some(Self { data, parent_to_remove })
54-
}
55-
56-
async fn remove(self) {
57-
let _ = tokio::fs::remove_dir_all(&self.data).await;
58-
if let Some(parent) = self.parent_to_remove {
59-
// Remove only the parent that did not exist before probing. A
60-
// concurrent file or directory makes this non-recursive removal
61-
// fail and preserves the parent.
62-
let _ = tokio::fs::remove_dir(parent).await;
63-
}
64-
}
65-
}
66-
6734
/// Restrict DLL search to system32 only to prevent DLL hijacking
6835
/// when the installer is run from a Downloads folder.
6936
#[cfg(windows)]
@@ -185,10 +152,7 @@ async fn run(mut opts: cli::Options, dirs: VpDirs) -> i32 {
185152
}
186153

187154
let code = match do_install(&opts, &dirs).await {
188-
Ok(effective_dirs) => {
189-
// do_install uses the monolithic root for a pre-split payload.
190-
// Report the directories that it used.
191-
let (data_dir_display, bin_dir_display) = dir_displays(&effective_dirs);
155+
Ok(()) => {
192156
print_success(&opts, &data_dir_display, &bin_dir_display);
193157
0
194158
}
@@ -207,23 +171,17 @@ async fn run(mut opts: cli::Options, dirs: VpDirs) -> i32 {
207171
code
208172
}
209173

210-
/// Install the resolved version and return the directories that the installer
211-
/// used. The installer uses the monolithic root for a pre-split payload.
174+
/// Install the resolved version.
212175
#[allow(clippy::print_stdout)]
213-
async fn do_install(
214-
opts: &cli::Options,
215-
dirs: &VpDirs,
216-
) -> Result<VpDirs, Box<dyn std::error::Error>> {
217-
let mut dirs = dirs.clone();
176+
async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box<dyn std::error::Error>> {
218177
let platform_suffix = platform::detect_platform_suffix()?;
219178
if !opts.quiet {
220179
print_info(&format!("detected platform: {platform_suffix}"));
221180
}
222181

223182
// Check local version first to potentially skip HTTP requests.
224183
// This operation is read-only. Create the install root only after the
225-
// downloaded payload confirms the layout. Thus, a pre-split fallback does
226-
// not leave empty split directories.
184+
// installer resolves and validates the target version.
227185
let current_version = install::read_current_version(&dirs.data).await;
228186

229187
let version_or_tag = opts.version.as_deref().unwrap_or(&opts.tag);
@@ -235,6 +193,12 @@ async fn do_install(
235193
}
236194
let target_version =
237195
registry::resolve_version_string(version_or_tag, opts.registry.as_deref()).await?;
196+
if !vp_setup::supports_split_layout(&target_version) {
197+
return Err(format!(
198+
"vite-plus {target_version} is not supported by vp-setup. Install vite-plus 0.3.0 or later."
199+
)
200+
.into());
201+
}
238202

239203
// Same version only if the binary is intact — a corrupted install needs a full reinstall.
240204
// `is_install_dir_for_version` also matches `{version}+force.*` dirs left by a forced
@@ -278,41 +242,6 @@ async fn do_install(
278242
}
279243
integrity::verify_integrity(&platform_data, &resolved.platform_integrity)?;
280244

281-
// A pre-split release resolves every path from VP_HOME. Its default is
282-
// ~/.vite-plus. Its environment setup, shims, and trampolines cannot
283-
// use split roots. Use that monolithic root when the payload cannot
284-
// report split category roots.
285-
let legacy = VpDirs::legacy_single_root(&vp_shared::EnvConfig::get().user_home);
286-
let abandoned_split_data = if legacy.data == dirs.data {
287-
// Pre-split and split-aware payloads use the same monolithic root
288-
// here. Skip the probe because it extracts and starts the payload.
289-
None
290-
} else {
291-
let split_data_cleanup = AbandonedSplitData::capture(dirs.data.clone()).await;
292-
if let Some(probed) = install::probe_payload_dirs(&platform_data).await {
293-
// Use the payload's resolution, as install.sh and install.ps1 do.
294-
// This keeps the written layout equal to the resolved layout.
295-
dirs = VpDirs::from_resolved_parts(
296-
probed.bin,
297-
probed.data,
298-
probed.cache,
299-
probed.config,
300-
probed.state,
301-
probed.layout,
302-
);
303-
None
304-
} else {
305-
if !opts.quiet {
306-
print_info(&format!(
307-
"vite-plus {target_version} does not support the split directory layout. Vite+ will install it in {}.",
308-
legacy.data.as_path().display()
309-
));
310-
}
311-
dirs = legacy;
312-
split_data_cleanup
313-
}
314-
};
315-
316245
let install_dir = &dirs.data;
317246
let version_dir = install_dir.join(&target_version);
318247
tokio::fs::create_dir_all(&version_dir).await?;
@@ -332,13 +261,6 @@ async fn do_install(
332261
let _ = tokio::fs::remove_dir_all(&version_dir).await;
333262
}
334263

335-
// The managed node and pnpm use paths from the process EnvConfig. The
336-
// installer pinned this configuration before the payload selected the
337-
// monolithic root. Remove the split data root and its empty application
338-
// parent if this run created them.
339-
if let Some(split_data) = abandoned_split_data {
340-
split_data.remove().await;
341-
}
342264
result?;
343265
}
344266

@@ -371,7 +293,7 @@ async fn do_install(
371293
}
372294
}
373295

374-
Ok(dirs)
296+
Ok(())
375297
}
376298

377299
/// Auto-detect whether the Node.js version manager should be enabled.
@@ -489,6 +411,10 @@ async fn install_new_version(
489411
if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) {
490412
return Err("Binary not found after extraction. The download may be corrupted.".into());
491413
}
414+
#[cfg(windows)]
415+
if !tokio::fs::try_exists(version_dir.join("bin").join("vp-shim.exe")).await.unwrap_or(false) {
416+
return Err("Trampoline not found after extraction. The download may be corrupted.".into());
417+
}
492418

493419
install::generate_wrapper_package_json(version_dir, version).await?;
494420

@@ -545,17 +471,8 @@ async fn setup_bin_shims(dirs: &VpDirs) -> Result<(), Box<dyn std::error::Error>
545471
let shim_src = dirs.data.join("current").join("bin").join("vp-shim.exe");
546472
let shim_dst = bin_dir.join("vp.exe");
547473

548-
// Prefer vp-shim.exe (trampoline); fall back to vp.exe for pre-trampoline releases
549-
let src = if tokio::fs::try_exists(&shim_src).await.unwrap_or(false) {
550-
shim_src
551-
} else {
552-
dirs.data.join("current").join("bin").join("vp.exe")
553-
};
554-
555-
if tokio::fs::try_exists(&src).await.unwrap_or(false) {
556-
replace_windows_exe(&src, &shim_dst, &bin_dir).await?;
557-
dirs.write_shim_pointer("vp")?;
558-
}
474+
replace_windows_exe(&shim_src, &shim_dst, &bin_dir).await?;
475+
dirs.write_shim_pointer("vp")?;
559476

560477
// Best-effort cleanup of old shim files
561478
if let Ok(mut entries) = tokio::fs::read_dir(&bin_dir).await {
@@ -968,32 +885,4 @@ mod tests {
968885
}
969886
});
970887
}
971-
972-
#[tokio::test]
973-
async fn abandoned_split_data_cleanup_removes_new_empty_parent() {
974-
let tmp = tempfile::tempdir().unwrap();
975-
let parent = tmp.path().join("vite-plus");
976-
let data = AbsolutePathBuf::new(parent.join("data")).unwrap();
977-
let cleanup = AbandonedSplitData::capture(data.clone()).await.unwrap();
978-
979-
tokio::fs::create_dir_all(&data).await.unwrap();
980-
cleanup.remove().await;
981-
982-
assert!(!parent.exists());
983-
}
984-
985-
#[tokio::test]
986-
async fn abandoned_split_data_cleanup_preserves_existing_parent() {
987-
let tmp = tempfile::tempdir().unwrap();
988-
let parent = tmp.path().join("vite-plus");
989-
std::fs::create_dir(&parent).unwrap();
990-
let data = AbsolutePathBuf::new(parent.join("data")).unwrap();
991-
let cleanup = AbandonedSplitData::capture(data.clone()).await.unwrap();
992-
993-
tokio::fs::create_dir_all(&data).await.unwrap();
994-
cleanup.remove().await;
995-
996-
assert!(parent.is_dir());
997-
assert!(!data.as_path().exists());
998-
}
999888
}

0 commit comments

Comments
 (0)