mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-24 16:46:06 -04:00
fix(pacquet/store-dir): append STORE_VERSION to storeDir so pnpm and pacquet stay interchangeable (#11891)
* fix(store-dir): append STORE_VERSION to storeDir so pnpm and pacquet stay interchangeable pnpm's `getStorePath` appends `STORE_VERSION` (`"v11"`) to whatever the user configured, so the `.modules.yaml` it writes records the v11-suffixed path. pacquet stored the suffix only as an internal sub-path accessor (`StoreDir::v11`), which meant `config.store_dir.display()` — the value pacquet writes to `.modules.yaml`, prints from `pacquet store path`, and emits in the NDJSON `context` log — yielded the un-suffixed parent. Switching between the two CLIs in the same project tripped pnpm's `checkCompatibility` with `ERR_PNPM_UNEXPECTED_STORE`. Fix is centralised in `From<PathBuf> for StoreDir`, mirroring pnpm's `if (endsWith(v11)) return; else append(v11)` branch at store/path/src/index.ts:39-42. Every consumer reading from `StoreDir` (`display()`, `root()`, `files()`, `tmp()`, `links()`, `projects()`) now sees the v11-suffixed path through one source of truth, so the on-disk layout is unchanged and the externally-reported `storeDir` matches pnpm's exactly. Ref: https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42 * fix(store-dir): satisfy Perfectionist macro-trailing-comma on remaining multi-line assert * fix(store-dir): enforce STORE_VERSION suffix on deserialize via #[serde(from)] CodeRabbit flagged that the previous `#[serde(transparent)]` derive on `StoreDir` deserialised straight into `StoreDir::root`, bypassing the auto-append in `impl From<PathBuf> for StoreDir`. A persisted unsuffixed path would therefore violate the [`STORE_VERSION`] invariant on the live struct until the next reconstruction. Pacquet doesn't currently deserialize `StoreDir` from any disk shape, but the type-level guarantee is part of the public contract — future serialised state must round-trip through the suffix logic. Route both directions through `PathBuf` with `#[serde(from = "PathBuf", into = "PathBuf")]`. Deserialize now flows through `From<PathBuf>` (which applies the suffix); serialize converts to `PathBuf` and back to the same wire shape `transparent` produced, so no on-disk format change. `Clone` is required by `into` and was added. Also fix CodeRabbit's doc-comment nit at project_registry::register_skips_when_store_is_inside_project — the comment referenced `StoreDir::from` while the test calls `StoreDir::new`; clarified that `new` routes through `From<PathBuf>`. Added round-trip tests in `store_dir::tests`: - `deserialize_applies_store_version_to_unsuffixed_path` - `deserialize_preserves_already_suffixed_path`
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use command_extra::CommandExtra;
|
||||
use pacquet_store_dir::STORE_VERSION;
|
||||
use pacquet_testing_utils::bin::CommandTempCwd;
|
||||
use pipe_trait::Pipe;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -39,7 +40,11 @@ fn store_path_should_return_store_dir_from_pnpm_workspace_yaml() {
|
||||
let normalize = |path: &str| path.replace('\\', "/");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout).trim_end().pipe(normalize),
|
||||
canonicalize(&workspace).join("foo/bar").to_string_lossy().pipe_as_ref(normalize),
|
||||
canonicalize(&workspace)
|
||||
.join("foo/bar")
|
||||
.join(STORE_VERSION)
|
||||
.to_string_lossy()
|
||||
.pipe_as_ref(normalize),
|
||||
);
|
||||
|
||||
drop(root); // cleanup
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
resolve_child_concurrency_with_parallelism,
|
||||
};
|
||||
use crate::api::{EnvVar, GetCurrentDir, GetHomeDir};
|
||||
use pacquet_store_dir::StoreDir;
|
||||
use pacquet_store_dir::{STORE_VERSION, StoreDir};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
@@ -48,7 +48,7 @@ fn test_default_store_dir_with_pnpm_home_env() {
|
||||
}
|
||||
}
|
||||
let store_dir = default_store_dir::<EnvWithPnpmHome>();
|
||||
assert_eq!(display_store_dir(&store_dir), "/tmp/pnpm-home/store");
|
||||
assert_eq!(display_store_dir(&store_dir), format!("/tmp/pnpm-home/store/{STORE_VERSION}"));
|
||||
}
|
||||
|
||||
/// `default_store_dir`'s `XDG_DATA_HOME` branch fires only when
|
||||
@@ -77,7 +77,10 @@ fn test_default_store_dir_with_xdg_env() {
|
||||
}
|
||||
}
|
||||
let store_dir = default_store_dir::<EnvWithXdgDataHome>();
|
||||
assert_eq!(display_store_dir(&store_dir), "/tmp/xdg_data_home/pnpm/store");
|
||||
assert_eq!(
|
||||
display_store_dir(&store_dir),
|
||||
format!("/tmp/xdg_data_home/pnpm/store/{STORE_VERSION}"),
|
||||
);
|
||||
}
|
||||
|
||||
/// When neither `PNPM_HOME` nor `XDG_DATA_HOME` is set, the
|
||||
@@ -110,8 +113,8 @@ fn test_default_store_dir_falls_back_to_home_dir() {
|
||||
}
|
||||
let store_dir = default_store_dir::<NoEnvWithHome>();
|
||||
let expected = match std::env::consts::OS {
|
||||
"linux" => "/home/test-user/.local/share/pnpm/store",
|
||||
"macos" => "/home/test-user/Library/pnpm/store",
|
||||
"linux" => format!("/home/test-user/.local/share/pnpm/store/{STORE_VERSION}"),
|
||||
"macos" => format!("/home/test-user/Library/pnpm/store/{STORE_VERSION}"),
|
||||
other => panic!("unexpected target OS in test: {other}"),
|
||||
};
|
||||
assert_eq!(display_store_dir(&store_dir), expected);
|
||||
|
||||
@@ -1222,13 +1222,17 @@ impl Config {
|
||||
// diverges from TypeScript's case-sensitive program when the
|
||||
// workspace is case-sensitive and the home is not.
|
||||
if !store_dir_explicit && let Some(home_dir) = Sys::home_dir() {
|
||||
// The "pnpm home dir" pnpm probes against is the parent of
|
||||
// the home store (`~/Library/pnpm` for `~/Library/pnpm/store`).
|
||||
// Fall back to the user's actual home if no parent is
|
||||
// available — same-volume linkability is what we're after,
|
||||
// and the home dir is on the same volume as any of its
|
||||
// children.
|
||||
let store_root = self.store_dir.root().to_path_buf();
|
||||
// `store_dir.root()` already carries the [`STORE_VERSION`]
|
||||
// suffix that [`StoreDir::from`] applied, so the
|
||||
// un-suffixed home store sits one level above. The "pnpm
|
||||
// home dir" pnpm probes against is the parent of that
|
||||
// un-suffixed home store (`~/Library/pnpm` for
|
||||
// `~/Library/pnpm/store`). Fall back to the user's actual
|
||||
// home whenever a parent is unavailable — same-volume
|
||||
// linkability is what we're after, and the home dir is on
|
||||
// the same volume as any of its children.
|
||||
let store_root_versioned = self.store_dir.root().to_path_buf();
|
||||
let store_root = store_root_versioned.parent().unwrap_or(&home_dir).to_path_buf();
|
||||
let pnpm_home_dir = store_root.parent().unwrap_or(&home_dir).to_path_buf();
|
||||
let resolved =
|
||||
store_path::resolve_store_dir::<Sys>(store_root, &pnpm_home_dir, start_dir);
|
||||
@@ -1423,7 +1427,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let store_dir = default_store_dir::<EnvWithPnpmHome>();
|
||||
assert_eq!(display_store_dir(&store_dir), "/hello/store");
|
||||
assert_eq!(
|
||||
display_store_dir(&store_dir),
|
||||
format!("/hello/store/{}", pacquet_store_dir::STORE_VERSION),
|
||||
);
|
||||
}
|
||||
|
||||
/// Companion to [`should_use_pnpm_home_env_var`]: when
|
||||
@@ -1451,7 +1458,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let store_dir = default_store_dir::<EnvWithXdgDataHome>();
|
||||
assert_eq!(display_store_dir(&store_dir), "/hello/pnpm/store");
|
||||
assert_eq!(
|
||||
display_store_dir(&store_dir),
|
||||
format!("/hello/pnpm/store/{}", pacquet_store_dir::STORE_VERSION),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -32,12 +32,17 @@
|
||||
//! question without touching disk. The production [`Host`] impl
|
||||
//! performs the real link attempts via [`host_can_link_between_dirs`].
|
||||
//!
|
||||
//! `STORE_VERSION` ("v11") is intentionally *not* appended here:
|
||||
//! pacquet's [`pacquet_store_dir::StoreDir`] stores the root one
|
||||
//! level above v11 and appends it at access time via
|
||||
//! [`pacquet_store_dir::StoreDir::v11`], where pnpm's `getStorePath`
|
||||
//! returns the v11-suffixed path directly. Both implementations land
|
||||
//! on identical on-disk paths.
|
||||
//! [`pacquet_store_dir::STORE_VERSION`] (`"v11"`) is *not* appended in
|
||||
//! this module; the path returned here is the un-suffixed base. Every
|
||||
//! caller wraps the result in [`pacquet_store_dir::StoreDir::from`],
|
||||
//! which appends the suffix in one place — mirroring pnpm's
|
||||
//! [`getStorePath`](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42)
|
||||
//! `if (!endsWith(v11)) append(v11)` branch. Doing the join at
|
||||
//! construction guarantees that everything pacquet exposes externally
|
||||
//! (the `storeDir` written to `.modules.yaml`, the path printed by
|
||||
//! `pacquet store path`, the NDJSON `context` log event) matches the
|
||||
//! value pnpm produces, so switching between the two tools no longer
|
||||
//! trips `ERR_PNPM_UNEXPECTED_STORE`.
|
||||
//!
|
||||
//! [`Host`]: crate::api::Host
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use pacquet_reporter::{
|
||||
PackageManifestMessage, ProgressLog, ProgressMessage, Reporter, SilentReporter, Stage,
|
||||
StageLog, StatsLog, StatsMessage, SummaryLog,
|
||||
};
|
||||
use pacquet_store_dir::STORE_VERSION;
|
||||
use pacquet_testing_utils::fs::{get_all_folders, is_symlink_or_junction};
|
||||
use pacquet_workspace_state::{
|
||||
self as workspace_state, NodeLinker as WorkspaceStateNodeLinker, load_workspace_state,
|
||||
@@ -578,7 +579,7 @@ async fn install_emits_pnpm_event_sequence() {
|
||||
unreachable!("second event is context, asserted above");
|
||||
};
|
||||
assert!(!current_lockfile_exists);
|
||||
assert_eq!(emitted_store_dir, &store_dir.display().to_string());
|
||||
assert_eq!(emitted_store_dir, &store_dir.join(STORE_VERSION).display().to_string());
|
||||
assert_eq!(emitted_virtual_store_dir, &virtual_store_dir.to_string_lossy().into_owned());
|
||||
|
||||
// Summary's `prefix` must equal the manifest-parent value
|
||||
@@ -675,7 +676,7 @@ async fn install_writes_modules_yaml() {
|
||||
assert!(included.dependencies);
|
||||
assert!(!included.dev_dependencies);
|
||||
assert!(included.optional_dependencies);
|
||||
assert_eq!(emitted_store_dir, store_dir.display().to_string());
|
||||
assert_eq!(emitted_store_dir, store_dir.join(STORE_VERSION).display().to_string());
|
||||
// `read_modules_manifest` resolves `virtualStoreDir` against
|
||||
// `modules_dir`, so a relative on-disk value round-trips back
|
||||
// to the absolute install-time path.
|
||||
|
||||
@@ -454,13 +454,20 @@ mod tests {
|
||||
|
||||
/// Subdir guard: when the store lives inside the project, the
|
||||
/// function is a silent no-op — registering would otherwise create
|
||||
/// a self-referential symlink.
|
||||
/// a self-referential symlink. The `STORE_VERSION` subdir
|
||||
/// (`store_dir.root()` after [`StoreDir::new`] routes the path
|
||||
/// through [`From<PathBuf>`] and applies the suffix) is
|
||||
/// materialised on disk so [`path_contains`]'s canonical-form
|
||||
/// comparison sees both sides as canonical paths even on macOS,
|
||||
/// where `/tmp` symlinks to `/private/tmp` and a missing target
|
||||
/// would silently fall back to lexical comparison and miss the
|
||||
/// containment.
|
||||
#[test]
|
||||
fn register_skips_when_store_is_inside_project() {
|
||||
let project = tempdir().unwrap();
|
||||
let store_path = project.path().join("nested-store");
|
||||
fs::create_dir_all(&store_path).unwrap();
|
||||
let store_dir = StoreDir::new(store_path);
|
||||
let store_dir = StoreDir::new(&store_path);
|
||||
fs::create_dir_all(store_dir.root()).unwrap();
|
||||
|
||||
register_project(&store_dir, project.path()).expect("subdir case is a no-op");
|
||||
// No projects/ dir should have been created.
|
||||
|
||||
@@ -6,20 +6,41 @@ use std::path::{self, PathBuf};
|
||||
/// Content hash of a file.
|
||||
pub type FileHash = digest::Output<Sha512>;
|
||||
|
||||
/// Major version of the pnpm store layout that pacquet writes to and reads
|
||||
/// from. Mirrors pnpm's [`STORE_VERSION`](https://github.com/pnpm/pnpm/blob/29a42efc3b/core/constants/src/index.ts#L9).
|
||||
///
|
||||
/// The constant is part of the public contract pnpm exposes to every
|
||||
/// project's `.modules.yaml` (the recorded `storeDir` is the
|
||||
/// `STORE_VERSION`-suffixed path), so changing it requires moving in
|
||||
/// lockstep with pnpm — otherwise both tools start refusing each
|
||||
/// other's stores with `ERR_PNPM_UNEXPECTED_STORE`.
|
||||
pub const STORE_VERSION: &str = "v11";
|
||||
|
||||
/// Represent a store directory.
|
||||
///
|
||||
/// * The store directory stores all files that were acquired by installing packages with pacquet or pnpm.
|
||||
/// * The files in `node_modules` directories are hardlinks or reflinks to the files in the store directory.
|
||||
/// * The store directory can and often act as a global shared cache of all installation of different workspaces.
|
||||
/// * The location of the store directory can be customized by `store-dir` field.
|
||||
/// * The on-disk layout matches pnpm v11 (`<root>/v11/files/XX/…[-exec]` + `<root>/v11/index.db`)
|
||||
/// so the two tools can share a store.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
/// * The on-disk layout matches pnpm v11 (`<root>/files/XX/…[-exec]` + `<root>/index.db`)
|
||||
/// where `<root>` already includes the `v11` suffix, so the two tools share both the
|
||||
/// physical layout *and* the user-visible `storeDir` string written to
|
||||
/// `.modules.yaml`.
|
||||
//
|
||||
// `#[serde(from = "PathBuf", into = "PathBuf")]` routes both
|
||||
// directions through the `PathBuf` boundary so deserialization goes
|
||||
// back through [`From<PathBuf>`] and the [`STORE_VERSION`] suffix
|
||||
// invariant holds for persisted unsuffixed paths too — the previous
|
||||
// `#[serde(transparent)]` derive deserialised straight into the
|
||||
// `root` field and bypassed the auto-append.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(from = "PathBuf", into = "PathBuf")]
|
||||
pub struct StoreDir {
|
||||
/// Path to the root of the store directory from which all sub-paths are derived.
|
||||
///
|
||||
/// Consumer of this struct should interact with the sub-paths instead of this path.
|
||||
/// The `STORE_VERSION`-suffixed store path, equivalent to pnpm's
|
||||
/// [`storeDir`](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42).
|
||||
/// Consumers should reach for the purpose-built helpers
|
||||
/// ([`Self::files`][], [`Self::tmp`], [`Self::links`],
|
||||
/// [`Self::projects`]) rather than this raw path.
|
||||
root: PathBuf,
|
||||
|
||||
/// Runtime cache of shard bytes (`files/XX/`) this process has already
|
||||
@@ -30,11 +51,19 @@ pub struct StoreDir {
|
||||
/// `stat` per file. After the first hit, the shard is cached and
|
||||
/// subsequent writes skip the syscall entirely. Populated lazily by
|
||||
/// [`StoreDir::write_cas_file`]; duplicate inserts across threads are
|
||||
/// harmless since `create_dir_all` is idempotent.
|
||||
#[serde(skip, default)]
|
||||
/// harmless since `create_dir_all` is idempotent. Not part of the
|
||||
/// serialised wire shape — `#[serde(from/into)]` round-trips only
|
||||
/// through `PathBuf`, so the cache is regenerated empty on every
|
||||
/// deserialise.
|
||||
ensured_shards: DashSet<u8>,
|
||||
}
|
||||
|
||||
impl From<StoreDir> for PathBuf {
|
||||
fn from(store_dir: StoreDir) -> Self {
|
||||
store_dir.root
|
||||
}
|
||||
}
|
||||
|
||||
/// Manual `PartialEq` / `Eq`: the shard cache is runtime state, two stores
|
||||
/// are equal iff they point at the same path.
|
||||
impl PartialEq for StoreDir {
|
||||
@@ -46,7 +75,18 @@ impl PartialEq for StoreDir {
|
||||
impl Eq for StoreDir {}
|
||||
|
||||
impl From<PathBuf> for StoreDir {
|
||||
/// Wrap a raw path into a [`StoreDir`], appending [`STORE_VERSION`]
|
||||
/// when the path doesn't already end with that segment. Mirrors
|
||||
/// pnpm's [`getStorePath`](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42),
|
||||
/// so both tools record the same `storeDir` string in
|
||||
/// `.modules.yaml` and switching between them stops tripping
|
||||
/// `ERR_PNPM_UNEXPECTED_STORE`.
|
||||
fn from(root: PathBuf) -> Self {
|
||||
let root = if root.file_name().and_then(|name| name.to_str()) == Some(STORE_VERSION) {
|
||||
root
|
||||
} else {
|
||||
root.join(STORE_VERSION)
|
||||
};
|
||||
StoreDir { root, ensured_shards: DashSet::new() }
|
||||
}
|
||||
}
|
||||
@@ -76,14 +116,9 @@ impl StoreDir {
|
||||
self.root.display()
|
||||
}
|
||||
|
||||
/// Get `{store}/v11` — the root of the pnpm v11 store layout.
|
||||
pub fn v11(&self) -> PathBuf {
|
||||
self.root.join("v11")
|
||||
}
|
||||
|
||||
/// The directory that contains all content-addressed files.
|
||||
fn files(&self) -> PathBuf {
|
||||
self.v11().join("files")
|
||||
self.root.join("files")
|
||||
}
|
||||
|
||||
/// Path to a file in the store directory.
|
||||
@@ -108,7 +143,7 @@ impl StoreDir {
|
||||
|
||||
/// Path to the temporary directory inside the store.
|
||||
pub fn tmp(&self) -> PathBuf {
|
||||
self.v11().join("tmp")
|
||||
self.root.join("tmp")
|
||||
}
|
||||
|
||||
/// Path to the shared global-virtual-store directory inside the
|
||||
@@ -117,27 +152,26 @@ impl StoreDir {
|
||||
/// `globalVirtualStoreDir = path.join(extendedOpts.storeDir, 'links')`.
|
||||
/// `extendedOpts.storeDir` has already been routed through
|
||||
/// [`getStorePath`](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42)
|
||||
/// by the time that join runs, and `getStorePath` appends
|
||||
/// `STORE_VERSION` (`"v11"`) to whatever the user configured. So
|
||||
/// the resulting on-disk location is `<root>/v11/links`, not
|
||||
/// `<root>/links` — the latter would put pacquet one level
|
||||
/// shallower than pnpm and split slot caches across the two
|
||||
/// tools. Sharing the path across pnpm and pacquet is the whole
|
||||
/// point, so anchor under [`Self::v11`].
|
||||
/// — which appends [`STORE_VERSION`] (`"v11"`) to whatever the
|
||||
/// user configured — by the time that join runs. Pacquet's
|
||||
/// [`StoreDir::from`] applies the same suffix, so `self.root` is
|
||||
/// already the v11 path and the on-disk location is
|
||||
/// `<root>/links`, identical to pnpm's. Sharing this path across
|
||||
/// pnpm and pacquet is the whole point.
|
||||
pub fn links(&self) -> PathBuf {
|
||||
self.v11().join("links")
|
||||
self.root.join("links")
|
||||
}
|
||||
|
||||
/// Path to the per-store projects registry — a flat directory of
|
||||
/// symlinks (`<store>/v11/projects/<short-hash>` → project dir)
|
||||
/// the global-virtual-store prune sweep walks when deciding which
|
||||
/// `<store>/v11/links/...` slots are still referenced. Mirrors
|
||||
/// pnpm 11's
|
||||
/// [`{storeDir}/v11/projects/` layout](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/controller/CHANGELOG.md#L136)
|
||||
/// — same `getStorePath`-driven `v11` reasoning as
|
||||
/// symlinks (`<store>/projects/<short-hash>` → project dir) the
|
||||
/// global-virtual-store prune sweep walks when deciding which
|
||||
/// `<store>/links/...` slots are still referenced. Mirrors pnpm
|
||||
/// 11's
|
||||
/// [`{storeDir}/projects/` layout](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/controller/CHANGELOG.md#L136)
|
||||
/// — `<store>` already carries the v11 suffix on both sides per
|
||||
/// [`Self::links`].
|
||||
pub fn projects(&self) -> PathBuf {
|
||||
self.v11().join("projects")
|
||||
self.root.join("projects")
|
||||
}
|
||||
|
||||
/// Borrow the raw store-root path. Most code should prefer the
|
||||
@@ -148,7 +182,7 @@ impl StoreDir {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// On a fresh store, eagerly create `<store>/v11/files/` plus every
|
||||
/// On a fresh store, eagerly create `<store>/files/` plus every
|
||||
/// `files/XX/` shard (00..ff) and seed the shard cache with the
|
||||
/// bytes we just created, so CAFS writes never pay a
|
||||
/// `create_dir_all` syscall in the hot path.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::StoreDir;
|
||||
use super::{STORE_VERSION, StoreDir};
|
||||
use pipe_trait::Pipe;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn file_path_by_head_tail() {
|
||||
@@ -20,6 +20,59 @@ fn tmp() {
|
||||
assert_eq!(received, expected);
|
||||
}
|
||||
|
||||
/// `StoreDir::from(PathBuf)` appends [`STORE_VERSION`] to any path
|
||||
/// that doesn't already end with it — matching pnpm's
|
||||
/// [`getStorePath`](https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L39-L42)
|
||||
/// branch. Both the auto-append happy path and the
|
||||
/// already-suffixed idempotent path are pinned here so a regression
|
||||
/// would surface as either a missing `v11` (the original bug — pnpm
|
||||
/// rejects the resulting `.modules.yaml` with
|
||||
/// `ERR_PNPM_UNEXPECTED_STORE`) or a duplicated `v11/v11` segment.
|
||||
#[test]
|
||||
fn from_pathbuf_auto_appends_store_version_when_missing() {
|
||||
let store = StoreDir::from(PathBuf::from("/home/user/.local/share/pnpm/store"));
|
||||
assert_eq!(store.root(), Path::new("/home/user/.local/share/pnpm/store/v11"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_pathbuf_does_not_double_append_when_already_suffixed() {
|
||||
let store = StoreDir::from(PathBuf::from("/home/user/.local/share/pnpm/store/v11"));
|
||||
assert_eq!(store.root(), Path::new("/home/user/.local/share/pnpm/store/v11"));
|
||||
}
|
||||
|
||||
/// Round-trip the `storeDir` string pacquet writes to `.modules.yaml`
|
||||
/// against the pnpm comparison contract: pnpm rebuilds `<X>/v11`
|
||||
/// from the user's home and demands an exact match against the
|
||||
/// recorded value. The constant test makes the bug
|
||||
/// `ERR_PNPM_UNEXPECTED_STORE` triggered legible for future readers.
|
||||
#[test]
|
||||
fn modules_yaml_serialized_store_dir_carries_store_version() {
|
||||
let store = StoreDir::new("/tmp/.pnpm-store");
|
||||
let recorded = store.display().to_string();
|
||||
let pnpm_would_emit = format!("/tmp/.pnpm-store/{STORE_VERSION}");
|
||||
assert_eq!(recorded, pnpm_would_emit);
|
||||
}
|
||||
|
||||
/// Deserialising an unsuffixed path (e.g. one persisted by an older
|
||||
/// pacquet that hadn't normalised yet) must route through
|
||||
/// [`From<PathBuf>`] and gain the [`STORE_VERSION`] suffix. The
|
||||
/// previous `#[serde(transparent)]` derive wrote straight into
|
||||
/// `root` and bypassed the auto-append, leaving the suffix invariant
|
||||
/// silently violated on the live `StoreDir`.
|
||||
#[test]
|
||||
fn deserialize_applies_store_version_to_unsuffixed_path() {
|
||||
let json = r#""/home/user/.local/share/pnpm/store""#;
|
||||
let store: StoreDir = serde_json::from_str(json).expect("deserialize StoreDir");
|
||||
assert_eq!(store.root(), Path::new("/home/user/.local/share/pnpm/store/v11"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_preserves_already_suffixed_path() {
|
||||
let json = r#""/home/user/.local/share/pnpm/store/v11""#;
|
||||
let store: StoreDir = serde_json::from_str(json).expect("deserialize StoreDir");
|
||||
assert_eq!(store.root(), Path::new("/home/user/.local/share/pnpm/store/v11"));
|
||||
}
|
||||
|
||||
/// `init` on a fresh store should materialize `v11/files/00..ff`
|
||||
/// and populate the shard cache so later `write_cas_file` calls
|
||||
/// can skip their lazy mkdir.
|
||||
|
||||
@@ -120,10 +120,10 @@ impl StoreIndexWriter {
|
||||
pub fn spawn(
|
||||
store_dir: &StoreDir,
|
||||
) -> (Arc<StoreIndexWriter>, tokio::task::JoinHandle<Result<(), StoreIndexError>>) {
|
||||
let v11_dir = store_dir.v11();
|
||||
let store_root = store_dir.root().to_path_buf();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<WriteMsg>();
|
||||
let handle = tokio::task::spawn_blocking(move || -> Result<(), StoreIndexError> {
|
||||
let mut index = StoreIndex::open(&v11_dir)?;
|
||||
let mut index = StoreIndex::open(&store_root)?;
|
||||
let mut batch: Vec<WriteMsg> = Vec::with_capacity(MAX_BATCH_SIZE);
|
||||
while let Some(first) = rx.blocking_recv() {
|
||||
batch.push(first);
|
||||
@@ -401,9 +401,9 @@ impl StoreIndex {
|
||||
Ok(StoreIndex { conn })
|
||||
}
|
||||
|
||||
/// Open the `index.db` that lives under a [`StoreDir`]'s `v11` subdirectory.
|
||||
/// Open the `index.db` that lives directly under a [`StoreDir`]'s root.
|
||||
pub fn open_in(store_dir: &StoreDir) -> Result<Self, StoreIndexError> {
|
||||
StoreIndex::open(&store_dir.v11())
|
||||
StoreIndex::open(store_dir.root())
|
||||
}
|
||||
|
||||
/// Open an existing `index.db` read-only. Skips the schema-mutating
|
||||
@@ -427,7 +427,7 @@ impl StoreIndex {
|
||||
|
||||
/// Read-only counterpart to [`StoreIndex::open_in`].
|
||||
pub fn open_readonly_in(store_dir: &StoreDir) -> Result<Self, StoreIndexError> {
|
||||
StoreIndex::open_readonly(&store_dir.v11())
|
||||
StoreIndex::open_readonly(store_dir.root())
|
||||
}
|
||||
|
||||
/// Open a read-only index wrapped in `Arc<Mutex<…>>` so it can be shared
|
||||
@@ -440,11 +440,11 @@ impl StoreIndex {
|
||||
/// redoing its PRAGMAs) on every package, which otherwise scales
|
||||
/// linearly with the snapshot count.
|
||||
pub fn shared_readonly_in(store_dir: &StoreDir) -> Option<SharedReadonlyStoreIndex> {
|
||||
let v11_dir = store_dir.v11();
|
||||
if !v11_dir.join("index.db").exists() {
|
||||
let store_root = store_dir.root();
|
||||
if !store_root.join("index.db").exists() {
|
||||
return None;
|
||||
}
|
||||
StoreIndex::open_readonly(&v11_dir).ok().map(|index| Arc::new(Mutex::new(index)))
|
||||
StoreIndex::open_readonly(store_root).ok().map(|index| Arc::new(Mutex::new(index)))
|
||||
}
|
||||
|
||||
/// Look up a package-files index by key. Returns `Ok(None)` if no row exists.
|
||||
|
||||
@@ -135,7 +135,7 @@ fn index_db_lives_at_store_dir_v11() {
|
||||
let store = StoreDir::new(root.path());
|
||||
let idx = StoreIndex::open_in(&store).unwrap();
|
||||
idx.set("k\tv", &sample_index()).unwrap();
|
||||
assert!(store.v11().join("index.db").exists());
|
||||
assert!(store.root().join("index.db").exists());
|
||||
}
|
||||
|
||||
/// A row whose bytes are msgpackr-records (as pnpm writes) must decode
|
||||
|
||||
Reference in New Issue
Block a user