Skip to content
Merged
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
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 @@ -1495,6 +1484,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