Skip to content

Commit aa4696e

Browse files
committed
feat(installer): add standalone Windows .exe installer (vp-setup.exe)
Add a standalone `vp-setup.exe` Windows installer binary that installs the vp CLI without requiring PowerShell, complementing the existing `irm https://vite.plus/ps1 | iex` script-based installer. - Create `vite_setup` shared library crate extracting installation logic (platform detection, registry queries, integrity verification, tarball extraction, symlink/junction management) from `vite_global_cli` - Create `vite_installer` binary crate producing `vp-setup.exe` with interactive prompts, silent mode (-y), progress bars, and Windows PATH modification via direct registry API (no PowerShell dependency) - Update `vite_global_cli` to use `vite_setup` instead of inline upgrade modules, ensuring upgrade and installer share identical logic - Add CI build/upload steps for installer in release workflow, attached as GitHub Release assets - Add RFC document at rfcs/windows-installer.md
1 parent f1f6016 commit aa4696e

File tree

20 files changed

+1484
-45
lines changed

20 files changed

+1484
-45
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' }}

Cargo.lock

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

Cargo.toml

Lines changed: 5 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 = "eb746ad3f35bd994ddb39be001eaf58986f48388" }
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 = "eb746ad3f35bd994ddb39be001eaf58986f48388" }
@@ -334,3 +335,7 @@ panic = "abort" # Let it crash and force ourselves to write safe Rust.
334335
# size instead of speed. This reduces it from ~200KB to ~100KB on Windows.
335336
[profile.release.package.vite_trampoline]
336337
opt-level = "z"
338+
339+
# The installer binary is downloaded by users, so optimize for size.
340+
[profile.release.package.vite_installer]
341+
opt-level = "z"

crates/vite_global_cli/Cargo.toml

Lines changed: 1 addition & 4 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,6 +30,7 @@ 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 }

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

Lines changed: 3 additions & 9 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> {
@@ -189,7 +182,8 @@ async fn install_platform_and_main(
189182
if let Some(ref prev) = previous_version {
190183
protected.push(prev.as_str());
191184
}
192-
if let Err(e) = install::cleanup_old_versions(install_dir, MAX_VERSIONS_KEEP, &protected).await
185+
if let Err(e) =
186+
install::cleanup_old_versions(install_dir, vite_setup::MAX_VERSIONS_KEEP, &protected).await
193187
{
194188
output::warn(&format!("Old version cleanup failed (non-fatal): {e}"));
195189
}

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{
1212
use owo_colors::OwoColorize;
1313
use serde::{Deserialize, Serialize};
1414

15-
use crate::commands::upgrade::registry;
15+
use vite_setup::registry;
1616

1717
const CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
1818
const PROMPT_INTERVAL_SECS: u64 = 24 * 60 * 60;

crates/vite_installer/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "vite_installer"
3+
version = "0.0.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
publish = false
8+
rust-version.workspace = true
9+
10+
[[bin]]
11+
name = "vp-setup"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
clap = { workspace = true, features = ["derive"] }
16+
indicatif = { workspace = true }
17+
owo-colors = { workspace = true }
18+
tokio = { workspace = true, features = ["full"] }
19+
tracing = { workspace = true }
20+
vite_install = { workspace = true }
21+
vite_path = { workspace = true }
22+
vite_setup = { workspace = true }
23+
vite_shared = { workspace = true }
24+
25+
[lints]
26+
workspace = true

crates/vite_installer/src/cli.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! CLI argument parsing for `vp-setup`.
2+
3+
use clap::Parser;
4+
5+
/// Vite+ Installer — standalone installer for the vp CLI.
6+
#[derive(Parser, Debug)]
7+
#[command(name = "vp-setup", about = "Install the Vite+ CLI")]
8+
struct Cli {
9+
/// Accept defaults without prompting (for CI/unattended installs)
10+
#[arg(short = 'y', long = "yes")]
11+
yes: bool,
12+
13+
/// Suppress all output except errors
14+
#[arg(short = 'q', long = "quiet")]
15+
quiet: bool,
16+
17+
/// Install a specific version (default: latest)
18+
#[arg(long = "version")]
19+
version: Option<String>,
20+
21+
/// npm dist-tag to install (default: latest)
22+
#[arg(long = "tag", default_value = "latest")]
23+
tag: String,
24+
25+
/// Custom installation directory (default: ~/.vite-plus)
26+
#[arg(long = "install-dir")]
27+
install_dir: Option<String>,
28+
29+
/// Custom npm registry URL
30+
#[arg(long = "registry")]
31+
registry: Option<String>,
32+
33+
/// Skip Node.js version manager setup
34+
#[arg(long = "no-node-manager")]
35+
no_node_manager: bool,
36+
37+
/// Do not modify the User PATH
38+
#[arg(long = "no-modify-path")]
39+
no_modify_path: bool,
40+
}
41+
42+
/// Parsed installation options.
43+
pub struct Options {
44+
pub yes: bool,
45+
pub quiet: bool,
46+
pub version: Option<String>,
47+
pub tag: String,
48+
pub install_dir: Option<String>,
49+
pub registry: Option<String>,
50+
pub no_node_manager: bool,
51+
pub no_modify_path: bool,
52+
}
53+
54+
/// Parse CLI arguments, merging with environment variables.
55+
///
56+
/// CLI flags take precedence over environment variables.
57+
pub fn parse() -> Options {
58+
let cli = Cli::parse();
59+
60+
// Environment variable overrides (CLI flags take precedence)
61+
let version = cli.version.or_else(|| std::env::var("VP_VERSION").ok());
62+
let install_dir = cli.install_dir.or_else(|| std::env::var("VP_HOME").ok());
63+
let registry = cli.registry.or_else(|| std::env::var("NPM_CONFIG_REGISTRY").ok());
64+
65+
let no_node_manager = cli.no_node_manager
66+
|| std::env::var("VP_NODE_MANAGER")
67+
.ok()
68+
.is_some_and(|v| v.eq_ignore_ascii_case("no"));
69+
70+
// quiet implies yes
71+
let yes = cli.yes || cli.quiet;
72+
73+
Options {
74+
yes,
75+
quiet: cli.quiet,
76+
version,
77+
tag: cli.tag,
78+
install_dir,
79+
registry,
80+
no_node_manager,
81+
no_modify_path: cli.no_modify_path,
82+
}
83+
}

0 commit comments

Comments
 (0)