Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "install-yarn-corepack-hash",
"private": true,
"scripts": {
"smoke": "vpt print yarn hash accepted"
},
"packageManager": "yarn@4.17.1+sha512.ccbfabf7d7b6b32075088be9386fb9a2e00bb6887ef07fa56effabc890a56d53da1ccc4128d62db245fcbd3961b236d75335bdf7d5320ed6eafb7588b7ad4697"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[[case]]
name = "install_yarn_corepack_hash"
vp = "global"
env = { YARN_ENABLE_TELEMETRY = "0" }
steps = [
{ argv = ["vpt", "rm", "-rf", "$VP_HOME/package_manager/yarn/4.17.1", "$VP_HOME/package_manager/yarn/4.17.1.lock"], comment = "Ensure the Corepack-pinned Yarn version is not cached", snapshot = false },
{ argv = ["vpt", "stat-file", "$VP_HOME/package_manager/yarn/4.17.1", "--assert", "missing"], comment = "Yarn 4.17.1 is not in the cache" },
{ argv = ["vp", "install"], comment = "A first install accepts the hash that Corepack wrote", timeout = 120000 },
{ argv = ["vpt", "stat-file", "$VP_HOME/package_manager/yarn/4.17.1/yarn/bin/yarn.js", "--assert", "file"], comment = "The cache holds the Yarn CLI that vp verified" },
]

[[case]]
name = "install_yarn_corepack_hash_mismatch"
vp = "global"
env = { YARN_ENABLE_TELEMETRY = "0" }
steps = [
{ argv = ["vpt", "rm", "-rf", "$VP_HOME/package_manager/yarn/4.17.1", "$VP_HOME/package_manager/yarn/4.17.1.lock"], comment = "Ensure the Corepack-pinned Yarn version is not cached", snapshot = false },
{ argv = ["vp", "install"], comment = "Cache the verified Yarn CLI", timeout = 120000, snapshot = false },
{ argv = ["vpt", "replace-file-content", "package.json", "b7ad4697", "b7ad4698"], comment = "Change the pin to a hash that the cached CLI does not match", snapshot = false },
{ argv = ["vp", "install"], comment = "The error names the artifact that the hash covers. vp does not download the CLI again", continue-on-failure = true },
]

[[case]]
name = "run_yarn_corepack_hash"
vp = "global"
env = { YARN_ENABLE_TELEMETRY = "0" }
steps = [
{ argv = ["vpt", "rm", "-rf", "$VP_HOME/package_manager/yarn/4.17.1", "$VP_HOME/package_manager/yarn/4.17.1.lock"], comment = "Ensure the Corepack-pinned Yarn version is not cached", snapshot = false },
{ argv = ["vpt", "stat-file", "$VP_HOME/package_manager/yarn/4.17.1", "--assert", "missing"], comment = "Yarn 4.17.1 is not in the cache" },
{ argv = ["vp", "run", "smoke"], comment = "A first vp run accepts the hash and runs the task", timeout = 120000 },
{ argv = ["vpt", "stat-file", "$VP_HOME/package_manager/yarn/4.17.1/yarn/bin/yarn.js", "--assert", "file"], comment = "vp run wrote the verified Yarn CLI to the cache" },
{ argv = ["vp", "run", "smoke"], comment = "A second vp run uses the cached Yarn CLI" },
]

