Skip to content

Commit 1b3a6a3

Browse files
committed
feat(installer): implement remaining RFC gaps
- Add interactive "Customize installation" submenu (option 2) allowing users to change version, registry, Node.js manager, and PATH settings - Add env file creation via `vp env setup --env-only` when Node.js manager is skipped (ensures shell env files exist in all code paths) - Add build.rs with /DEPENDENTLOADFLAG:0x800 linker flag for DLL hijacking prevention at load time (complements runtime mitigation) - Add test-vp-setup-exe CI job to test-standalone-install.yml testing silent installation from cmd, pwsh, and bash on Windows
1 parent 0264102 commit 1b3a6a3

File tree

3 files changed

+154
-36
lines changed

3 files changed

+154
-36
lines changed

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

Lines changed: 35 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:
@@ -626,3 +628,36 @@ jobs:
626628
which npm
627629
which npx
628630
which vp
631+
632+
test-vp-setup-exe:
633+
name: Test vp-setup.exe (${{ matrix.shell }})
634+
runs-on: windows-latest
635+
permissions:
636+
contents: read
637+
strategy:
638+
fail-fast: false
639+
matrix:
640+
shell: [cmd, pwsh, bash]
641+
steps:
642+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
643+
- uses: oxc-project/setup-rust@23f38cfb0c04af97a055f76acee94d5be71c7c82 # v1.0.16
644+
645+
- name: Build vp-setup.exe
646+
shell: bash
647+
run: cargo build --release -p vite_installer
648+
649+
- name: Install via vp-setup.exe (silent)
650+
shell: ${{ matrix.shell }}
651+
run: ./target/release/vp-setup.exe -y
652+
env:
653+
VP_VERSION: alpha
654+
655+
- name: Set PATH
656+
shell: bash
657+
run: echo "$USERPROFILE/.vite-plus/bin" >> $GITHUB_PATH
658+
659+
- name: Verify installation
660+
shell: ${{ matrix.shell }}
661+
run: |
662+
vp --version
663+
vp --help

crates/vite_installer/build.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fn main() {
2+
// On Windows, set DEPENDENTLOADFLAG to only search system32 for DLLs at load time.
3+
// This prevents DLL hijacking when the installer is downloaded to a folder
4+
// containing malicious DLLs (e.g. Downloads). Matches rustup's approach.
5+
#[cfg(windows)]
6+
println!("cargo:rustc-link-arg=/DEPENDENTLOADFLAG:0x800");
7+
}

crates/vite_installer/src/main.rs

Lines changed: 112 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn main() {
5252
}
5353

