Skip to content

Commit 2e26da4

Browse files
committed
feat(installer): add standalone Windows .exe installer (vp-setup.exe)
Add a standalone `vp-setup.exe` Windows installer that installs the vp CLI without requiring PowerShell, complementing the existing `install.ps1`. Architecture: - New `vite_setup` shared library crate extracting installation logic (platform detection, registry queries, integrity verification, tarball extraction, symlink/junction management) from `vite_global_cli` - New `vite_installer` binary crate producing `vp-setup.exe` Features: - Interactive menu with customize submenu (version, registry, Node.js manager, PATH modification) - Silent mode via `-y` or auto-detected CI environment - Node.js manager auto-detection matching install.ps1/install.sh logic - Same-version repair (skips download, reruns post-activation setup) - Windows PATH modification via `winreg` crate - DLL security mitigations (build.rs linker flag + runtime SetDefaultDllDirectories) - ANSI color support with fallback for legacy Windows consoles - Respects NO_COLOR env var - Post-activation steps are best-effort (non-fatal) - "Press Enter to close..." pause in interactive mode - VP_HOME propagated to child processes for custom install dirs CI: - Build + cache installer in release workflow - Attach as GitHub Release assets - Test job in test-standalone-install.yml Docs: - RFC at rfcs/windows-installer.md - Installation guide updated with vp-setup.exe download link - SmartScreen warning guide added Closes #1293
1 parent 5ab336f commit 2e26da4

File tree

25 files changed

+1672
-55
lines changed

25 files changed

+1672
-55
lines changed

.github/actions/build-upstream/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ runs:
4747
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp
4848
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp.exe
4949
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp-shim.exe
50+
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp-setup.exe
5051
key: ${{ steps.cache-key.outputs.key }}
5152

5253
# Apply Vite+ branding patches to vite source (CI checks out
@@ -143,6 +144,11 @@ runs:
143144
shell: bash
144145
run: cargo build --release --target ${{ inputs.target }} -p vite_trampoline
145146

147+
- name: Build installer binary (Windows only)
148+
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'windows')
149+
shell: bash
150+
run: cargo build --release --target ${{ inputs.target }} -p vite_installer
151+
146152
- name: Save NAPI binding cache
147153
if: steps.cache-restore.outputs.cache-hit != 'true'
148154
uses: actions/cache/save@94b89442628ad1d101e352b7ee38f30e1bef108e # v5
@@ -156,6 +162,7 @@ runs:
156162
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp
157163
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp.exe
158164
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp-shim.exe
165+
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp-setup.exe
159166
key: ${{ steps.cache-key.outputs.key }}
160167

161168
# Build vite-plus TypeScript after native bindings are ready

.github/workflows/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ jobs:
131131
./target/${{ matrix.settings.target }}/release/vp-shim.exe
132132
if-no-files-found: error
133133

134+
- name: Upload installer binary artifact (Windows only)
135+
if: contains(matrix.settings.target, 'windows')
136+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
137+
with:
138+
name: vp-setup-${{ matrix.settings.target }}
139+
path: ./target/${{ matrix.settings.target }}/release/vp-setup.exe
140+
if-no-files-found: error
141+
134142
- name: Remove .node files before upload dist
135143
if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }}
136144
run: |
@@ -241,6 +249,12 @@ jobs:
241249
path: rust-cli-artifacts
242250
pattern: vite-global-cli-*
243251