[[case]]
name = "run_yarn_corepack_hash_mismatch"
vp = "global"
env = { YARN_ENABLE_TELEMETRY = "0" }
steps = [
{ argv = ["vpt", "rm", "-rf", "$VP_HOME/package_manager/yarn/4.17.1", "$VP_HOME/package_manager/yarn/4.17.1.lock"], comment = "Ensure the Corepack-pinned Yarn version is not cached", snapshot = false },
{ argv = ["vp", "install"], comment = "Cache the verified Yarn CLI", timeout = 120000, snapshot = false },
{ argv = ["vpt", "replace-file-content", "package.json", "b7ad4697", "b7ad4698"], comment = "Change the pin to a hash that the cached CLI does not match", snapshot = false },
{ argv = ["vp", "run", "smoke"], comment = "vp run reports the integrity failure. It does not start the task", continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# install_yarn_corepack_hash

## `vpt rm -rf $VP_HOME/package_manager/yarn/4.17.1 $VP_HOME/package_manager/yarn/4.17.1.lock`

Ensure the Corepack-pinned Yarn version is not cached


## `vpt stat-file $VP_HOME/package_manager/yarn/4.17.1 --assert missing`

Yarn 4.17.1 is not in the cache

```
<home>/.vite-plus/package_manager/yarn/<version>: missing
```

## `vp install`

A first install accepts the hash that Corepack wrote

```
VITE+ - The Unified Toolchain for the Web

➤ YN0000: · Yarn <version>
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: · Done in <duration> <duration>
```

## `vpt stat-file $VP_HOME/package_manager/yarn/4.17.1/yarn/bin/yarn.js --assert file`

The cache holds the Yarn CLI that vp verified

```
<home>/.vite-plus/package_manager/yarn/<version>/yarn/bin/yarn.js: file
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# install_yarn_corepack_hash_mismatch

## `vpt rm -rf $VP_HOME/package_manager/yarn/4.17.1 $VP_HOME/package_manager/yarn/4.17.1.lock`

Ensure the Corepack-pinned Yarn version is not cached


## `vp install`

Cache the verified Yarn CLI


## `vpt replace-file-content package.json b7ad4697 b7ad4698`

Change the pin to a hash that the cached CLI does not match


## `vp install`

The error names the artifact that the hash covers. vp does not download the CLI again

**Exit code:** 1

```
VITE+ - The Unified Toolchain for the Web

error: Hash mismatch for yarn@4.17.1: expected sha512.ccbfabf7d7b6b32075088be9386fb9a2e00bb6887ef07fa56effabc890a56d53da1ccc4128d62db245fcbd3961b236d75335bdf7d5320ed6eafb7588b7ad4698, got sha512.ccbfabf7d7b6b32075088be9386fb9a2e00bb6887ef07fa56effabc890a56d53da1ccc4128d62db245fcbd3961b236d75335bdf7d5320ed6eafb7588b7ad4697
The `packageManager` hash covers the extracted Yarn CLI (bin/yarn.js). Corepack hashes the same artifact.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# run_yarn_corepack_hash

## `vpt rm -rf $VP_HOME/package_manager/yarn/4.17.1 $VP_HOME/package_manager/yarn/4.17.1.lock`

Ensure the Corepack-pinned Yarn version is not cached


## `vpt stat-file $VP_HOME/package_manager/yarn/4.17.1 --assert missing`

Yarn 4.17.1 is not in the cache

```
<home>/.vite-plus/package_manager/yarn/<version>: missing
```

## `vp run smoke`

A first vp run accepts the hash and runs the task

```
VITE+ - The Unified Toolchain for the Web

$ vpt print yarn hash accepted ⊘ cache disabled
yarn hash accepted
```

## `vpt stat-file $VP_HOME/package_manager/yarn/4.17.1/yarn/bin/yarn.js --assert file`

vp run wrote the verified Yarn CLI to the cache

```
<home>/.vite-plus/package_manager/yarn/<version>/yarn/bin/yarn.js: file
```

## `vp run smoke`

A second vp run uses the cached Yarn CLI

```
VITE+ - The Unified Toolchain for the Web

$ vpt print yarn hash accepted ⊘ cache disabled
yarn hash accepted
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# run_yarn_corepack_hash_mismatch

## `vpt rm -rf $VP_HOME/package_manager/yarn/4.17.1 $VP_HOME/package_manager/yarn/4.17.1.lock`

Ensure the Corepack-pinned Yarn version is not cached


## `vp install`

Cache the verified Yarn CLI


## `vpt replace-file-content package.json b7ad4697 b7ad4698`

Change the pin to a hash that the cached CLI does not match


## `vp run smoke`

vp run reports the integrity failure. It does not start the task

**Exit code:** 1

```
VITE+ - The Unified Toolchain for the Web

error: Hash mismatch for yarn@4.17.1: expected sha512.ccbfabf7d7b6b32075088be9386fb9a2e00bb6887ef07fa56effabc890a56d53da1ccc4128d62db245fcbd3961b236d75335bdf7d5320ed6eafb7588b7ad4698, got sha512.ccbfabf7d7b6b32075088be9386fb9a2e00bb6887ef07fa56effabc890a56d53da1ccc4128d62db245fcbd3961b236d75335bdf7d5320ed6eafb7588b7ad4697
The `packageManager` hash covers the extracted Yarn CLI (bin/yarn.js). Corepack hashes the same artifact.
```
37 changes: 37 additions & 0 deletions crates/vp_error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ pub enum Error {
#[error("Hash mismatch: expected {expected}, got {actual}")]
HashMismatch { expected: Str, actual: Str },

/// A `packageManager` integrity pin does not match the artifact it covers.
///
/// This variant boxes its payload. Without the box, it makes every
/// `Result<_, Error>` in the CLI larger (`clippy::result_large_err`).
#[error(transparent)]
PackageManagerHashMismatch(#[from] Box<PackageManagerHashMismatch>),

#[error("Invalid hash format: {0}")]
InvalidHashFormat(Str),

Expand All @@ -131,3 +138,33 @@ pub enum Error {
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}

impl Error {
/// Whether the error says that an artifact failed its integrity check.
///
/// Some callers continue when a managed tool is missing. They must stop for
/// this error. The user must fix an unverified artifact, and a fallback
/// hides the cause behind a later, unrelated failure.
#[must_use]
pub const fn is_integrity_failure(&self) -> bool {
matches!(self, Self::PackageManagerHashMismatch(_) | Self::HashMismatch { .. })
}
}

/// Details of a failed `packageManager` integrity check.
///
/// `basis` names the artifact that vp hashed. Corepack hashes the extracted CLI
/// for Yarn 2+, and the npm tarball for every other package manager. A message
/// that says only "hash mismatch" reads like a corrupt download.
#[derive(Error, Debug)]
#[error(
"Hash mismatch for {name}@{version}: expected {expected}, got {actual}\n\
The `packageManager` hash covers {basis}. Corepack hashes the same artifact."
)]
pub struct PackageManagerHashMismatch {
pub name: Str,
pub version: Str,
pub expected: Str,
pub actual: Str,
pub basis: Str,
}
58 changes: 42 additions & 16 deletions crates/vp_global_cli/src/shim/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
//! 3. Tool execution (core tools and package binaries)

use vp_pm_cli::{
PackageManagerType, download_package_manager, package_manager_bin_path,
package_manager_install_dir, resolve_package_manager_from_package_json,
PackageManagerType, ensure_package_manager_bin, resolve_package_manager_from_package_json,
};
use vp_shared::{PrependOptions, env_vars, output, prepend_to_path_env};
use vt_path::{AbsolutePath, AbsolutePathBuf, current_dir};
Expand Down Expand Up @@ -686,24 +685,14 @@ async fn resolve_matching_package_manager_tool(
}

let bin_name = expected_type.bin_name_for_tool(tool);

// Fast path: if the managed install already exists, skip download_package_manager
// entirely. The slow path stats three files (`bin`, `.cmd`, `.ps1`) on every
// invocation, which adds up on the shim hot path.
if let Some(install_dir) = package_manager_install_dir(expected_type, &resolution.version) {
let bin_path = package_manager_bin_path(&install_dir, bin_name);
if bin_path.as_path().exists() {
return Ok(Some(bin_path));
}
}

let (install_dir, _, _) = download_package_manager(
resolution.package_manager_type,
let bin_path = ensure_package_manager_bin(
expected_type,
&resolution.version,
resolution.hash.as_deref(),
bin_name,
)
.await?;
Ok(Some(package_manager_bin_path(&install_dir, bin_name)))
Ok(Some(bin_path))
}

async fn prepend_js_child_process_path_env(
Expand Down Expand Up @@ -1491,6 +1480,43 @@ mod tests {
}
}

#[tokio::test]
#[serial]
async fn test_hash_pinned_modern_yarn_rechecks_cached_cli() {
let temp = TempDir::new().unwrap();
let vp_home = AbsolutePathBuf::new(temp.path().join("vp-home")).unwrap();
let cwd = AbsolutePathBuf::new(temp.path().join("project")).unwrap();
std::fs::create_dir_all(&cwd).unwrap();

let expected_hash = format!("sha512.{}", "0".repeat(128));
std::fs::write(
cwd.join("package.json"),
format!(r#"{{"packageManager":"yarn@4.17.1+{expected_hash}"}}"#),
)
.unwrap();

let bin_dir =
vp_home.join("package_manager").join("yarn").join("4.17.1").join("yarn").join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
std::fs::write(bin_dir.join("yarn"), "shim").unwrap();
std::fs::write(bin_dir.join("yarn.cmd"), "shim").unwrap();
std::fs::write(bin_dir.join("yarn.ps1"), "shim").unwrap();
std::fs::write(bin_dir.join("yarn.js"), "corrupt").unwrap();

let _guard = vp_shared::EnvConfig::test_guard(vp_shared::EnvConfig::for_test_with_home(
vp_home.as_path(),
));

let result = resolve_matching_package_manager_tool(&cwd, "yarn").await;
assert!(
matches!(
result,
Err(Error::Install(vp_error::Error::PackageManagerHashMismatch { .. }))
),
"the global Yarn shim must reject a corrupted pinned cache: {result:?}"
);
}

#[tokio::test]
#[serial]
async fn test_resolve_with_cache_bypasses_stale_lts_after_dev_engines_is_added() {
Expand Down
4 changes: 2 additions & 2 deletions crates/vp_pm_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ pub use dispatch::{DispatchResult, dispatch, dispatch_with_metadata};
pub use error::Error;
pub use package_manager::{
PackageManager, PackageManagerBuilder, PackageManagerResolution, PackageManagerSource,
PackageManagerType, download_package_manager, get_package_manager_type_and_version,
package_manager_bin_path, package_manager_install_dir,
PackageManagerType, download_package_manager, ensure_package_manager_bin,
get_package_manager_type_and_version, package_manager_bin_path, package_manager_install_dir,
resolve_package_manager_from_package_json,
};
pub use request::HttpClient;
Expand Down
Loading
Loading