feat: support macOS and Windows shared build artifacts (#14263)
Teach the shared-artifact protocol to recognize additive v1 Darwin and Windows tags that encode architecture, Node major, and a minimum operating-system version. Select exact supported tags before version floors, then choose the greatest compatible floor and keep universal artifacts as the final fallback. Detect macOS product versions with the absolute system `sw_vers` path and Windows NT kernel versions with the platform APIs. Enable restoration and publishing on Darwin and Windows x64 and arm64. Implement the same protocol, fingerprints, selection, and host-detection behavior in the TypeScript CLI and pacquet. Keep all existing Linux tags and fingerprints stable. Related to pnpm/pnpm#13771 and pnpm/rfcs#24.
This commit is contained in:
1 parent
fc0c282b71
commit
e972cb5012
14 files changed
+889
-130
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/pnpr.client": minor
|
||||
"pacquet": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added macOS and Windows x64 and arm64 support to remote shared build artifacts [pnpm/pnpm#13771](https://github.com/pnpm/pnpm/issues/13771).
|
||||
Generated
+1
@@ -4722,6 +4722,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ssri",
|
||||
"sysinfo",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"text-block-macros",
|
||||
|
||||
@@ -80,6 +80,7 @@ libc = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
same-file = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -10,9 +10,10 @@ use pnpm_lockfile::{PackageKey, PackageMetadata, SnapshotEntry};
|
||||
use pnpm_pnpr_client::{
|
||||
ARTIFACT_KIND, ArtifactBlobRequest, ArtifactBlobUpload, ArtifactCandidate, ArtifactFile,
|
||||
ArtifactManifest, ArtifactPayload, BuilderProfile, CompatibilityConstraints,
|
||||
LinuxGlibcPlatform, OwnerScope, PackageIdentity, PnprClient, PnprClientError,
|
||||
LinuxGlibcPlatform, MacOsPlatform, OwnerScope, PackageIdentity, PnprClient, PnprClientError,
|
||||
PublishArtifactRequest, RejectedArtifact, ResolveArtifactsOptions, SignedArtifactEnvelope,
|
||||
blob_id, linux_glibc_supported_tags, linux_glibc_tag, platform_fingerprint,
|
||||
WindowsPlatform, blob_id, linux_glibc_supported_tags, linux_glibc_tag, macos_supported_tags,
|
||||
macos_tag, platform_fingerprint, windows_supported_tags, windows_tag,
|
||||
};
|
||||
use pnpm_shared_artifact_protocol::compatibility_rank;
|
||||
use pnpm_store_dir::{CafsFileInfo, RemoteSideEffectsOrigin, SideEffectsDiff, StoreIndexWriter};
|
||||
@@ -20,8 +21,11 @@ use sha2::{Digest as _, Sha512};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use sysinfo::System;
|
||||
|
||||
pub(crate) type BaseCasPaths = HashMap<PackageKey, HashMap<String, PathBuf>>;
|
||||
|
||||
@@ -33,11 +37,46 @@ pub struct SharedSideEffectsPublisher {
|
||||
key_id: String,
|
||||
organization: String,
|
||||
packages: HashSet<String>,
|
||||
platform: LinuxGlibcPlatform<'static>,
|
||||
platform: ArtifactPlatform<'static>,
|
||||
private_key: Vec<u8>,
|
||||
runtime: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ArtifactPlatform<'a> {
|
||||
LinuxGlibc(LinuxGlibcPlatform<'a>),
|
||||
MacOs(MacOsPlatform<'a>),
|
||||
Windows(WindowsPlatform<'a>),
|
||||
}
|
||||
|
||||
impl ArtifactPlatform<'_> {
|
||||
fn node_major(self) -> u32 {
|
||||
match self {
|
||||
Self::LinuxGlibc(platform) => platform.node_major,
|
||||
Self::MacOs(platform) => platform.node_major,
|
||||
Self::Windows(platform) => platform.node_major,
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_tags(
|
||||
self,
|
||||
) -> Result<Vec<String>, pnpm_shared_artifact_protocol::ArtifactProtocolError> {
|
||||
match self {
|
||||
Self::LinuxGlibc(platform) => linux_glibc_supported_tags(platform),
|
||||
Self::MacOs(platform) => macos_supported_tags(platform),
|
||||
Self::Windows(platform) => windows_supported_tags(platform),
|
||||
}
|
||||
}
|
||||
|
||||
fn tag(self) -> Result<String, pnpm_shared_artifact_protocol::ArtifactProtocolError> {
|
||||
match self {
|
||||
Self::LinuxGlibc(platform) => linux_glibc_tag(platform),
|
||||
Self::MacOs(platform) => macos_tag(platform),
|
||||
Self::Windows(platform) => windows_tag(platform),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CandidateGroup {
|
||||
candidate: ArtifactCandidate,
|
||||
pinned_envelope_digest: Option<String>,
|
||||
@@ -83,8 +122,8 @@ pub(crate) async fn apply_shared_side_effects(
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(settings) = config.remote_side_effects_cache.as_ref() else { return Vec::new() };
|
||||
let Some(platform) = linux_glibc_platform(snapshots) else { return Vec::new() };
|
||||
let supported_tags = match linux_glibc_supported_tags(platform) {
|
||||
let Some(platform) = artifact_platform(snapshots) else { return Vec::new() };
|
||||
let supported_tags = match platform.supported_tags() {
|
||||
Ok(tags) => tags,
|
||||
Err(error) => {
|
||||
tracing::warn!(target: "pacquet::install", %error, "remote side-effects platform is unsupported");
|
||||
@@ -129,7 +168,7 @@ pub(crate) async fn apply_shared_side_effects(
|
||||
&mut deps_state_cache,
|
||||
in_lockfile_order(&graph).into_iter().map(|(key, _)| key),
|
||||
);
|
||||
let engine_name = pnpm_graph_hasher::engine_name(platform.node_major, None, None);
|
||||
let engine_name = pnpm_graph_hasher::engine_name(platform.node_major(), None, None);
|
||||
let mut groups = BTreeMap::<String, CandidateGroup>::new();
|
||||
let mut collisions = HashSet::new();
|
||||
let mut artifact_pin_records = Vec::new();
|
||||
@@ -749,7 +788,7 @@ pub(crate) fn shared_side_effects_publisher(
|
||||
return None;
|
||||
}
|
||||
let snapshots = snapshots?;
|
||||
let platform = linux_glibc_platform(snapshots)?;
|
||||
let platform = artifact_platform(snapshots)?;
|
||||
let private_key = BASE64.decode(settings.private_key.as_ref()?).ok()?;
|
||||
let key_id = settings.key_id.clone()?;
|
||||
let builder_id = settings.builder_id.clone()?;
|
||||
@@ -839,7 +878,7 @@ impl SharedSideEffectsPublisher {
|
||||
builder_id: self.builder_id.clone(),
|
||||
builder_profile: self.builder_profile.clone(),
|
||||
compatibility: CompatibilityConstraints::Tagged {
|
||||
tags: vec![linux_glibc_tag(self.platform).map_err(|error| error.to_string())?],
|
||||
tags: vec![self.platform.tag().map_err(|error| error.to_string())?],
|
||||
},
|
||||
manifest: ArtifactManifest { added: files, deleted: diff.deleted.unwrap_or_default() },
|
||||
};
|
||||
@@ -859,23 +898,77 @@ impl SharedSideEffectsPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_glibc_platform(
|
||||
fn artifact_platform(
|
||||
snapshots: &HashMap<PackageKey, SnapshotEntry>,
|
||||
) -> Option<LinuxGlibcPlatform<'static>> {
|
||||
if pnpm_graph_hasher::host_platform() != "linux"
|
||||
|| !matches!(pnpm_graph_hasher::host_arch(), "x64" | "arm64")
|
||||
{
|
||||
) -> Option<ArtifactPlatform<'static>> {
|
||||
let architecture = pnpm_graph_hasher::host_arch();
|
||||
if !matches!(architecture, "x64" | "arm64") {
|
||||
return None;
|
||||
}
|
||||
let node_major =
|
||||
find_runtime_node_major(Some(snapshots)).or_else(pnpm_graph_hasher::detect_node_major)?;
|
||||
let (glibc_major, glibc_minor) = pnpm_detect_libc::glibc_version()?;
|
||||
Some(LinuxGlibcPlatform {
|
||||
architecture: pnpm_graph_hasher::host_arch(),
|
||||
node_major,
|
||||
glibc_major,
|
||||
glibc_minor,
|
||||
})
|
||||
match pnpm_graph_hasher::host_platform() {
|
||||
"linux" => {
|
||||
let (glibc_major, glibc_minor) = pnpm_detect_libc::glibc_version()?;
|
||||
Some(ArtifactPlatform::LinuxGlibc(LinuxGlibcPlatform {
|
||||
architecture,
|
||||
node_major,
|
||||
glibc_major,
|
||||
glibc_minor,
|
||||
}))
|
||||
}
|
||||
"darwin" => {
|
||||
let (macos_major, macos_minor) = macos_product_version()?;
|
||||
Some(ArtifactPlatform::MacOs(MacOsPlatform {
|
||||
architecture,
|
||||
node_major,
|
||||
macos_major,
|
||||
macos_minor,
|
||||
}))
|
||||
}
|
||||
"win32" => {
|
||||
let (windows_major, windows_minor, windows_build) = windows_kernel_version()?;
|
||||
Some(ArtifactPlatform::Windows(WindowsPlatform {
|
||||
architecture,
|
||||
node_major,
|
||||
windows_major,
|
||||
windows_minor,
|
||||
windows_build,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn macos_product_version() -> Option<(u32, u32)> {
|
||||
let output = Command::new("/usr/bin/sw_vers").arg("-productVersion").output().ok()?;
|
||||
output.status.success().then_some(())?;
|
||||
parse_macos_product_version(std::str::from_utf8(&output.stdout).ok()?)
|
||||
}
|
||||
|
||||
fn parse_macos_product_version(value: &str) -> Option<(u32, u32)> {
|
||||
let mut components = value.trim().split('.');
|
||||
let major = components.next()?.parse().ok()?;
|
||||
let minor = components.next()?.parse().ok()?;
|
||||
(major > 0 && major < 1_000_000 && minor < 1_000_000).then_some((major, minor))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_kernel_version() -> Option<(u32, u32, u32)> {
|
||||
let build = System::kernel_version()?.parse().ok()?;
|
||||
// Windows 10 and 11 both use NT kernel version 10.0; sysinfo provides the build number.
|
||||
validate_windows_kernel_version(10, 0, build)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn windows_kernel_version() -> Option<(u32, u32, u32)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn validate_windows_kernel_version(major: u32, minor: u32, build: u32) -> Option<(u32, u32, u32)> {
|
||||
(major > 0 && major < 1_000 && minor < 1_000 && build > 0 && build < 1_000_000)
|
||||
.then_some((major, minor, build))
|
||||
}
|
||||
|
||||
fn patch_hash(snapshot_key: &PackageKey) -> Option<String> {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
use super::package_version;
|
||||
use super::{package_version, parse_macos_product_version, validate_windows_kernel_version};
|
||||
use pnpm_lockfile::PackageKey;
|
||||
|
||||
#[test]
|
||||
fn parses_macos_product_versions() {
|
||||
assert_eq!(parse_macos_product_version("15.5.1\n"), Some((15, 5)));
|
||||
assert_eq!(parse_macos_product_version("26.0\n"), Some((26, 0)));
|
||||
assert_eq!(parse_macos_product_version("Darwin 25.0\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_windows_kernel_versions() {
|
||||
assert_eq!(validate_windows_kernel_version(10, 0, 26_100), Some((10, 0, 26_100)));
|
||||
assert_eq!(validate_windows_kernel_version(0, 0, 26_100), None);
|
||||
assert_eq!(validate_windows_kernel_version(10, 0, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_identity_uses_the_manifest_version_for_non_registry_sources() {
|
||||
let file: PackageKey = "native-addon@file:../native-addon.tgz".parse().unwrap();
|
||||
@@ -117,10 +131,9 @@ async fn a_non_regular_file_is_not_reused_as_store_content() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A restore only happens where the remote cache applies at all:
|
||||
/// `linux_glibc_platform` refuses anything but linux-glibc on x64 or arm64, so
|
||||
/// anywhere else — musl, or a linux arch pacquet publishes no artifacts for —
|
||||
/// there is no restore to observe and each test skips itself.
|
||||
/// A restore only happens where the remote cache applies at all. Unsupported
|
||||
/// libc, operating-system, and architecture combinations have no restore to
|
||||
/// observe, so the platform gates below make that boundary visible to tests.
|
||||
mod restore {
|
||||
use crate::{AllowBuildPolicy, RequiresBuildBySnapshot, SideEffectsMapsBySnapshot};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
@@ -133,7 +146,6 @@ mod restore {
|
||||
use pnpm_pnpr_client::{
|
||||
ARTIFACT_KIND, ArtifactFile, ArtifactManifest, ArtifactPayload, BuilderProfile,
|
||||
CompatibilityConstraints, OwnerScope, ResolveArtifactsRequest, SignedArtifactEnvelope,
|
||||
linux_glibc_supported_tags,
|
||||
};
|
||||
use pnpm_shared_artifact_protocol::{
|
||||
ArtifactVariant, ResolveArtifactsResponse, ResolvedArtifact,
|
||||
@@ -371,19 +383,14 @@ mod restore {
|
||||
async fn restore(store_dir: &StoreDir, expected_downloads: usize) -> PathBuf {
|
||||
let snapshots = snapshots();
|
||||
let packages = packages();
|
||||
// The cfg above settles the target; the glibc *version* is read from
|
||||
// the environment (`getconf`, then `ldd`), which the target cannot
|
||||
// promise. A host with neither is not one the remote cache serves, so
|
||||
// say that rather than leaving a bare unwrap to explain itself.
|
||||
let platform = super::super::linux_glibc_platform(&snapshots).expect(
|
||||
"the glibc version must be readable — install `getconf` or `ldd`, \
|
||||
or run this suite on a host the remote side-effects cache serves",
|
||||
);
|
||||
let platform = super::super::artifact_platform(&snapshots)
|
||||
.expect("the host compatibility floor must be readable on a supported platform");
|
||||
assert_eq!(
|
||||
platform.node_major, 22,
|
||||
platform.node_major(),
|
||||
22,
|
||||
"the lockfile's Node pin, not the machine's Node, must decide the platform",
|
||||
);
|
||||
let mut supported_tags = linux_glibc_supported_tags(platform).expect("supported tags");
|
||||
let mut supported_tags = platform.supported_tags().expect("supported tags");
|
||||
let compatibility_tag = supported_tags.swap_remove(0);
|
||||
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
@@ -452,12 +459,16 @@ mod restore {
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(
|
||||
not(all(
|
||||
target_os = "linux",
|
||||
target_env = "gnu",
|
||||
any(target_arch = "x86_64", target_arch = "aarch64")
|
||||
not(any(
|
||||
all(
|
||||
target_os = "linux",
|
||||
target_env = "gnu",
|
||||
any(target_arch = "x86_64", target_arch = "aarch64")
|
||||
),
|
||||
all(target_os = "macos", any(target_arch = "x86_64", target_arch = "aarch64")),
|
||||
all(target_os = "windows", any(target_arch = "x86_64", target_arch = "aarch64"))
|
||||
)),
|
||||
ignore = "the remote side-effects cache only serves linux-glibc on x64 and arm64"
|
||||
ignore = "the remote side-effects cache only serves glibc Linux, macOS, and Windows on x64 and arm64"
|
||||
)]
|
||||
async fn content_the_store_lacks_is_downloaded() {
|
||||
let store = tempfile::tempdir().expect("tempdir");
|
||||
@@ -477,12 +488,16 @@ mod restore {
|
||||
/// difference between them.
|
||||
#[tokio::test]
|
||||
#[cfg_attr(
|
||||
not(all(
|
||||
target_os = "linux",
|
||||
target_env = "gnu",
|
||||
any(target_arch = "x86_64", target_arch = "aarch64")
|
||||
not(any(
|
||||
all(
|
||||
target_os = "linux",
|
||||
target_env = "gnu",
|
||||
any(target_arch = "x86_64", target_arch = "aarch64")
|
||||
),
|
||||
all(target_os = "macos", any(target_arch = "x86_64", target_arch = "aarch64")),
|
||||
all(target_os = "windows", any(target_arch = "x86_64", target_arch = "aarch64"))
|
||||
)),
|
||||
ignore = "the remote side-effects cache only serves linux-glibc on x64 and arm64"
|
||||
ignore = "the remote side-effects cache only serves glibc Linux, macOS, and Windows on x64 and arm64"
|
||||
)]
|
||||
async fn content_the_store_already_holds_is_not_downloaded() {
|
||||
let store = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -36,13 +36,15 @@ use reqwest::Client;
|
||||
pub use pnpm_shared_artifact_protocol::{
|
||||
ARTIFACT_KIND, ArtifactBlobRequest, ArtifactBlobUpload, ArtifactCandidate, ArtifactFile,
|
||||
ArtifactManifest, ArtifactPayload, BuilderProfile, COMPATIBILITY_TAG_SCHEMA,
|
||||
CompatibilityConstraints, INPUT_KEY_PREFIX, LinuxGlibcPlatform, OwnerScope, PackageIdentity,
|
||||
PublishArtifactRequest, ResolveArtifactsRequest, SIGNATURE_ALGORITHM, SignedArtifactEnvelope,
|
||||
blob_id, linux_glibc_supported_tags, linux_glibc_tag, platform_fingerprint,
|
||||
CompatibilityConstraints, INPUT_KEY_PREFIX, LinuxGlibcPlatform, MacOsPlatform, OwnerScope,
|
||||
PackageIdentity, PublishArtifactRequest, ResolveArtifactsRequest, SIGNATURE_ALGORITHM,
|
||||
SignedArtifactEnvelope, WindowsPlatform, blob_id, linux_glibc_supported_tags, linux_glibc_tag,
|
||||
macos_supported_tags, macos_tag, platform_fingerprint, windows_supported_tags, windows_tag,
|
||||
};
|
||||
use pnpm_shared_artifact_protocol::{
|
||||
MAX_CANDIDATES, MAX_FILE_SIZE, MAX_RESOLVE_RESPONSE_SIZE, MAX_VARIANTS_PER_CANDIDATE,
|
||||
ResolveArtifactsResponse, compatibility_rank, validate_supported_tags, verify_blob,
|
||||
ResolveArtifactsResponse, compatibility_rank_prevalidated, validate_supported_tags,
|
||||
verify_blob,
|
||||
};
|
||||
|
||||
/// The `registries` a request declares, keyed by registry URL.
|
||||
@@ -590,7 +592,7 @@ impl PnprClient {
|
||||
artifact.key,
|
||||
)));
|
||||
}
|
||||
let mut best: Option<(usize, String, VerifiedArtifact)> = None;
|
||||
let mut best: Option<(u64, String, VerifiedArtifact)> = None;
|
||||
for variant in artifact.variants {
|
||||
let Some(public_key) = opts.trusted_keys.get(&variant.envelope.key_id) else {
|
||||
continue;
|
||||
@@ -635,7 +637,8 @@ impl PnprClient {
|
||||
if !artifact_matches_candidate(&payload, candidate) {
|
||||
continue;
|
||||
}
|
||||
let Some(rank) = compatibility_rank(&payload.compatibility, &opts.supported_tags)
|
||||
let Some(rank) =
|
||||
compatibility_rank_prevalidated(&payload.compatibility, &opts.supported_tags)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,8 @@ pub const MAX_SIGNATURE_SIZE: usize = 72;
|
||||
pub const MAX_ENCODED_SIGNED_PAYLOAD_SIZE: usize = MAX_SIGNED_PAYLOAD_SIZE.div_ceil(3) * 4;
|
||||
pub const MAX_ENCODED_SIGNATURE_SIZE: usize = MAX_SIGNATURE_SIZE.div_ceil(3) * 4;
|
||||
pub const MAX_RESOLVE_RESPONSE_SIZE: usize = 16 * 1024 * 1024;
|
||||
const COMPATIBILITY_FLOOR_RANK_OFFSET: u64 = 64;
|
||||
const COMPATIBILITY_FLOOR_RANK_STRIDE: u64 = 1_000_000_000_000;
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub enum ArtifactProtocolError {
|
||||
@@ -70,6 +72,23 @@ pub struct LinuxGlibcPlatform<'a> {
|
||||
pub glibc_minor: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MacOsPlatform<'a> {
|
||||
pub architecture: &'a str,
|
||||
pub node_major: u32,
|
||||
pub macos_major: u32,
|
||||
pub macos_minor: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct WindowsPlatform<'a> {
|
||||
pub architecture: &'a str,
|
||||
pub node_major: u32,
|
||||
pub windows_major: u32,
|
||||
pub windows_minor: u32,
|
||||
pub windows_build: u32,
|
||||
}
|
||||
|
||||
impl OwnerScope {
|
||||
#[must_use]
|
||||
pub fn organization(name: impl Into<String>) -> Self {
|
||||
@@ -611,13 +630,38 @@ pub fn verify_blob(integrity: &str, bytes: &[u8]) -> Result<(), ArtifactProtocol
|
||||
pub fn compatibility_rank(
|
||||
constraints: &CompatibilityConstraints,
|
||||
supported_tags: &[String],
|
||||
) -> Option<usize> {
|
||||
) -> Option<u64> {
|
||||
validate_supported_tags(supported_tags).ok()?;
|
||||
compatibility_rank_prevalidated(constraints, supported_tags)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn compatibility_rank_prevalidated(
|
||||
constraints: &CompatibilityConstraints,
|
||||
supported_tags: &[String],
|
||||
) -> Option<u64> {
|
||||
match constraints {
|
||||
CompatibilityConstraints::Universal => Some(supported_tags.len()),
|
||||
CompatibilityConstraints::Tagged { tags } => {
|
||||
supported_tags.iter().position(|supported| tags.iter().any(|tag| tag == supported))
|
||||
}
|
||||
CompatibilityConstraints::Universal => Some(u64::MAX),
|
||||
CompatibilityConstraints::Tagged { tags } => supported_tags
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, supported)| {
|
||||
tags.iter().filter_map(move |artifact| {
|
||||
if artifact == supported {
|
||||
return u64::try_from(index).ok();
|
||||
}
|
||||
let distance = version_floor_rank(
|
||||
parse_compatibility_tag(supported).ok()?,
|
||||
parse_compatibility_tag(artifact).ok()?,
|
||||
)?;
|
||||
u64::try_from(index)
|
||||
.ok()?
|
||||
.checked_mul(COMPATIBILITY_FLOOR_RANK_STRIDE)?
|
||||
.checked_add(COMPATIBILITY_FLOOR_RANK_OFFSET)?
|
||||
.checked_add(distance)
|
||||
})
|
||||
})
|
||||
.min(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +698,37 @@ pub fn linux_glibc_supported_tags(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn macos_tag(platform: MacOsPlatform<'_>) -> Result<String, ArtifactProtocolError> {
|
||||
let MacOsPlatform { architecture, node_major, macos_major, macos_minor } = platform;
|
||||
let tag = format!(
|
||||
"{COMPATIBILITY_TAG_SCHEMA}:darwin-{architecture}-node{node_major}-macos{macos_major}.{macos_minor}",
|
||||
);
|
||||
validate_compatibility_tag(&tag)?;
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
pub fn macos_supported_tags(
|
||||
platform: MacOsPlatform<'_>,
|
||||
) -> Result<Vec<String>, ArtifactProtocolError> {
|
||||
Ok(vec![macos_tag(platform)?])
|
||||
}
|
||||
|
||||
pub fn windows_tag(platform: WindowsPlatform<'_>) -> Result<String, ArtifactProtocolError> {
|
||||
let WindowsPlatform { architecture, node_major, windows_major, windows_minor, windows_build } =
|
||||
platform;
|
||||
let tag = format!(
|
||||
"{COMPATIBILITY_TAG_SCHEMA}:win32-{architecture}-node{node_major}-windows{windows_major}.{windows_minor}.{windows_build}",
|
||||
);
|
||||
validate_compatibility_tag(&tag)?;
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
pub fn windows_supported_tags(
|
||||
platform: WindowsPlatform<'_>,
|
||||
) -> Result<Vec<String>, ArtifactProtocolError> {
|
||||
Ok(vec![windows_tag(platform)?])
|
||||
}
|
||||
|
||||
pub fn platform_fingerprint(supported_tags: &[String]) -> Result<String, ArtifactProtocolError> {
|
||||
validate_supported_tags(supported_tags)?;
|
||||
let mut hasher = Sha256::new();
|
||||
@@ -739,30 +814,142 @@ fn validate_compatibility(
|
||||
}
|
||||
|
||||
fn validate_compatibility_tag(tag: &str) -> Result<(), ArtifactProtocolError> {
|
||||
parse_compatibility_tag(tag).map(|_| ())
|
||||
}
|
||||
|
||||
enum ParsedCompatibilityTag<'a> {
|
||||
Linux,
|
||||
MacOs(MacOsPlatform<'a>),
|
||||
Windows(WindowsPlatform<'a>),
|
||||
}
|
||||
|
||||
fn parse_compatibility_tag(tag: &str) -> Result<ParsedCompatibilityTag<'_>, ArtifactProtocolError> {
|
||||
validate_scalar("compatibility tag", tag, 512)?;
|
||||
let Some(platform) = tag.strip_prefix("pnpm:v1:") else {
|
||||
return Err(invalid_tag("unknown compatibility tag schema"));
|
||||
};
|
||||
let mut parts = platform.split('-');
|
||||
let (Some(os), Some(architecture), Some(node), Some(libc), None) =
|
||||
let (Some(os), Some(architecture), Some(node), Some(runtime), None) =
|
||||
(parts.next(), parts.next(), parts.next(), parts.next(), parts.next())
|
||||
else {
|
||||
return Err(invalid_tag("compatibility tag has the wrong number of dimensions"));
|
||||
};
|
||||
if os != "linux" || !matches!(architecture, "x64" | "arm64") {
|
||||
return Err(invalid_tag("v1 only defines Linux x64 and arm64 tags"));
|
||||
if !matches!(architecture, "x64" | "arm64") {
|
||||
return Err(invalid_tag("v1 only defines x64 and arm64 tags"));
|
||||
}
|
||||
parse_canonical_number(
|
||||
let node_major = parse_canonical_number(
|
||||
node.strip_prefix("node").ok_or_else(|| invalid_tag("missing Node dimension"))?,
|
||||
"Node major version",
|
||||
false,
|
||||
)?;
|
||||
let libc = libc.strip_prefix("glibc").ok_or_else(|| invalid_tag("missing glibc floor"))?;
|
||||
let (major, minor) =
|
||||
libc.split_once('.').ok_or_else(|| invalid_tag("glibc floor must be major.minor"))?;
|
||||
parse_canonical_number(major, "glibc major version", false)?;
|
||||
parse_canonical_number(minor, "glibc minor version", true)?;
|
||||
Ok(())
|
||||
match os {
|
||||
"linux" => {
|
||||
let libc =
|
||||
runtime.strip_prefix("glibc").ok_or_else(|| invalid_tag("missing glibc floor"))?;
|
||||
let (major, minor) = libc
|
||||
.split_once('.')
|
||||
.ok_or_else(|| invalid_tag("glibc floor must be major.minor"))?;
|
||||
parse_canonical_number(major, "glibc major version", false)?;
|
||||
parse_canonical_number(minor, "glibc minor version", true)?;
|
||||
Ok(ParsedCompatibilityTag::Linux)
|
||||
}
|
||||
"darwin" => {
|
||||
let macos =
|
||||
runtime.strip_prefix("macos").ok_or_else(|| invalid_tag("missing macOS floor"))?;
|
||||
let (major, minor) = macos
|
||||
.split_once('.')
|
||||
.ok_or_else(|| invalid_tag("macOS floor must be major.minor"))?;
|
||||
let macos_major = parse_macos_version_component(major, "macOS major version", false)?;
|
||||
let macos_minor = parse_macos_version_component(minor, "macOS minor version", true)?;
|
||||
Ok(ParsedCompatibilityTag::MacOs(MacOsPlatform {
|
||||
architecture,
|
||||
node_major,
|
||||
macos_major,
|
||||
macos_minor,
|
||||
}))
|
||||
}
|
||||
"win32" => {
|
||||
let windows = runtime
|
||||
.strip_prefix("windows")
|
||||
.ok_or_else(|| invalid_tag("missing Windows floor"))?;
|
||||
let mut components = windows.split('.');
|
||||
let (Some(major), Some(minor), Some(build), None) =
|
||||
(components.next(), components.next(), components.next(), components.next())
|
||||
else {
|
||||
return Err(invalid_tag("Windows floor must be major.minor.build"));
|
||||
};
|
||||
let windows_major =
|
||||
parse_windows_version_component(major, "Windows major version", false, 1_000)?;
|
||||
let windows_minor =
|
||||
parse_windows_version_component(minor, "Windows minor version", true, 1_000)?;
|
||||
let windows_build =
|
||||
parse_windows_version_component(build, "Windows build number", false, 1_000_000)?;
|
||||
Ok(ParsedCompatibilityTag::Windows(WindowsPlatform {
|
||||
architecture,
|
||||
node_major,
|
||||
windows_major,
|
||||
windows_minor,
|
||||
windows_build,
|
||||
}))
|
||||
}
|
||||
_ => Err(invalid_tag("v1 only defines Linux, macOS, and Windows tags")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_macos_version_component(
|
||||
value: &str,
|
||||
label: &str,
|
||||
allow_zero: bool,
|
||||
) -> Result<u32, ArtifactProtocolError> {
|
||||
let number = parse_canonical_number(value, label, allow_zero)?;
|
||||
if number >= 1_000_000 {
|
||||
return Err(invalid_tag(&format!("{label} is too large")));
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
fn macos_version_rank(platform: MacOsPlatform<'_>) -> u64 {
|
||||
u64::from(platform.macos_major) * 1_000_000 + u64::from(platform.macos_minor)
|
||||
}
|
||||
|
||||
fn parse_windows_version_component(
|
||||
value: &str,
|
||||
label: &str,
|
||||
allow_zero: bool,
|
||||
exclusive_maximum: u32,
|
||||
) -> Result<u32, ArtifactProtocolError> {
|
||||
let number = parse_canonical_number(value, label, allow_zero)?;
|
||||
if number >= exclusive_maximum {
|
||||
return Err(invalid_tag(&format!("{label} is too large")));
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
fn windows_version_rank(platform: WindowsPlatform<'_>) -> u64 {
|
||||
u64::from(platform.windows_major) * 1_000_000_000
|
||||
+ u64::from(platform.windows_minor) * 1_000_000
|
||||
+ u64::from(platform.windows_build)
|
||||
}
|
||||
|
||||
fn version_floor_rank(
|
||||
consumer: ParsedCompatibilityTag<'_>,
|
||||
artifact: ParsedCompatibilityTag<'_>,
|
||||
) -> Option<u64> {
|
||||
match (consumer, artifact) {
|
||||
(ParsedCompatibilityTag::MacOs(consumer), ParsedCompatibilityTag::MacOs(artifact))
|
||||
if consumer.architecture == artifact.architecture
|
||||
&& consumer.node_major == artifact.node_major =>
|
||||
{
|
||||
macos_version_rank(consumer).checked_sub(macos_version_rank(artifact))
|
||||
}
|
||||
(ParsedCompatibilityTag::Windows(consumer), ParsedCompatibilityTag::Windows(artifact))
|
||||
if consumer.architecture == artifact.architecture
|
||||
&& consumer.node_major == artifact.node_major =>
|
||||
{
|
||||
windows_version_rank(consumer).checked_sub(windows_version_rank(artifact))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_canonical_number(
|
||||
|
||||
@@ -10,10 +10,11 @@ use sha2::{Digest as _, Sha512};
|
||||
|
||||
use crate::{
|
||||
ARTIFACT_KIND, ArtifactBlobUpload, ArtifactFile, ArtifactManifest, ArtifactPayload,
|
||||
BuilderProfile, CompatibilityConstraints, LinuxGlibcPlatform, OwnerScope, PackageIdentity,
|
||||
PublishArtifactRequest, SIGNATURE_ALGORITHM, SignedArtifactEnvelope, blob_id,
|
||||
compatibility_rank, linux_glibc_supported_tags, linux_glibc_tag, platform_fingerprint,
|
||||
validate_manifest_path, verify_blob,
|
||||
BuilderProfile, CompatibilityConstraints, LinuxGlibcPlatform, MacOsPlatform, OwnerScope,
|
||||
PackageIdentity, PublishArtifactRequest, SIGNATURE_ALGORITHM, SignedArtifactEnvelope,
|
||||
WindowsPlatform, blob_id, compatibility_rank, linux_glibc_supported_tags, linux_glibc_tag,
|
||||
macos_supported_tags, macos_tag, platform_fingerprint, validate_manifest_path, verify_blob,
|
||||
windows_supported_tags, windows_tag,
|
||||
};
|
||||
|
||||
fn integrity(bytes: &[u8]) -> String {
|
||||
@@ -24,6 +25,19 @@ fn linux(architecture: &str, glibc_minor: u32) -> LinuxGlibcPlatform<'_> {
|
||||
LinuxGlibcPlatform { architecture, node_major: 22, glibc_major: 2, glibc_minor }
|
||||
}
|
||||
|
||||
fn macos(architecture: &str, macos_major: u32, macos_minor: u32) -> MacOsPlatform<'_> {
|
||||
MacOsPlatform { architecture, node_major: 22, macos_major, macos_minor }
|
||||
}
|
||||
|
||||
fn windows(
|
||||
architecture: &str,
|
||||
windows_major: u32,
|
||||
windows_minor: u32,
|
||||
windows_build: u32,
|
||||
) -> WindowsPlatform<'_> {
|
||||
WindowsPlatform { architecture, node_major: 22, windows_major, windows_minor, windows_build }
|
||||
}
|
||||
|
||||
fn payload(file_integrity: String) -> ArtifactPayload {
|
||||
ArtifactPayload {
|
||||
kind: ARTIFACT_KIND.to_string(),
|
||||
@@ -273,7 +287,7 @@ fn compatibility_uses_the_consumers_preference_order() {
|
||||
assert_eq!(compatibility_rank(&fallback, &supported), Some(22));
|
||||
assert_eq!(
|
||||
compatibility_rank(&CompatibilityConstraints::Universal, &supported),
|
||||
Some(supported.len()),
|
||||
Some(u64::MAX),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
@@ -286,6 +300,109 @@ fn compatibility_uses_the_consumers_preference_order() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_compatibility_uses_product_version_floors() {
|
||||
let supported = macos_supported_tags(macos("arm64", 15, 5)).unwrap();
|
||||
assert_eq!(supported, ["pnpm:v1:darwin-arm64-node22-macos15.5"]);
|
||||
assert_eq!(
|
||||
platform_fingerprint(&supported).unwrap(),
|
||||
"b56fa5629b56d18308bbf7978d61b9afaf862e133ad18aef31588e0888eef3f8",
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![macos_tag(macos("arm64", 15, 4)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
Some(65),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![macos_tag(macos("arm64", 14, 6)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
Some(1_000_063),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![macos_tag(macos("arm64", 16, 0)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(&CompatibilityConstraints::Universal, &supported),
|
||||
Some(u64::MAX),
|
||||
);
|
||||
|
||||
let multiple_supported =
|
||||
vec![macos_tag(macos("arm64", 15, 5)).unwrap(), macos_tag(macos("arm64", 14, 6)).unwrap()];
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![macos_tag(macos("arm64", 14, 6)).unwrap()],
|
||||
},
|
||||
&multiple_supported,
|
||||
),
|
||||
Some(1),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![macos_tag(macos("arm64", 15, 4)).unwrap()],
|
||||
},
|
||||
&multiple_supported,
|
||||
),
|
||||
Some(65),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_compatibility_uses_kernel_version_floors() {
|
||||
let supported = windows_supported_tags(windows("x64", 10, 0, 26_100)).unwrap();
|
||||
assert_eq!(supported, ["pnpm:v1:win32-x64-node22-windows10.0.26100"]);
|
||||
assert_eq!(
|
||||
platform_fingerprint(&supported).unwrap(),
|
||||
"f5590f12a6d651acdcb3b60d7d25a5d2e1ad2f5af3e53d841391dec9e871c46e",
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![windows_tag(windows("x64", 10, 0, 22_621)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
Some(3_543),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![windows_tag(windows("x64", 6, 3, 9_600)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
Some(3_997_016_564),
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(
|
||||
&CompatibilityConstraints::Tagged {
|
||||
tags: vec![windows_tag(windows("x64", 10, 0, 26_101)).unwrap()],
|
||||
},
|
||||
&supported,
|
||||
),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
compatibility_rank(&CompatibilityConstraints::Universal, &supported),
|
||||
Some(u64::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_tags_and_platform_fingerprints_are_canonical() {
|
||||
let supported = linux_glibc_supported_tags(linux("x64", 3)).unwrap();
|
||||
@@ -306,6 +423,10 @@ fn compatibility_tags_and_platform_fingerprints_are_canonical() {
|
||||
for invalid in [
|
||||
"pnpm:v2:linux-x64-node22-glibc2.17",
|
||||
"pnpm:v1:darwin-x64-node22-glibc2.17",
|
||||
"pnpm:v1:darwin-x64-node22-macos15",
|
||||
"pnpm:v1:darwin-x64-node22-macos015.5",
|
||||
"pnpm:v1:win32-x64-node22-windows10.0",
|
||||
"pnpm:v1:win32-x64-node22-windows10.0.026100",
|
||||
"pnpm:v1:linux-x64-node022-glibc2.17",
|
||||
"pnpm:v1:linux-x64-node22-glibc02.17",
|
||||
"pnpm:v1:linux-x64-node22-glibc2",
|
||||
|
||||
+20
-8
@@ -72,7 +72,7 @@ remoteSideEffectsCache:
|
||||
`requiresBuild: true`, pass `allowBuilds`, and have a verified source integrity.
|
||||
`--ignore-scripts` disables remote reuse. An unavailable server, invalid
|
||||
signature, incompatible platform, or bad blob falls back to the ordinary local
|
||||
build. The PoC supports Linux glibc on x64 and arm64.
|
||||
build. The PoC supports Linux glibc, macOS, and Windows on x64 and arm64.
|
||||
|
||||
### Trust material
|
||||
|
||||
@@ -211,12 +211,14 @@ artifacts.
|
||||
|
||||
### v0 compatibility tags
|
||||
|
||||
The PoC intentionally defines one narrow tagged platform vocabulary rather
|
||||
The PoC intentionally defines an explicit tagged platform vocabulary rather
|
||||
than interpreting unknown producer claims. `universal` is the positive claim
|
||||
for platform-independent output. The only `tagged` form is:
|
||||
for platform-independent output. The `tagged` forms are:
|
||||
|
||||
```text
|
||||
pnpm:v1:linux-<architecture>-node<major>-glibc<major>.<minor>
|
||||
pnpm:v1:darwin-<architecture>-node<major>-macos<major>.<minor>
|
||||
pnpm:v1:win32-<architecture>-node<major>-windows<major>.<minor>.<build>
|
||||
```
|
||||
|
||||
`architecture` is `x64` or `arm64`; all numeric components are canonical
|
||||
@@ -224,15 +226,25 @@ unsigned decimals without leading zeroes. A consumer generates tags for its
|
||||
glibc version down to minor zero, most recent floor first. For example, glibc
|
||||
2.3 advertises `glibc2.3`, `glibc2.2`, `glibc2.1`, and `glibc2.0`. Matching is
|
||||
exact against that ordered set, so an artifact tagged with a 2.1 floor serves a
|
||||
2.3 consumer. Tagged matches beat `universal`; equal-rank variants are ordered
|
||||
by ascending signed-envelope digest. Unknown schemas, platforms, dimensions,
|
||||
or malformed tags are misses. Other platforms and libc families remain out of
|
||||
the PoC instead of being treated as compatible guesses.
|
||||
2.3 consumer.
|
||||
|
||||
A macOS consumer advertises one tag containing its product-version major and
|
||||
minor. A Windows consumer likewise advertises one tag containing its NT kernel
|
||||
major, minor, and build number. These versions are parsed minimum-runtime
|
||||
floors: the operating system, architecture, and Node major must match exactly,
|
||||
and the consumer version must be at least the artifact version. Exact matches
|
||||
precede derived floor matches, and the greatest compatible floor wins. Tagged
|
||||
matches beat `universal`; equal-rank variants are ordered by ascending
|
||||
signed-envelope digest. Unknown schemas, platforms, dimensions, or malformed
|
||||
tags are misses. Other operating systems and libc families remain out of the
|
||||
PoC instead of being treated as compatible guesses.
|
||||
|
||||
`platformFingerprint` is SHA-256 over the ASCII bytes
|
||||
`pnpm-platform-fingerprint-v1\0`, followed by every canonical supported tag in
|
||||
preference order and a NUL byte after each tag. Duplicate tags and lists longer
|
||||
than 64 entries are rejected.
|
||||
than 64 entries are rejected. macOS and Windows therefore fingerprint their
|
||||
one exact consumer tag, so a macOS minor or Windows kernel-build change creates
|
||||
a distinct platform fingerprint.
|
||||
|
||||
### Signed envelope and blobs
|
||||
|
||||
|
||||
@@ -22,12 +22,16 @@ export {
|
||||
type BuilderProfile,
|
||||
COMPATIBILITY_TAG_SCHEMA,
|
||||
type CompatibilityConstraints,
|
||||
compatibilityRank,
|
||||
createSignedArtifactEnvelope,
|
||||
downloadSharedArtifactBlob,
|
||||
INPUT_KEY_PREFIX,
|
||||
linuxGlibcCompatibilityTag,
|
||||
type LinuxGlibcPlatform,
|
||||
linuxGlibcSupportedTags,
|
||||
macOSCompatibilityTag,
|
||||
type MacOSPlatform,
|
||||
macOSSupportedTags,
|
||||
type OwnerScope,
|
||||
type PackageIdentity,
|
||||
platformFingerprint,
|
||||
@@ -42,4 +46,7 @@ export {
|
||||
verifySignedArtifactEnvelope,
|
||||
verifyStoredSharedSideEffects,
|
||||
type VerifyStoredSharedSideEffectsOptions,
|
||||
windowsCompatibilityTag,
|
||||
type WindowsPlatform,
|
||||
windowsSupportedTags,
|
||||
} from './sharedSideEffects.js'
|
||||
@@ -1,5 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createPrivateKey } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import { release as osRelease } from 'node:os'
|
||||
import util from 'node:util'
|
||||
|
||||
import { calcDepState, calcDepStateInputKey, type DepsGraph, type DepsStateCache } from '@pnpm/deps.graph-hasher'
|
||||
@@ -18,7 +20,11 @@ import {
|
||||
createSignedArtifactEnvelope,
|
||||
downloadSharedArtifactBlob,
|
||||
linuxGlibcCompatibilityTag,
|
||||
type LinuxGlibcPlatform,
|
||||
linuxGlibcSupportedTags,
|
||||
macOSCompatibilityTag,
|
||||
type MacOSPlatform,
|
||||
macOSSupportedTags,
|
||||
ownerNamespace,
|
||||
platformFingerprint,
|
||||
pnprSupportsSharedSideEffects,
|
||||
@@ -28,6 +34,9 @@ import {
|
||||
type SignedArtifactEnvelope,
|
||||
type VerifiedArtifact,
|
||||
verifyStoredSharedSideEffects,
|
||||
windowsCompatibilityTag,
|
||||
type WindowsPlatform,
|
||||
windowsSupportedTags,
|
||||
} from './sharedSideEffects.js'
|
||||
|
||||
export interface RemoteSideEffectsInstallNode<T extends string> {
|
||||
@@ -80,6 +89,11 @@ export interface RemoteSideEffectsRestorer<T extends string> {
|
||||
restore: (node: RemoteSideEffectsInstallNode<T>) => Promise<string | undefined>
|
||||
}
|
||||
|
||||
type ArtifactPlatform =
|
||||
| { kind: 'linuxGlibc', platform: LinuxGlibcPlatform }
|
||||
| { kind: 'macOS', platform: MacOSPlatform }
|
||||
| { kind: 'windows', platform: WindowsPlatform }
|
||||
|
||||
/**
|
||||
* How long the first queued candidate waits for company before its lookup
|
||||
* leaves. Long enough to gather the packages whose fetches land together,
|
||||
@@ -108,23 +122,23 @@ export function canRestoreRemoteSideEffects (opts: RemoteSideEffectsPrerequisite
|
||||
(opts.settings.packages?.length ?? 0) > 0 &&
|
||||
Object.keys(opts.settings.trustedKeys ?? {}).length > 0 &&
|
||||
!opts.ignoreScripts &&
|
||||
currentLinuxGlibcPlatform(opts.nodeVersion) != null
|
||||
currentArtifactPlatform(opts.nodeVersion) != null
|
||||
}
|
||||
|
||||
export function createRemoteSideEffectsRestorer<T extends string> (
|
||||
opts: RemoteSideEffectsRestorerOptions<T>
|
||||
): RemoteSideEffectsRestorer<T> | undefined {
|
||||
if (!canRestoreRemoteSideEffects(opts)) return undefined
|
||||
const platform = currentLinuxGlibcPlatform(opts.nodeVersion)
|
||||
const artifactPlatform = currentArtifactPlatform(opts.nodeVersion)
|
||||
const { pnprServer, settings } = opts
|
||||
const organization = settings?.organization
|
||||
if (platform == null || settings == null || organization == null) return undefined
|
||||
if (artifactPlatform == null || settings == null || organization == null) return undefined
|
||||
const registryUrl = pnprServer
|
||||
const ownerName = organization
|
||||
const owner = { type: 'organization', name: ownerName } as const
|
||||
let supportedTags: string[]
|
||||
try {
|
||||
supportedTags = linuxGlibcSupportedTags(platform)
|
||||
supportedTags = artifactSupportedTags(artifactPlatform)
|
||||
} catch (err: unknown) {
|
||||
opts.warn?.(`Remote side-effects platform is unsupported: ${errorMessage(err)}`)
|
||||
return undefined
|
||||
@@ -607,9 +621,9 @@ export async function publishBuiltSharedSideEffects<T extends string> (
|
||||
) return
|
||||
const { builderId, keyId, organization, privateKey } = opts.settings
|
||||
if (organization == null) return
|
||||
const platform = currentLinuxGlibcPlatform(opts.nodeVersion)
|
||||
const artifactPlatform = currentArtifactPlatform(opts.nodeVersion)
|
||||
const sourceIntegrity = verifiedIntegrity(opts.resolution)
|
||||
if (keyId == null || privateKey == null || builderId == null || platform == null || sourceIntegrity == null) return
|
||||
if (keyId == null || privateKey == null || builderId == null || artifactPlatform == null || sourceIntegrity == null) return
|
||||
const manifest = await artifactManifest(opts.upload)
|
||||
if (manifest == null) return
|
||||
const inputKey = calcDepStateInputKey({
|
||||
@@ -632,7 +646,7 @@ export async function publishBuiltSharedSideEffects<T extends string> (
|
||||
},
|
||||
compatibility: {
|
||||
kind: 'tagged',
|
||||
tags: [linuxGlibcCompatibilityTag(platform)],
|
||||
tags: [artifactCompatibilityTag(artifactPlatform)],
|
||||
},
|
||||
manifest: manifest.manifest,
|
||||
}
|
||||
@@ -683,25 +697,96 @@ async function artifactManifest (upload: UploadPkgToStoreResult): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
function currentLinuxGlibcPlatform (nodeVersion?: string): {
|
||||
architecture: string
|
||||
nodeMajor: number
|
||||
glibcMajor: number
|
||||
glibcMinor: number
|
||||
} | undefined {
|
||||
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) return undefined
|
||||
const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } }
|
||||
const glibc = report.header?.glibcVersionRuntime?.split('.')
|
||||
const nodeMajor = Number((nodeVersion ?? process.version).replace(/^v/, '').split('.')[0])
|
||||
const glibcMajor = Number(glibc?.[0])
|
||||
const glibcMinor = Number(glibc?.[1])
|
||||
if (![nodeMajor, glibcMajor, glibcMinor].every(Number.isSafeInteger)) return undefined
|
||||
return {
|
||||
architecture: process.arch,
|
||||
nodeMajor,
|
||||
glibcMajor,
|
||||
glibcMinor,
|
||||
function currentArtifactPlatform (nodeVersion?: string): ArtifactPlatform | undefined {
|
||||
if (!['x64', 'arm64'].includes(process.arch)) return undefined
|
||||
const version = nodeVersion ?? process.version
|
||||
const nodeMajor = Number((version.startsWith('v') ? version.slice(1) : version).split('.')[0])
|
||||
if (!Number.isSafeInteger(nodeMajor) || nodeMajor <= 0) return undefined
|
||||
if (process.platform === 'linux') {
|
||||
const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } }
|
||||
const [glibcMajor, glibcMinor] = report.header?.glibcVersionRuntime?.split('.').map(Number) ?? []
|
||||
if (![glibcMajor, glibcMinor].every(Number.isSafeInteger)) return undefined
|
||||
return {
|
||||
kind: 'linuxGlibc',
|
||||
platform: { architecture: process.arch, nodeMajor, glibcMajor, glibcMinor },
|
||||
}
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
const version = macOSProductVersion()
|
||||
if (version == null) return undefined
|
||||
return {
|
||||
kind: 'macOS',
|
||||
platform: {
|
||||
architecture: process.arch,
|
||||
nodeMajor,
|
||||
macOSMajor: version.major,
|
||||
macOSMinor: version.minor,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
const version = windowsKernelVersion(osRelease())
|
||||
if (version == null) return undefined
|
||||
return {
|
||||
kind: 'windows',
|
||||
platform: {
|
||||
architecture: process.arch,
|
||||
nodeMajor,
|
||||
windowsMajor: version.major,
|
||||
windowsMinor: version.minor,
|
||||
windowsBuild: version.build,
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function artifactCompatibilityTag (artifactPlatform: ArtifactPlatform): string {
|
||||
switch (artifactPlatform.kind) {
|
||||
case 'linuxGlibc': return linuxGlibcCompatibilityTag(artifactPlatform.platform)
|
||||
case 'macOS': return macOSCompatibilityTag(artifactPlatform.platform)
|
||||
case 'windows': return windowsCompatibilityTag(artifactPlatform.platform)
|
||||
}
|
||||
}
|
||||
|
||||
function artifactSupportedTags (artifactPlatform: ArtifactPlatform): string[] {
|
||||
switch (artifactPlatform.kind) {
|
||||
case 'linuxGlibc': return linuxGlibcSupportedTags(artifactPlatform.platform)
|
||||
case 'macOS': return macOSSupportedTags(artifactPlatform.platform)
|
||||
case 'windows': return windowsSupportedTags(artifactPlatform.platform)
|
||||
}
|
||||
}
|
||||
|
||||
let cachedMacOSProductVersion: { major: number, minor: number } | null | undefined
|
||||
|
||||
function macOSProductVersion (): { major: number, minor: number } | undefined {
|
||||
if (cachedMacOSProductVersion !== undefined) return cachedMacOSProductVersion ?? undefined
|
||||
try {
|
||||
const [major, minor] = execFileSync('/usr/bin/sw_vers', ['-productVersion'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5_000,
|
||||
}).trim().split('.').map(Number)
|
||||
cachedMacOSProductVersion =
|
||||
Number.isSafeInteger(major) && major > 0 && major < 1_000_000 &&
|
||||
Number.isSafeInteger(minor) && minor >= 0 && minor < 1_000_000
|
||||
? { major, minor }
|
||||
: null
|
||||
} catch {
|
||||
cachedMacOSProductVersion = null
|
||||
}
|
||||
return cachedMacOSProductVersion ?? undefined
|
||||
}
|
||||
|
||||
function windowsKernelVersion (release: string): { major: number, minor: number, build: number } | undefined {
|
||||
const components = release.split('.')
|
||||
if (components.length !== 3) return undefined
|
||||
const [major, minor, build] = components.map(Number)
|
||||
if (
|
||||
!Number.isSafeInteger(major) || major <= 0 || major >= 1_000 ||
|
||||
!Number.isSafeInteger(minor) || minor < 0 || minor >= 1_000 ||
|
||||
!Number.isSafeInteger(build) || build <= 0 || build >= 1_000_000
|
||||
) return undefined
|
||||
return { major, minor, build }
|
||||
}
|
||||
|
||||
function verifiedIntegrity (resolution: LockfileResolution): string | undefined {
|
||||
|
||||
@@ -45,6 +45,21 @@ export interface LinuxGlibcPlatform {
|
||||
glibcMinor: number
|
||||
}
|
||||
|
||||
export interface MacOSPlatform {
|
||||
architecture: string
|
||||
nodeMajor: number
|
||||
macOSMajor: number
|
||||
macOSMinor: number
|
||||
}
|
||||
|
||||
export interface WindowsPlatform {
|
||||
architecture: string
|
||||
nodeMajor: number
|
||||
windowsMajor: number
|
||||
windowsMinor: number
|
||||
windowsBuild: number
|
||||
}
|
||||
|
||||
export interface BuilderProfile {
|
||||
imageDigest?: string
|
||||
architectureBaseline: string
|
||||
@@ -293,7 +308,7 @@ export async function resolveSharedSideEffects (
|
||||
payload.sourceIntegrity !== candidate.sourceIntegrity ||
|
||||
!ownersEqual(payload.owner, candidate.owner)
|
||||
) continue
|
||||
const rank = compatibilityRank(payload.compatibility, opts.supportedTags)
|
||||
const rank = rankCompatibility(payload.compatibility, opts.supportedTags)
|
||||
if (rank == null) continue
|
||||
const pinnedDigest = opts.pinnedEnvelopeDigests?.get(candidate.key)
|
||||
if (pinnedDigest != null && digest !== pinnedDigest) continue
|
||||
@@ -676,12 +691,48 @@ function insertUniquePath (path: string, exact: Set<string>, folded: Set<string>
|
||||
folded.add(caseFolded)
|
||||
}
|
||||
|
||||
function compatibilityRank (constraints: CompatibilityConstraints, supportedTags: string[]): number | undefined {
|
||||
if (constraints.kind === 'universal') return supportedTags.length
|
||||
for (let index = 0; index < supportedTags.length; index++) {
|
||||
if (constraints.tags.includes(supportedTags[index])) return index
|
||||
export function compatibilityRank (constraints: CompatibilityConstraints, supportedTags: string[]): number | undefined {
|
||||
try {
|
||||
validateSupportedTags(supportedTags)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
return rankCompatibility(constraints, supportedTags)
|
||||
}
|
||||
|
||||
function rankCompatibility (constraints: CompatibilityConstraints, supportedTags: string[]): number | undefined {
|
||||
if (constraints.kind === 'universal') return Number.MAX_SAFE_INTEGER
|
||||
let bestRank: number | undefined
|
||||
for (let index = 0; index < supportedTags.length; index++) {
|
||||
const supportedTag = supportedTags[index]
|
||||
for (const artifactTag of constraints.tags) {
|
||||
if (artifactTag === supportedTag) {
|
||||
bestRank = Math.min(bestRank ?? index, index)
|
||||
continue
|
||||
}
|
||||
let consumer: VersionedPlatform | undefined
|
||||
let artifact: VersionedPlatform | undefined
|
||||
try {
|
||||
consumer = parseVersionedCompatibilityTag(supportedTag)
|
||||
artifact = parseVersionedCompatibilityTag(artifactTag)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
consumer == null ||
|
||||
artifact == null ||
|
||||
consumer.kind !== artifact.kind ||
|
||||
consumer.platform.architecture !== artifact.platform.architecture ||
|
||||
consumer.platform.nodeMajor !== artifact.platform.nodeMajor
|
||||
) continue
|
||||
const consumerVersion = versionedPlatformRank(consumer)
|
||||
const artifactVersion = versionedPlatformRank(artifact)
|
||||
if (consumerVersion < artifactVersion) continue
|
||||
const rank = 64 + index * 1_000_000_000_000 + consumerVersion - artifactVersion
|
||||
bestRank = Math.min(bestRank ?? rank, rank)
|
||||
}
|
||||
}
|
||||
return bestRank
|
||||
}
|
||||
|
||||
export function linuxGlibcCompatibilityTag (
|
||||
@@ -711,6 +762,32 @@ export function linuxGlibcSupportedTags (
|
||||
)
|
||||
}
|
||||
|
||||
export function macOSSupportedTags (platform: MacOSPlatform): string[] {
|
||||
return [macOSCompatibilityTag(platform)]
|
||||
}
|
||||
|
||||
export function macOSCompatibilityTag (
|
||||
platform: MacOSPlatform
|
||||
): string {
|
||||
const { architecture, nodeMajor, macOSMajor, macOSMinor } = platform
|
||||
const tag = `${COMPATIBILITY_TAG_SCHEMA}:darwin-${architecture}-node${nodeMajor}-macos${macOSMajor}.${macOSMinor}`
|
||||
validateCompatibilityTag(tag)
|
||||
return tag
|
||||
}
|
||||
|
||||
export function windowsSupportedTags (platform: WindowsPlatform): string[] {
|
||||
return [windowsCompatibilityTag(platform)]
|
||||
}
|
||||
|
||||
export function windowsCompatibilityTag (
|
||||
platform: WindowsPlatform
|
||||
): string {
|
||||
const { architecture, nodeMajor, windowsMajor, windowsMinor, windowsBuild } = platform
|
||||
const tag = `${COMPATIBILITY_TAG_SCHEMA}:win32-${architecture}-node${nodeMajor}-windows${windowsMajor}.${windowsMinor}.${windowsBuild}`
|
||||
validateCompatibilityTag(tag)
|
||||
return tag
|
||||
}
|
||||
|
||||
export function platformFingerprint (supportedTags: string[]): string {
|
||||
validateSupportedTags(supportedTags)
|
||||
const hash = createHash('sha256').update('pnpm-platform-fingerprint-v1\0')
|
||||
@@ -737,16 +814,75 @@ function validateCompatibilityTag (tag: string): void {
|
||||
}
|
||||
const parts = tag.slice(COMPATIBILITY_TAG_SCHEMA.length + 1).split('-')
|
||||
if (parts.length !== 4) throw new Error('Shared artifact compatibility tag has the wrong number of dimensions')
|
||||
const [os, architecture, node, libc] = parts
|
||||
if (os !== 'linux' || !['x64', 'arm64'].includes(architecture)) {
|
||||
throw new Error('Shared artifact compatibility tag only supports Linux x64 and arm64 in v1')
|
||||
const [os, architecture, node, runtime] = parts
|
||||
if (!['x64', 'arm64'].includes(architecture)) {
|
||||
throw new Error('Shared artifact compatibility tag only supports x64 and arm64 in v1')
|
||||
}
|
||||
parseCanonicalNumber(node.startsWith('node') ? node.slice(4) : '', 'Node major version', false)
|
||||
const glibc = libc.startsWith('glibc') ? libc.slice(5) : ''
|
||||
const version = glibc.split('.')
|
||||
if (version.length !== 2) throw new Error('Shared artifact glibc floor must be major.minor')
|
||||
parseCanonicalNumber(version[0], 'glibc major version', false)
|
||||
parseCanonicalNumber(version[1], 'glibc minor version', true)
|
||||
if (os === 'linux') {
|
||||
const glibc = runtime.startsWith('glibc') ? runtime.slice(5) : ''
|
||||
const version = glibc.split('.')
|
||||
if (version.length !== 2) throw new Error('Shared artifact glibc floor must be major.minor')
|
||||
parseCanonicalNumber(version[0], 'glibc major version', false)
|
||||
parseCanonicalNumber(version[1], 'glibc minor version', true)
|
||||
} else if (os === 'darwin') {
|
||||
const macOS = runtime.startsWith('macos') ? runtime.slice(5) : ''
|
||||
const version = macOS.split('.')
|
||||
if (version.length !== 2) throw new Error('Shared artifact macOS floor must be major.minor')
|
||||
const major = parseCanonicalNumber(version[0], 'macOS major version', false)
|
||||
const minor = parseCanonicalNumber(version[1], 'macOS minor version', true)
|
||||
if (major >= 1_000_000 || minor >= 1_000_000) {
|
||||
throw new Error('Shared artifact macOS version component is too large')
|
||||
}
|
||||
} else if (os === 'win32') {
|
||||
const windows = runtime.startsWith('windows') ? runtime.slice(7) : ''
|
||||
const version = windows.split('.')
|
||||
if (version.length !== 3) throw new Error('Shared artifact Windows floor must be major.minor.build')
|
||||
const major = parseCanonicalNumber(version[0], 'Windows major version', false)
|
||||
const minor = parseCanonicalNumber(version[1], 'Windows minor version', true)
|
||||
const build = parseCanonicalNumber(version[2], 'Windows build number', false)
|
||||
if (major >= 1_000 || minor >= 1_000 || build >= 1_000_000) {
|
||||
throw new Error('Shared artifact Windows version component is too large')
|
||||
}
|
||||
} else {
|
||||
throw new Error('Shared artifact compatibility tag only supports Linux, macOS, and Windows in v1')
|
||||
}
|
||||
}
|
||||
|
||||
type VersionedPlatform =
|
||||
| { kind: 'macOS', platform: MacOSPlatform }
|
||||
| { kind: 'windows', platform: WindowsPlatform }
|
||||
|
||||
function parseVersionedCompatibilityTag (tag: string): VersionedPlatform | undefined {
|
||||
validateCompatibilityTag(tag)
|
||||
const [os, architecture, node, runtime] = tag.slice(COMPATIBILITY_TAG_SCHEMA.length + 1).split('-')
|
||||
const nodeMajor = Number(node.slice(4))
|
||||
if (os === 'darwin') {
|
||||
const [macOSMajor, macOSMinor] = runtime.slice(5).split('.').map(Number)
|
||||
return {
|
||||
kind: 'macOS',
|
||||
platform: { architecture, nodeMajor, macOSMajor, macOSMinor },
|
||||
}
|
||||
}
|
||||
if (os === 'win32') {
|
||||
const [windowsMajor, windowsMinor, windowsBuild] = runtime.slice(7).split('.').map(Number)
|
||||
return {
|
||||
kind: 'windows',
|
||||
platform: { architecture, nodeMajor, windowsMajor, windowsMinor, windowsBuild },
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function versionedPlatformRank (versionedPlatform: VersionedPlatform): number {
|
||||
if (versionedPlatform.kind === 'macOS') {
|
||||
return versionedPlatform.platform.macOSMajor * 1_000_000 + versionedPlatform.platform.macOSMinor
|
||||
}
|
||||
return (
|
||||
versionedPlatform.platform.windowsMajor * 1_000_000_000 +
|
||||
versionedPlatform.platform.windowsMinor * 1_000_000 +
|
||||
versionedPlatform.platform.windowsBuild
|
||||
)
|
||||
}
|
||||
|
||||
function parseCanonicalNumber (value: string, label: string, allowZero: boolean): number {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash, generateKeyPairSync } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
@@ -13,10 +14,11 @@ import {
|
||||
createRemoteSideEffectsRestorer,
|
||||
createSignedArtifactEnvelope,
|
||||
linuxGlibcCompatibilityTag,
|
||||
type LinuxGlibcPlatform,
|
||||
macOSCompatibilityTag,
|
||||
publishBuiltSharedSideEffects,
|
||||
type SignedArtifactEnvelope,
|
||||
verifySignedArtifactEnvelope,
|
||||
windowsCompatibilityTag,
|
||||
} from '@pnpm/pnpr.client'
|
||||
import type { PackageFilesResponse, SideEffectsDiff, StoreController } from '@pnpm/store.controller-types'
|
||||
import type { DepPath } from '@pnpm/types'
|
||||
@@ -30,8 +32,8 @@ const builtFile = Buffer.from('compiled native addon')
|
||||
const builtFileIntegrity = `sha512-${createHash('sha512').update(builtFile).digest('base64')}`
|
||||
describe('install remote side-effects', () => {
|
||||
test('hydrates the store and selects a verified remote build', async () => {
|
||||
const platform = currentLinuxGlibcPlatform()
|
||||
if (platform == null) return
|
||||
const compatibilityTag = currentArtifactCompatibilityTag()
|
||||
if (compatibilityTag == null) return
|
||||
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' })
|
||||
const trustedKeys = {
|
||||
@@ -69,7 +71,7 @@ describe('install remote side-effects', () => {
|
||||
},
|
||||
compatibility: {
|
||||
kind: 'tagged',
|
||||
tags: [linuxGlibcCompatibilityTag(platform)],
|
||||
tags: [compatibilityTag],
|
||||
},
|
||||
manifest: {
|
||||
added: [
|
||||
@@ -824,7 +826,7 @@ describe('install remote side-effects', () => {
|
||||
})
|
||||
|
||||
test('publishes a signed build diff produced by install', async () => {
|
||||
if (currentLinuxGlibcPlatform() == null) return
|
||||
if (currentArtifactCompatibilityTag() == null) return
|
||||
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' })
|
||||
let publishedBody: Buffer | undefined
|
||||
@@ -909,13 +911,27 @@ describe('install remote side-effects', () => {
|
||||
})
|
||||
})
|
||||
|
||||
function currentLinuxGlibcPlatform (): LinuxGlibcPlatform | undefined {
|
||||
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) return undefined
|
||||
const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } }
|
||||
const [glibcMajor, glibcMinor] = report.header?.glibcVersionRuntime?.split('.').map(Number) ?? []
|
||||
function currentArtifactCompatibilityTag (): string | undefined {
|
||||
if (!['x64', 'arm64'].includes(process.arch)) return undefined
|
||||
const nodeMajor = Number(process.versions.node.split('.')[0])
|
||||
if (![nodeMajor, glibcMajor, glibcMinor].every(Number.isSafeInteger)) return undefined
|
||||
return { architecture: process.arch, nodeMajor, glibcMajor, glibcMinor }
|
||||
if (!Number.isSafeInteger(nodeMajor)) return undefined
|
||||
if (process.platform === 'linux') {
|
||||
const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } }
|
||||
const [glibcMajor, glibcMinor] = report.header?.glibcVersionRuntime?.split('.').map(Number) ?? []
|
||||
if (![glibcMajor, glibcMinor].every(Number.isSafeInteger)) return undefined
|
||||
return linuxGlibcCompatibilityTag({ architecture: process.arch, nodeMajor, glibcMajor, glibcMinor })
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
const [macOSMajor, macOSMinor] = execFileSync('/usr/bin/sw_vers', ['-productVersion'], {
|
||||
encoding: 'utf8',
|
||||
}).trim().split('.').map(Number)
|
||||
return macOSCompatibilityTag({ architecture: process.arch, nodeMajor, macOSMajor, macOSMinor })
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
const [windowsMajor, windowsMinor, windowsBuild] = os.release().split('.').map(Number)
|
||||
return windowsCompatibilityTag({ architecture: process.arch, nodeMajor, windowsMajor, windowsMinor, windowsBuild })
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function listen (server: ReturnType<typeof createServer>): Promise<string> {
|
||||
|
||||
@@ -4,15 +4,20 @@ import { createServer } from 'node:http'
|
||||
import { describe, expect, test } from '@jest/globals'
|
||||
import {
|
||||
type ArtifactPayload,
|
||||
compatibilityRank,
|
||||
createSignedArtifactEnvelope,
|
||||
downloadSharedArtifactBlob,
|
||||
linuxGlibcCompatibilityTag,
|
||||
linuxGlibcSupportedTags,
|
||||
macOSCompatibilityTag,
|
||||
macOSSupportedTags,
|
||||
platformFingerprint,
|
||||
publishSharedSideEffects,
|
||||
resolveSharedSideEffects,
|
||||
signedArtifactEnvelopeDigest,
|
||||
verifySignedArtifactEnvelope,
|
||||
windowsCompatibilityTag,
|
||||
windowsSupportedTags,
|
||||
} from '@pnpm/pnpr.client'
|
||||
|
||||
const contents = Buffer.from('native-addon')
|
||||
@@ -180,9 +185,60 @@ describe('signed shared artifacts', () => {
|
||||
])
|
||||
expect(platformFingerprint(supportedTags)).toBe('fdfaaed730a56031779ee5e572e1e82aad454501ec5fbcfad6648e8a1e465f0c')
|
||||
|
||||
const macOSSupported = macOSSupportedTags(macOS(15, 5))
|
||||
expect(macOSSupported).toEqual(['pnpm:v1:darwin-arm64-node22-macos15.5'])
|
||||
expect(platformFingerprint(macOSSupported)).toBe('b56fa5629b56d18308bbf7978d61b9afaf862e133ad18aef31588e0888eef3f8')
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [macOSCompatibilityTag(macOS(15, 4))],
|
||||
}, macOSSupported)).toBe(65)
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [macOSCompatibilityTag(macOS(14, 6))],
|
||||
}, macOSSupported)).toBe(1_000_063)
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [macOSCompatibilityTag(macOS(16, 0))],
|
||||
}, macOSSupported)).toBeUndefined()
|
||||
expect(compatibilityRank({ kind: 'universal' }, macOSSupported)).toBe(Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const multipleMacOSSupported = [
|
||||
macOSCompatibilityTag(macOS(15, 5)),
|
||||
macOSCompatibilityTag(macOS(14, 6)),
|
||||
]
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [macOSCompatibilityTag(macOS(14, 6))],
|
||||
}, multipleMacOSSupported)).toBe(1)
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [macOSCompatibilityTag(macOS(15, 4))],
|
||||
}, multipleMacOSSupported)).toBe(65)
|
||||
|
||||
const windowsSupported = windowsSupportedTags(windows({ major: 10, minor: 0, build: 26_100 }))
|
||||
expect(windowsSupported).toEqual(['pnpm:v1:win32-x64-node22-windows10.0.26100'])
|
||||
expect(platformFingerprint(windowsSupported)).toBe('f5590f12a6d651acdcb3b60d7d25a5d2e1ad2f5af3e53d841391dec9e871c46e')
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [windowsCompatibilityTag(windows({ major: 10, minor: 0, build: 22_621 }))],
|
||||
}, windowsSupported)).toBe(3_543)
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [windowsCompatibilityTag(windows({ major: 6, minor: 3, build: 9_600 }))],
|
||||
}, windowsSupported)).toBe(3_997_016_564)
|
||||
expect(compatibilityRank({
|
||||
kind: 'tagged',
|
||||
tags: [windowsCompatibilityTag(windows({ major: 10, minor: 0, build: 26_101 }))],
|
||||
}, windowsSupported)).toBeUndefined()
|
||||
expect(compatibilityRank({ kind: 'universal' }, windowsSupported)).toBe(Number.MAX_SAFE_INTEGER)
|
||||
|
||||
for (const invalid of [
|
||||
'pnpm:v2:linux-x64-node22-glibc2.17',
|
||||
'pnpm:v1:darwin-x64-node22-glibc2.17',
|
||||
'pnpm:v1:darwin-x64-node22-macos15',
|
||||
'pnpm:v1:darwin-x64-node22-macos015.5',
|
||||
'pnpm:v1:win32-x64-node22-windows10.0',
|
||||
'pnpm:v1:win32-x64-node22-windows10.0.026100',
|
||||
'pnpm:v1:linux-x64-node022-glibc2.17',
|
||||
'pnpm:v1:linux-x64-node22-glibc02.17',
|
||||
'pnpm:v1:linux-x64-node22-glibc2',
|
||||
@@ -439,6 +495,25 @@ describe('signed shared artifacts', () => {
|
||||
})
|
||||
})
|
||||
|
||||
function macOS (macOSMajor: number, macOSMinor: number, architecture = 'arm64') {
|
||||
return { architecture, nodeMajor: 22, macOSMajor, macOSMinor }
|
||||
}
|
||||
|
||||
function windows ({ major, minor, build, architecture = 'x64' }: {
|
||||
major: number
|
||||
minor: number
|
||||
build: number
|
||||
architecture?: string
|
||||
}) {
|
||||
return {
|
||||
architecture,
|
||||
nodeMajor: 22,
|
||||
windowsMajor: major,
|
||||
windowsMinor: minor,
|
||||
windowsBuild: build,
|
||||
}
|
||||
}
|
||||
|
||||
async function listen (server: ReturnType<typeof createServer>): Promise<string> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
|
||||
Reference in new issue
Block a user