Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/actions/build-upstream/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,20 @@ runs:
env:
INPUTS_TARGET: ${{ inputs.target }}

# The trampoline is excluded from the workspace and must build from its
# own directory so its .cargo/config.toml (build-std) applies. Pin
# CARGO_TARGET_DIR to an absolute path so the artifact still lands in the
# rust-target dir the cache and artifact paths expect. This runs on native
# Windows runners for release builds, so also treat drive-letter paths as
# absolute.
- name: Build trampoline shim binary (Windows only)
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'windows')
shell: bash
run: cargo build --release --target ${INPUTS_TARGET} -p vp_trampoline
run: |
target_dir="${CARGO_TARGET_DIR:-$PWD/target}"
case "$target_dir" in /*|[A-Za-z]:*) ;; *) target_dir="$PWD/$target_dir" ;; esac
cd crates/vp_trampoline
CARGO_TARGET_DIR="$target_dir" cargo build --release --target ${INPUTS_TARGET}
env:
INPUTS_TARGET: ${{ inputs.target }}

Expand Down
17 changes: 16 additions & 1 deletion .github/actions/build-windows-cli/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,26 @@ runs:
- name: Build Rust CLI binaries
if: steps.binaries-cache.outputs.cache-hit != 'true'
shell: bash
run: cargo xwin build --release --target x86_64-pc-windows-msvc -p vp_global_cli -p vp_trampoline -p vp_installer
run: cargo xwin build --release --target x86_64-pc-windows-msvc -p vp_global_cli -p vp_installer
env:
XWIN_ACCEPT_LICENSE: '1'
CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH

# The trampoline is excluded from the workspace and must build from its
# own directory so its .cargo/config.toml (build-std) applies. Pin
# CARGO_TARGET_DIR to an absolute path so the artifact still lands in the
# same target/ directory the artifact list above expects.
- name: Build trampoline shim binary
if: steps.binaries-cache.outputs.cache-hit != 'true'
shell: bash
run: |
target_dir="${CARGO_TARGET_DIR:-$PWD/target}"
case "$target_dir" in /*) ;; *) target_dir="$PWD/$target_dir" ;; esac
cd crates/vp_trampoline
CARGO_TARGET_DIR="$target_dir" cargo xwin build --release --target x86_64-pc-windows-msvc
env:
XWIN_ACCEPT_LICENSE: '1'

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact-name }}
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,13 @@ jobs:
# Keep the package selection in sync with the `test` recipe in justfile.
# vp_cli_snapshots is excluded there too: its snapshot suite needs a
# built vp and node at runtime and joins the Windows archive later.
# vp_trampoline is excluded from the workspace and its only test module
# is unix-only, so it has nothing to contribute to the Windows archive.
- name: Build test archive
run: |
eval "$(cargo xwin env --target x86_64-pc-windows-msvc | grep '^export ')"
unset RUSTFLAGS
cargo nextest archive $(for d in crates/*/; do n=$(basename $d); [ "$n" = "vp_cli_snapshots" ] || echo -n "-p $n "; done) -p vite-plus-cli \
cargo nextest archive $(for d in crates/*/; do n=$(basename $d); [ "$n" = "vp_cli_snapshots" ] || [ "$n" = "vp_trampoline" ] || echo -n "-p $n "; done) -p vite-plus-cli \
--target x86_64-pc-windows-msvc --archive-file windows-tests.tar.zst

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -335,6 +337,7 @@ jobs:
- run: |
cargo shear
cargo fmt --check
cargo fmt --manifest-path crates/vp_trampoline/Cargo.toml --check
just lint
# RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ vite

# PTY snapshot runner failure artifacts (reviewed via the diff, never committed)
crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/*/snapshots/*.md.new
# `cargo fmt/clippy --manifest-path crates/vp_trampoline/Cargo.toml` from the repo
# root does not read the crate config (target-dir), so it creates a nested
# target dir.
/crates/vp_trampoline/target
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ vite-plus/
├── crates/vp_setup/ # Setup helpers
├── crates/vp_shared/ # Shared Rust env config, tracing, output, utilities
├── crates/vp_static_config/ # Static extraction of vite.config.* data
└── crates/vp_trampoline/ # Windows shim trampoline
└── crates/vp_trampoline/ # Windows shim trampoline (standalone package, excluded from the workspace)
```

`packages/test` is no longer tracked. The public test API is `vite-plus/test*`, generated by `packages/cli/build.ts` as shims over upstream `vitest` and `@vitest/browser*` exports.
Expand Down
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[workspace]
resolver = "3"
members = ["bench", "crates/*", "packages/cli/binding"]
# vp_trampoline is a standalone package: it needs its own release profile with
# panic = "immediate-abort" (cargo ignores `panic` in per-package profile
# overrides) and a crate-local build-std config. See crates/vp_trampoline/Cargo.toml.
exclude = ["crates/vp_trampoline"]

[workspace.metadata.cargo-shear]
ignored = [
Expand Down Expand Up @@ -427,11 +431,6 @@ strip = "symbols" # set to `false` for debug information
debug = false # set to `true` for debug information
panic = "abort" # Let it crash and force ourselves to write safe Rust.

# The trampoline binary is copied per shim tool (~5-10 copies), so optimize for
# size instead of speed. This reduces it from ~200KB to ~100KB on Windows.
[profile.release.package.vp_trampoline]
opt-level = "z"

# The installer binary is downloaded by users, so optimize for size.
[profile.release.package.vp_installer]
opt-level = "z"
2 changes: 1 addition & 1 deletion crates/vp_cli_snapshots/tests/cli_snapshots/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ impl CaseHome {
.join("vp-shim.exe");
if !shim.is_file() {
return Err(format!(
"global vp trampoline template not found at {}; run `cargo build -p vp_trampoline`",
"global vp trampoline template not found at {}; run `cd crates/vp_trampoline && cargo build`",
shim.display()
));
}
Expand Down
24 changes: 24 additions & 0 deletions crates/vp_trampoline/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This config only applies when cargo runs from this directory (config
# discovery is cwd-based), which is why the trampoline must be built with
# `cd crates/vp_trampoline && cargo build ...` and not with `-p vp_trampoline`
# from the repo root.

[unstable]
# Recompile std with this crate's release profile (opt-level = "z" and
# panic = "immediate-abort"). Together with the no_main raw-Win32 source this
# takes the exe from ~208KB to ~8KB on x86_64-pc-windows-msvc. Requires the
# rust-src rustup component.
build-std = ["std", "panic_abort"]
# Replace std's default features (drops panic-unwind and backtrace, enables
# the size-optimized code paths). compiler-builtins-mem provides memcpy and
# friends from compiler_builtins instead of the CRT, which the #![no_main]
# entry point needs.
build-std-features = ["optimize_for_size", "compiler-builtins-mem"]
# Let `cargo test` run with the abort-family panic strategy.
panic-abort-tests = true

[build]
# Keep artifacts in the repo-root target/ directory, where CI steps, the
# snapshot runner, and install-global-cli expect them. This path resolves
# relative to this crate directory. A CARGO_TARGET_DIR env var still wins.
target-dir = "../../target"
7 changes: 7 additions & 0 deletions crates/vp_trampoline/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 52 additions & 10 deletions crates/vp_trampoline/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# This crate is excluded from the workspace on purpose (see the root
# Cargo.toml). It needs its own release profile with panic = "immediate-abort",
# which cargo ignores in per-package profile overrides, plus the crate-local
# .cargo/config.toml that enables build-std. Build it from this directory so
# that config applies:
#
# cd crates/vp_trampoline && cargo build --release [--target <triple>]
#
# Artifacts land in the repo-root target/ directory (see .cargo/config.toml),
# the same location as workspace builds. The build needs the pinned nightly
# toolchain and the rust-src component (both come from the repo
# rust-toolchain.toml).
#
# Size on x86_64-pc-windows-msvc: ~8KB, down from ~208KB when it was a
# workspace member built with the precompiled std. build-std recompiles std
# with this profile, panic = "immediate-abort" compiles out the panic
# formatting, unwinding, and backtrace machinery, and the Windows build uses
# #![no_main] with raw Win32 calls (src/win.rs) instead of
# std::process::Command. Background: rfcs/trampoline-exe-for-shims.md.
cargo-features = ["panic-immediate-abort"]

[package]
name = "vp_trampoline"
version = "0.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
authors = ["Vite+ Authors"]
edition = "2024"
license = "MIT"
publish = false
rust-version.workspace = true
description = "Minimal Windows trampoline exe for vite-plus shims"

[[bin]]
Expand All @@ -15,14 +35,36 @@ path = "src/main.rs"
# No dependencies — the single Win32 FFI call (SetConsoleCtrlHandler) is
# declared inline to avoid pulling in the heavy `windows`/`windows-core` crates.

# Override workspace lints: this is a standalone minimal binary that intentionally
# avoids dependencies on vp_shared, vt_path, vt_str, etc. to keep binary
# size small. It uses std types and macros directly.
# This crate does not inherit the workspace lints. It intentionally uses std
# types and macros directly instead of vp_shared, vt_path, vt_str, etc. to
# keep the binary size small; allow the repo-wide .clippy.toml restrictions
# that exist to funnel code through those crates.
[lints.clippy]
disallowed_macros = "allow"
disallowed_types = "allow"
disallowed_methods = "allow"

# Note: Release profile is defined at workspace root (Cargo.toml).
# The workspace already sets lto="fat", codegen-units=1, strip="symbols", panic="abort".
# For even smaller binaries, consider building this crate separately with opt-level="z".
[profile.release]
opt-level = "z"
lto = "fat"
codegen-units = 1
strip = "symbols"
# Stronger than "abort": panics become a bare abort with no message
# formatting, so core::fmt and std::panicking never get linked.
panic = "immediate-abort"
debug = false

# Debug builds must still optimize a little: at opt-level 0 the compiler can
# emit references to the MSVC unwinding helper __CxxFrameHandler3 even with
# panic = "immediate-abort", and the link fails (same constraint as
# uv-trampoline).
[profile.dev]
opt-level = 1
lto = true
panic = "immediate-abort"
debug = true

[profile.test]
inherits = "dev"

[workspace]
121 changes: 121 additions & 0 deletions crates/vp_trampoline/src/cmdline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Pure helpers over UTF-16 code units and bytes, shared by the Windows
//! implementation. They live outside win.rs so the unit tests run on every
//! platform.

const SPACE: u16 = b' ' as u16;
const TAB: u16 = b'\t' as u16;
const QUOTE: u16 = b'"' as u16;
const DOT: u16 = b'.' as u16;

/// Index where the raw command line's first (program) argument ends.
///
/// This follows the MSVC parsing rule for the program name: a quote toggles
/// quoted mode and backslashes have no escaping effect. The remainder
/// (`&cmdline[result..]`, leading whitespace included) is the argument tail to
/// forward to the child verbatim.
pub fn skip_program_argument(cmdline: &[u16]) -> usize {
let mut i = 0;
while i < cmdline.len() && (cmdline[i] == SPACE || cmdline[i] == TAB) {
i += 1;
}
let mut quoted = false;
while i < cmdline.len() {
let c = cmdline[i];
if c == QUOTE {
quoted = !quoted;
} else if (c == SPACE || c == TAB) && !quoted {
break;
}
i += 1;
}
i
}

/// Length of the file stem, matching `Path::file_stem`: everything before the
/// last `.`, except that a leading `.` never starts an extension.
pub fn file_stem_len(name: &[u16]) -> usize {
match name.iter().skip(1).rposition(|&c| c == DOT) {
Some(pos) => pos + 1,
None => name.len(),
}
}

/// Case-sensitive comparison of a UTF-16 slice against an ASCII string.
pub fn eq_ascii(wide: &[u16], ascii: &[u8]) -> bool {
wide.len() == ascii.len() && wide.iter().zip(ascii).all(|(&w, &a)| w == u16::from(a))
}

/// Format `value` as decimal ASCII into `buf`, returning the used suffix.
pub fn format_u32(mut value: u32, buf: &mut [u8; 10]) -> &[u8] {
let mut i = buf.len();
loop {
i -= 1;
buf[i] = b'0' + (value % 10) as u8;
value /= 10;
if value == 0 {
break;
}
}
&buf[i..]
}

#[cfg(test)]
mod tests {
use super::*;

fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}

#[test]
fn skips_unquoted_program() {
let cl = wide(r"C:\bin\node.exe --version");
assert_eq!(&cl[skip_program_argument(&cl)..], &wide(" --version")[..]);
}

#[test]
fn skips_quoted_program_with_spaces() {
let cl = wide(r#""C:\Program Files\node.exe" -e "1 + 1""#);
assert_eq!(&cl[skip_program_argument(&cl)..], &wide(r#" -e "1 + 1""#)[..]);
}

#[test]
fn skips_leading_whitespace_and_bare_program() {
let cl = wide(" node");
assert_eq!(skip_program_argument(&cl), cl.len());
assert_eq!(skip_program_argument(&[]), 0);
}

#[test]
fn keeps_argument_tail_verbatim() {
let cl = wide(r#"npx "a b\" literal" --flag"#);
assert_eq!(&cl[skip_program_argument(&cl)..], &wide(r#" "a b\" literal" --flag"#)[..]);
}

#[test]
fn file_stem_matches_path_file_stem() {
assert_eq!(file_stem_len(&wide("node.exe")), 4);
assert_eq!(file_stem_len(&wide("node")), 4);
assert_eq!(file_stem_len(&wide("NODE.EXE")), 4);
assert_eq!(file_stem_len(&wide("a.b.exe")), 3);
assert_eq!(file_stem_len(&wide(".hidden")), 7);
assert_eq!(file_stem_len(&wide("node.")), 4);
}

#[test]
fn eq_ascii_is_exact() {
assert!(eq_ascii(&wide("vp"), b"vp"));
assert!(!eq_ascii(&wide("VP"), b"vp"));
assert!(!eq_ascii(&wide("vpx"), b"vp"));
}

#[test]
fn formats_decimal() {
let mut buf = [0u8; 10];
assert_eq!(format_u32(0, &mut buf), b"0");
let mut buf = [0u8; 10];
assert_eq!(format_u32(203, &mut buf), b"203");
let mut buf = [0u8; 10];
assert_eq!(format_u32(u32::MAX, &mut buf), b"4294967295");
}
}
Loading
Loading