252+
- name: Download installer binaries (Windows)
253+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
254+
with:
255+
path: installer-artifacts
256+
pattern: vp-setup-*
257+
244258
- name: Move Rust CLI binaries to target directories
245259
run: |
246260
# Move each artifact's binary to the correct target directory
@@ -265,6 +279,19 @@ jobs:
265279
echo "Found binaries:"
266280
echo "$vp_files"
267281
282+
- name: Prepare installer binaries for release
283+
run: |
284+
mkdir -p installer-release
285+
for artifact_dir in installer-artifacts/vp-setup-*/; do
286+
if [ -d "$artifact_dir" ]; then
287+
dir_name=$(basename "$artifact_dir")
288+
target_name=${dir_name#vp-setup-}
289+
cp "$artifact_dir/vp-setup.exe" "installer-release/vp-setup-${target_name}.exe"
290+
fi
291+
done
292+
echo "Installer binaries:"
293+
ls -la installer-release/ || echo "No installer binaries found"
294+
268295
- name: Set npm packages version
269296
run: |
270297
sed -i 's/"version": "0.0.0"/"version": "${{ env.VERSION }}"/' packages/core/package.json
@@ -318,6 +345,8 @@ jobs:
318345
${INSTALL_PS1}
319346
\`\`\`
320347
348+
Or download and run \`vp-setup.exe\` from the assets below.
349+
321350
View the full commit: https://github.com/${{ github.repository }}/commit/${{ github.sha }}
322351
EOF
323352
@@ -332,6 +361,8 @@ jobs:
332361
name: vite-plus v${{ env.VERSION }}
333362
tag_name: v${{ env.VERSION }}
334363
target_commitish: ${{ github.sha }}
364+
files: |
365+
installer-release/vp-setup-*.exe
335366
336367
- name: Send Discord notification
337368
if: ${{ inputs.npm_tag == 'latest' }}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ on:
88
paths:
99
- 'packages/cli/install.sh'
1010
- 'packages/cli/install.ps1'
11+
- 'crates/vite_installer/**'
12+
- 'crates/vite_setup/**'
1113
- '.github/workflows/test-standalone-install.yml'
1214

1315
concurrency:
@@ -786,3 +788,57 @@ jobs:
786788
which npm
787789
which npx
788790
which vp
791+
792+
test-vp-setup-exe:
793+
name: Test vp-setup.exe (pwsh)
794+
runs-on: windows-latest
795+
permissions:
796+
contents: read
797+
steps:
798+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
799+
- uses: ./.github/actions/clone
800+
801+
- name: Setup Dev Drive
802+
uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0
803+
with:
804+
drive-size: 12GB
805+
drive-format: ReFS
806+
env-mapping: |
807+
CARGO_HOME,{{ DEV_DRIVE }}/.cargo
808+
RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup
809+
810+
- uses: oxc-project/setup-rust@23f38cfb0c04af97a055f76acee94d5be71c7c82 # v1.0.16
811+
with:
812+
target-dir: ${{ format('{0}/target', env.DEV_DRIVE) }}
813+
814+
- name: Build vp-setup.exe
815+
shell: bash
816+
run: cargo build --release -p vite_installer
817+
818+
- name: Install via vp-setup.exe (silent)
819+
shell: pwsh
820+
run: ${{ format('{0}/target/release/vp-setup.exe', env.DEV_DRIVE) }}
821+
env:
822+
VP_VERSION: alpha
823+
824+
- name: Set PATH
825+
shell: bash
826+
run: echo "$USERPROFILE/.vite-plus/bin" >> $GITHUB_PATH
827+
828+
- name: Verify installation
829+
shell: pwsh
830+
run: |
831+
vp --version
832+
vp --help
833+
834+
- name: Verify installation (cmd)
835+
shell: cmd
836+
run: |
837+
vp --version
838+
vp --help
839+
840+
- name: Verify installation (bash)
841+
shell: bash
842+
run: |
843+
vp --version
844+
vp --help

.typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
ratatui = "ratatui"
33
PUNICODE = "PUNICODE"
44
Jod = "Jod" # Node.js v22 LTS codename
5+
flate = "flate" # flate2 crate name (gzip/deflate compression)
56

67
[files]
78
extend-exclude = [

Cargo.lock

Lines changed: 48 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ vite_js_runtime = { path = "crates/vite_js_runtime" }
197197
vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "076cef486127e6cd1fefc58945f00dac316888ca" }
198198
vite_install = { path = "crates/vite_install" }
199199
vite_migration = { path = "crates/vite_migration" }
200+
vite_setup = { path = "crates/vite_setup" }
200201
vite_shared = { path = "crates/vite_shared" }
201202
vite_static_config = { path = "crates/vite_static_config" }
202203
vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "076cef486127e6cd1fefc58945f00dac316888ca" }
@@ -206,6 +207,7 @@ vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev =
206207
walkdir = "2.5.0"
207208
wax = "0.6.0"
208209
which = "8.0.0"
210+
winreg = "0.56.0"
209211
xxhash-rust = "0.8.15"
210212
zip = "7.2"
211213

@@ -334,3 +336,7 @@ panic = "abort" # Let it crash and force ourselves to write safe Rust.
334336
# size instead of speed. This reduces it from ~200KB to ~100KB on Windows.
335337
[profile.release.package.vite_trampoline]
336338
opt-level = "z"
339+
340+
# The installer binary is downloaded by users, so optimize for size.
341+
[profile.release.package.vite_installer]
342+
opt-level = "z"

crates/vite_global_cli/Cargo.toml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,13 @@ name = "vp"
1212
path = "src/main.rs"
1313

1414
[dependencies]
15-
base64-simd = { workspace = true }
1615
chrono = { workspace = true }
1716
clap = { workspace = true, features = ["derive"] }
1817
clap_complete = { workspace = true, features = ["unstable-dynamic"] }
1918
directories = { workspace = true }
20-
flate2 = { workspace = true }
2119
serde = { workspace = true }
2220
serde_json = { workspace = true }
2321
node-semver = { workspace = true }
24-
sha2 = { workspace = true }
25-
tar = { workspace = true }
2622
thiserror = { workspace = true }
2723
tokio = { workspace = true, features = ["full"] }
2824
tracing = { workspace = true }
@@ -34,13 +30,11 @@ vite_install = { workspace = true }
3430
vite_js_runtime = { workspace = true }
3531
vite_path = { workspace = true }
3632
vite_command = { workspace = true }
33+
vite_setup = { workspace = true }
3734
vite_shared = { workspace = true }
3835
vite_str = { workspace = true }
3936
vite_workspace = { workspace = true }
4037

41-
[target.'cfg(windows)'.dependencies]
42-
junction = { workspace = true }
43-
4438
[dev-dependencies]
4539
serial_test = { workspace = true }
4640
tempfile = { workspace = true }

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@
33
//! Downloads and installs a new version of the CLI from the npm registry
44
//! with SHA-512 integrity verification.
55
6-
mod install;
7-
mod integrity;
8-
mod platform;
9-
pub(crate) mod registry;
10-
116
use std::process::ExitStatus;
127

138
use owo_colors::OwoColorize;
149
use vite_install::request::HttpClient;
1510
use vite_path::AbsolutePathBuf;
11+
use vite_setup::{install, integrity, platform, registry};
1612
use vite_shared::output;
1713

1814
use crate::{commands::env::config::get_vp_home, error::Error};
@@ -35,9 +31,6 @@ pub struct UpgradeOptions {
3531
pub registry: Option<String>,
3632
}
3733

38-
/// Maximum number of old versions to keep.
39-
const MAX_VERSIONS_KEEP: usize = 5;
40-
4134
/// Execute the upgrade command.
4235
#[allow(clippy::print_stdout, clippy::print_stderr)]
4336
pub async fn execute(options: UpgradeOptions) -> Result<ExitStatus, Error> {
@@ -154,7 +147,7 @@ async fn install_platform_and_main(
154147
install::extract_platform_package(platform_data, version_dir).await?;
155148

156149
// Verify binary was extracted
157-
let binary_name = if cfg!(windows) { "vp.exe" } else { "vp" };
150+
let binary_name = vite_setup::VP_BINARY_NAME;
158151
let binary_path = version_dir.join("bin").join(binary_name);
159152
if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) {
160153
return Err(Error::Upgrade(
@@ -184,7 +177,8 @@ async fn install_platform_and_main(
184177
if let Some(ref prev) = previous_version {
185178
protected.push(prev.as_str());
186179
}
187-
if let Err(e) = install::cleanup_old_versions(install_dir, MAX_VERSIONS_KEEP, &protected).await
180+
if let Err(e) =
181+
install::cleanup_old_versions(install_dir, vite_setup::MAX_VERSIONS_KEEP, &protected).await
188182
{
189183
output::warn(&format!("Old version cleanup failed (non-fatal): {e}"));
190184
}

crates/vite_global_cli/src/error.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ pub enum Error {
5252
#[error("Upgrade error: {0}")]
5353
Upgrade(Str),
5454

55-
#[error("Integrity mismatch: expected {expected}, got {actual}")]
56-
IntegrityMismatch { expected: Str, actual: Str },
57-
58-
#[error("Unsupported integrity format: {0} (only sha512 is supported)")]
59-
UnsupportedIntegrity(Str),
55+
#[error("{0}")]
56+
Setup(#[from] vite_setup::error::Error),
6057
}

crates/vite_global_cli/src/upgrade_check.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ use std::{
1111

1212
use owo_colors::OwoColorize;
1313
use serde::{Deserialize, Serialize};
14-
15-
use crate::commands::upgrade::registry;
14+
use vite_setup::registry;
1615

1716
const CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
1817
const PROMPT_INTERVAL_SECS: u64 = 24 * 60 * 60;

0 commit comments

Comments
 (0)