5454
#[allow(clippy::print_stdout, clippy::print_stderr)]
55-
async fn run(opts: cli::Options) -> i32 {
55+
async fn run(mut opts: cli::Options) -> i32 {
5656
let install_dir = match resolve_install_dir(&opts) {
5757
Ok(dir) => dir,
5858
Err(e) => {
@@ -63,7 +63,7 @@ async fn run(opts: cli::Options) -> i32 {
6363
let install_dir_display = install_dir.as_path().to_string_lossy().to_string();
6464

6565
if !opts.yes {
66-
let proceed = show_interactive_menu(&opts, &install_dir_display);
66+
let proceed = show_interactive_menu(&mut opts, &install_dir_display);
6767
if !proceed {
6868
println!("Installation cancelled.");
6969
return 0;
@@ -170,6 +170,9 @@ async fn do_install(
170170
print_info("setting up Node.js version manager...");
171171
}
172172
install::refresh_shims(install_dir).await?;
173+
} else {
174+
// When skipping Node.js manager, still create shell env files
175+
create_env_files(install_dir).await;
173176
}
174177

175178
if let Err(e) = install::cleanup_old_versions(
@@ -281,6 +284,25 @@ async fn download_with_progress(
281284
Ok(data)
282285
}
283286

287+
/// Create shell env files by spawning `vp env setup --env-only`.
288+
async fn create_env_files(install_dir: &vite_path::AbsolutePath) {
289+
let vp_binary =
290+
install_dir.join("current").join("bin").join(if cfg!(windows) { "vp.exe" } else { "vp" });
291+
292+
if !tokio::fs::try_exists(&vp_binary).await.unwrap_or(false) {
293+
return;
294+
}
295+
296+
let output = tokio::process::Command::new(vp_binary.as_path())
297+
.args(["env", "setup", "--env-only"])
298+
.output()
299+
.await;
300+
301+
if let Err(e) = output {
302+
tracing::warn!("Failed to create env files (non-fatal): {e}");
303+
}
304+
}
305+
284306
fn resolve_install_dir(opts: &cli::Options) -> Result<AbsolutePathBuf, Box<dyn std::error::Error>> {
285307
if let Some(ref dir) = opts.install_dir {
286308
let path = std::path::PathBuf::from(dir);
@@ -312,44 +334,98 @@ fn modify_path(bin_dir: &str, quiet: bool) -> Result<(), Box<dyn std::error::Err
312334
}
313335

314336
#[allow(clippy::print_stdout)]
315-
fn show_interactive_menu(opts: &cli::Options, install_dir: &str) -> bool {
316-
let version = opts.version.as_deref().unwrap_or("latest");
317-
let bin_dir = format!("{install_dir}{sep}bin", sep = std::path::MAIN_SEPARATOR);
318-
319-
println!();
320-
println!(" {}", "Welcome to Vite+ Installer!".bold());
321-
println!();
322-
println!(" This will install the {} CLI and monorepo task runner.", "vp".cyan());
323-
println!();
324-
println!(" Install directory: {}", install_dir.cyan());
325-
println!(
326-
" PATH modification: {}",
327-
if opts.no_modify_path {
328-
"no".to_string()
329-
} else {
330-
format!("{bin_dir} \u{2192} User PATH")
337+
fn show_interactive_menu(opts: &mut cli::Options, install_dir: &str) -> bool {
338+
loop {
339+
let version = opts.version.as_deref().unwrap_or("latest");
340+
let bin_dir = format!("{install_dir}{sep}bin", sep = std::path::MAIN_SEPARATOR);
341+
342+
println!();
343+
println!(" {}", "Welcome to Vite+ Installer!".bold());
344+
println!();
345+
println!(" This will install the {} CLI and monorepo task runner.", "vp".cyan());
346+
println!();
347+
println!(" Install directory: {}", install_dir.cyan());
348+
println!(
349+
" PATH modification: {}",
350+
if opts.no_modify_path {
351+
"no".to_string()
352+
} else {
353+
format!("{bin_dir} \u{2192} User PATH")
354+
}
355+
.cyan()
356+
);
357+
println!(" Version: {}", version.cyan());
358+
println!(
359+
" Node.js manager: {}",
360+
if opts.no_node_manager { "disabled" } else { "auto-detect" }.cyan()
361+
);
362+
println!();
363+
println!(" 1) {} (default)", "Proceed with installation".bold());
364+
println!(" 2) Customize installation");
365+
println!(" 3) Cancel");
366+
println!();
367+
368+
let choice = read_input(" > ");
369+
match choice.as_str() {
370+
"" | "1" => return true,
371+
"2" => show_customize_menu(opts),
372+
"3" => return false,
373+
_ => {
374+
println!(" Invalid choice. Please enter 1, 2, or 3.");
375+
}
331376
}
332-
.cyan()
333-
);
334-
println!(" Version: {}", version.cyan());
335-
println!(
336-
" Node.js manager: {}",
337-
if opts.no_node_manager { "disabled" } else { "auto-detect" }.cyan()
338-
);
339-
println!();
340-
println!(" 1) {} (default)", "Proceed with installation".bold());
341-
println!(" 2) Cancel");
342-
println!();
343-
print!(" > ");
344-
let _ = io::stdout().flush();
377+
}
378+
}
345379

346-
let mut input = String::new();
347-
if io::stdin().read_line(&mut input).is_err() {
348-
return false;
380+
#[allow(clippy::print_stdout)]
381+
fn show_customize_menu(opts: &mut cli::Options) {
382+
loop {
383+
let version_display = opts.version.as_deref().unwrap_or("latest");
384+
let registry_display = opts.registry.as_deref().unwrap_or("(default)");
385+
386+
println!();
387+
println!(" {}", "Customize installation:".bold());
388+
println!();
389+
println!(" 1) Version: [{}]", version_display.cyan());
390+
println!(" 2) npm registry: [{}]", registry_display.cyan());
391+
println!(
392+
" 3) Node.js manager: [{}]",
393+
if opts.no_node_manager { "disabled" } else { "auto-detect" }.cyan()
394+
);
395+
println!(
396+
" 4) Modify PATH: [{}]",
397+
if opts.no_modify_path { "no" } else { "yes" }.cyan()
398+
);
399+
println!();
400+
401+
let choice = read_input(" Enter option number to change, or press Enter to go back: ");
402+
match choice.as_str() {
403+
"" => return,
404+
"1" => {
405+
let v = read_input(" Version (e.g. 0.3.0, or 'latest'): ");
406+
if v == "latest" || v.is_empty() {
407+
opts.version = None;
408+
} else {
409+
opts.version = Some(v);
410+
}
411+
}
412+
"2" => {
413+
let r = read_input(" npm registry URL (or empty for default): ");
414+
opts.registry = if r.is_empty() { None } else { Some(r) };
415+
}
416+
"3" => opts.no_node_manager = !opts.no_node_manager,
417+
"4" => opts.no_modify_path = !opts.no_modify_path,
418+
_ => println!(" Invalid option."),
419+
}
349420
}
421+
}
350422

351-
let choice = input.trim();
352-
choice.is_empty() || choice == "1"
423+
fn read_input(prompt: &str) -> String {
424+
print!("{prompt}");
425+
let _ = io::stdout().flush();
426+
let mut input = String::new();
427+
let _ = io::stdin().read_line(&mut input);
428+
input.trim().to_string()
353429
}
354430

355431
#[allow(clippy::print_stdout)]

0 commit comments

Comments
 (0)