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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
| `redirect_pipenv_refused` | `redirect.warnings[]` (warning) | scan `--mode hosted` (pipenv): the Pipfile.lock pins another version or a non-registry / foreign source for the package — refused atomically across categories, and the patch is vetoed from the sibling Python rewriters (see the "Pipenv hosted redirect" section). |
| `redirect_pipenv_skipped` | `redirect.warnings[]` (warning) | scan `--mode hosted` (pipenv): no entry for the package, pipfile-spec < 6, an unparseable lock or a digest-less patch — nothing rewritten here; the sibling rewriters proceed. |
| `redirect_pipenv_installer_unknown` | `redirect.warnings[]` (warning) | scan `--mode hosted` (pipenv): the lock was rewritten with the modern `file` reference because no `pipenv` answered on PATH; Pipenv 7–11 projects need `path` — put that pipenv on PATH or set `SOCKET_PIPENV_MAJOR`. |
| `redirect_pypi_platform_wheel` | `redirect.warnings[]` (warning) | scan / get `--mode hosted` (pypi, every lane: uv.lock, PEP 723 script locks, pylock.toml, Pipfile.lock, poetry.lock, pdm.lock, requirements.txt, Hatch): the patch service granted the patch as a platform- or ABI-tagged wheel (any tag triple other than `<py>-none-any`, e.g. `cp311-cp311-manylinux…`). A hosted pin would narrow the cross-platform lock entry to that one wheel, so installs on any other interpreter, OS or architecture would fail and hosted rollback could not derive the upstream wheels to restore. The patch is withheld from every PyPI rewriter (nothing is written or confirmed for it, and a same-run `--vex` does not attest it); exit 0, like every hosted refusal. The tags are read as vendored mode reads them for `vendor_platform_locked`. |
| `redirect_pypi_platform_wheel` | `redirect.warnings[]` (warning) | scan / get `--mode hosted` (pypi, every lane: uv.lock, PEP 723 script locks, pylock.toml, Pipfile.lock, poetry.lock, pdm.lock, requirements.txt, Hatch): the patch service granted the patch as a platform-, ABI- or interpreter-tagged wheel (any tag triple other than `<py>-none-any` whose python tag set holds a generic Python 3 tag, `py3` or `py3<minor>`; e.g. `cp311-cp311-manylinux…`, or `cp311-none-any`, which pip installs on CPython 3.11 only). A hosted pin would narrow the cross-platform lock entry to that one wheel, so installs on any other interpreter, OS or architecture would fail and hosted rollback could not derive the upstream wheels to restore. The patch is withheld from every PyPI rewriter (nothing is written or confirmed for it, and a same-run `--vex` does not attest it); exit 0, like every hosted refusal. The tags are read as vendored mode reads them for `vendor_platform_locked`. |
| `pypi_pipenv_installer_unsupported` | `failed` | vendor (pipenv): the installed Pipenv is older than 2018 and cannot consume vendored wheel references — upgrade Pipenv or use hosted mode. |
| `pypi_pipenv_version_mismatch` | `failed` | vendor (pipenv): a category pins a different version than the patch — refused before any write. (`pypi_pipenv_invalid_wheel` retired in v5.0: the backend takes the orchestrator's resolved version instead of parsing the wheel filename.) |
| `pypi_poetry_symlink_unsupported` / `pypi_pipenv_symlink_unsupported` / `pypi_requirements_symlink_unsupported` | `failed` | vendor (pypi, v5.0): a target file (`pyproject.toml` / `poetry.lock`, `Pipfile` / `Pipfile.lock`, or any planned `requirements*.txt`) is a symlink — refused before any write on wire AND on revert (the revert keeps the artifact, `kept_artifact`); the twins of the existing pdm/uv symlink refusals. |
Expand Down
19 changes: 15 additions & 4 deletions crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,12 +528,23 @@ async fn live_pipfile_lock_conflict_vetoes_the_requirements_redirect() {
#[tokio::test]
#[serial]
async fn platform_wheel_is_not_pinned_into_the_lock() {
assert_wheel_tag_is_not_pinned("cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64")
.await;
}

/// #1048: a pure wheel bound to one interpreter (`cp311-none-any`) fails
/// `pipenv sync` on every other CPython minor, so it is refused the same
/// way as a platform-tagged one.
#[tokio::test]
#[serial]
async fn interpreter_bound_wheel_is_not_pinned_into_the_lock() {
assert_wheel_tag_is_not_pinned("cp311-none-any").await;
}

async fn assert_wheel_tag_is_not_pinned(tag: &str) {
let _major = MajorGuard::set("2026");
let server = MockServer::start().await;
let platform_url = HOSTED_URL.replace(
"py2.py3-none-any",
"cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64",
);
let platform_url = HOSTED_URL.replace("py2.py3-none-any", tag);
mock_api_serving(&server, &platform_url).await;
let tmp = tempfile::tempdir().unwrap();
write_project(tmp.path());
Expand Down
18 changes: 10 additions & 8 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,11 +551,12 @@ pub fn rewrite_registry_redirect_with_pipenv_version(
}

/// #701 / #932: the patch service can grant a pypi patch as a platform- or
/// ABI-tagged wheel (`…-cp311-cp311-manylinux…whl`). Every hosted PyPI lock
/// (uv.lock, PEP 723 script locks, pylock.toml, Pipfile.lock, poetry.lock,
/// pdm.lock, requirements.txt, Hatch's pyproject) is meant to install on
/// any platform its markers allow, and a hosted pin narrows the entry to
/// that one wheel: installs on any other interpreter, OS or architecture
/// ABI-tagged wheel (`…-cp311-cp311-manylinux…whl`), or as a pure wheel
/// bound to one interpreter (`…-cp311-none-any.whl`, #1048). Every hosted
/// PyPI lock (uv.lock, PEP 723 script locks, pylock.toml, Pipfile.lock,
/// poetry.lock, pdm.lock, requirements.txt, Hatch's pyproject) is meant to
/// install on any platform its markers allow, and a hosted pin narrows the
/// entry to that one wheel: installs on any other interpreter, OS or architecture
/// then fail, and hosted rollback cannot derive which upstream wheels to
/// put back. Fail closed like hosted gem (`redirect_gem_platform_unsupported`).
/// The tags are read the way vendored mode reads them for
Expand All @@ -581,9 +582,10 @@ pub fn pypi_platform_wheel_refusal(dep: &DepOverride) -> Option<RewriteWarning>
platform_locked.then(|| RewriteWarning {
code: "redirect_pypi_platform_wheel".into(),
detail: format!(
"the patched wheel for {}=={} is platform-specific ({tags}); pinning it \
would make the project's Python lockfiles install it on this platform \
only, so the redirect is skipped and nothing was written for it",
"the patched wheel for {}=={} is interpreter- or platform-specific \
({tags}); pinning it would make the project's Python lockfiles install \
it on this interpreter or platform only, so the redirect is skipped and \
nothing was written for it",
dep.name, dep.version
),
})
Expand Down
96 changes: 58 additions & 38 deletions crates/socket-patch-core/src/patch/redirect/platform_wheel_tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
//! #701 / #932: a pypi patch granted as a platform- or ABI-tagged wheel is
//! never pinned into a cross-platform Python lock. Each lane first proves
//! its fixture redirects a pure wheel (the control), then that the same
//! project with a `cp311-cp311-manylinux` wheel is left untouched, warned
//! about once, and confirms nothing.
//! project with a `cp311-cp311-manylinux` wheel, or an interpreter-bound
//! `cp311-none-any` one (#1048), is left untouched, warned about once, and
//! confirms nothing.

use super::*;

const PURE: &str = "urllib3-1.26.18-py2.py3-none-any.whl";
const PLATFORM: &str = "urllib3-1.26.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";
/// #1048: pip installs a `cp311-none-any` wheel on CPython 3.11 only.
const INTERPRETER: &str = "urllib3-1.26.18-cp311-none-any.whl";
/// Every wheel each lane must withhold, with the tag the warning names.
const REFUSED: [(&str, &str); 2] = [
(PLATFORM, "cp311-cp311-manylinux"),
(INTERPRETER, "cp311-none-any"),
];
const UUID: &str = "aaaaaaaa-0000-4000-8000-000000000701";
const HEX: &str = "34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07";

Expand Down Expand Up @@ -69,24 +77,26 @@ fn assert_lane(lane: &str, files: &[(&str, &str)], lock: &str) {
assert!(confirmed(&control), "{lane}: control not confirmed");
assert_eq!(platform_warnings(&control), 0, "{lane}");

let result = rewrite_registry_redirect(&files, &[dep(PLATFORM)]);
assert!(
result.files.is_empty() && result.edits.is_empty(),
"{lane}: platform wheel was pinned: {:?}",
result.files.keys().collect::<Vec<_>>()
);
assert!(!confirmed(&result), "{lane}: platform wheel confirmed");
assert_eq!(
platform_warnings(&result),
1,
"{lane}: {:?}",
result.warnings
);
let detail = &result.warnings[0].detail;
assert!(
detail.contains("urllib3==1.26.18") && detail.contains("cp311-cp311-manylinux"),
"{lane}: {detail}"
);
for (wheel, tag) in REFUSED {
let result = rewrite_registry_redirect(&files, &[dep(wheel)]);
assert!(
result.files.is_empty() && result.edits.is_empty(),
"{lane}: {wheel} was pinned: {:?}",
result.files.keys().collect::<Vec<_>>()
);
assert!(!confirmed(&result), "{lane}: {wheel} confirmed");
assert_eq!(
platform_warnings(&result),
1,
"{lane}: {wheel}: {:?}",
result.warnings
);
let detail = &result.warnings[0].detail;
assert!(
detail.contains("urllib3==1.26.18") && detail.contains(tag),
"{lane}: {detail}"
);
}
}

/// #701: a uv project's `uv.lock` (and its `[tool.uv.sources]`).
Expand Down Expand Up @@ -156,20 +166,22 @@ fn pipfile_lock_refuses_a_platform_wheel() {
control.warnings
);
assert!(confirmed(&control));
let result = rewrite_registry_redirect_with_pipenv_version(
&files,
&[dep(PLATFORM)],
&BTreeMap::new(),
major,
false,
);
assert!(
result.files.is_empty(),
"pipenv {major:?}: {:?}",
result.files
);
assert!(!confirmed(&result));
assert_eq!(platform_warnings(&result), 1, "{:?}", result.warnings);
for (wheel, _) in REFUSED {
let result = rewrite_registry_redirect_with_pipenv_version(
&files,
&[dep(wheel)],
&BTreeMap::new(),
major,
false,
);
assert!(
result.files.is_empty(),
"pipenv {major:?}: {wheel}: {:?}",
result.files
);
assert!(!confirmed(&result));
assert_eq!(platform_warnings(&result), 1, "{:?}", result.warnings);
}
}
assert_lane(
"Pipfile.lock",
Expand Down Expand Up @@ -234,9 +246,11 @@ fn hatch_refuses_a_platform_wheel() {
assert_lane("hatch", &[("pyproject.toml", pyproject)], "pyproject.toml");
}

/// The tag rule matches vendored mode's: a version-bound `cp311-none-any`
/// wheel and an sdist stay redirectable, an `abi3` or platform-only tag
/// does not, and a query or fragment on the serve URL is ignored.
/// The tag rule matches vendored mode's: a wheel any Python 3 accepts
/// (`py3`, `py2.py3`, `py311`, which later 3.x accept too) and an sdist
/// stay redirectable; an interpreter-bound python tag (`cp311`, `pp310`,
/// #1048), a Python-2-only one, an `abi3` or a platform-only tag does
/// not; and a query or fragment on the serve URL is ignored.
#[test]
fn only_platform_or_abi_tagged_wheels_are_withheld() {
let files: BTreeMap<String, String> = [(
Expand All @@ -247,7 +261,13 @@ fn only_platform_or_abi_tagged_wheels_are_withheld() {
.collect();
for (artifact, refused) in [
(PURE.to_string(), false),
("urllib3-1.26.18-cp311-none-any.whl".to_string(), false),
("urllib3-1.26.18-py3-none-any.whl".to_string(), false),
("urllib3-1.26.18-py311-none-any.whl".to_string(), false),
("urllib3-1.26.18-cp311.py3-none-any.whl".to_string(), false),
(INTERPRETER.to_string(), true),
("urllib3-1.26.18-pp310-none-any.whl".to_string(), true),
("urllib3-1.26.18-py2-none-any.whl".to_string(), true),
("urllib3-1.26.18-cp311.cp312-none-any.whl".to_string(), true),
("urllib3-1.26.18.tar.gz".to_string(), false),
(format!("{PURE}?token=x#sha256={HEX}"), false),
(PLATFORM.to_string(), true),
Expand Down
65 changes: 62 additions & 3 deletions crates/socket-patch-core/src/vendor/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1256,8 +1256,8 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
warnings.push(VendorWarning::new(
"vendor_platform_locked",
format!(
"the vendored wheel for {canon_name}=={version} is platform-specific \
({platform_tags_display}); {per_flavor}"
"the vendored wheel for {canon_name}=={version} is interpreter- or \
platform-specific ({platform_tags_display}); {per_flavor}"
),
));
}
Expand Down Expand Up @@ -3168,10 +3168,65 @@ wheels = [
);
}

/// #1048: a pure wheel whose python tag binds one interpreter
/// (`cp311-none-any`) installs on CPython 3.11 only, so it gets the same
/// `vendor_platform_locked` advisory as an ABI- or platform-tagged one.
#[tokio::test]
async fn interpreter_bound_tag_sets_platform_locked_and_warns() {
let fx = e2e_fixture().await;
tokio::fs::write(
fx.site_packages.join("six-1.16.0.dist-info/WHEEL"),
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: cp311-none-any\n",
)
.await
.unwrap();
let sources = PatchSources::blobs_only(&fx.blobs);
let outcome = crate::vendor::test_support::vendor_pypi(
"pkg:pypi/six@1.16.0",
&fx.site_packages,
&fx.root,
&fx.record,
&sources,
"2026-06-09T00:00:00Z",
false,
false,
None,
)
.await;
let VendorOutcome::Done {
result,
entry,
warnings,
} = outcome
else {
panic!("expected Done, got {outcome:?}");
};
assert!(result.success, "{:?}", result.error);
let entry = entry.unwrap();
assert!(entry
.artifact
.path
.ends_with("six-1.16.0-cp311-none-any.whl"));
assert_eq!(entry.artifact.platform_locked, Some(true));
let warning = warnings
.iter()
.find(|w| w.code == "vendor_platform_locked")
.unwrap_or_else(|| panic!("{warnings:?}"));
assert!(warning.detail.contains("cp311-none-any"), "{warning:?}");
}

#[test]
fn platform_specific_tag_detection() {
assert!(!tag_is_platform_specific("py3-none-any"));
assert!(!tag_is_platform_specific("cp311-none-any"));
assert!(!tag_is_platform_specific("py2.py3-none-any"));
assert!(!tag_is_platform_specific("py311-none-any"));
assert!(!tag_is_platform_specific("cp311.py3-none-any"));
// #1048: pip installs these on one interpreter (or Python 2) only.
assert!(tag_is_platform_specific("cp311-none-any"));
assert!(tag_is_platform_specific("pp310-none-any"));
assert!(tag_is_platform_specific("py2-none-any"));
assert!(tag_is_platform_specific("py-none-any"));
assert!(tag_is_platform_specific("py3x-none-any"));
assert!(tag_is_platform_specific(
"cp311-cp311-manylinux_2_17_x86_64"
));
Expand Down Expand Up @@ -6552,6 +6607,10 @@ wheels = [
wheel_platform_from_filename("x-1.0-cp312-cp312-manylinux_2_17_x86_64.whl"),
(true, "cp312-cp312-manylinux_2_17_x86_64".to_string())
);
assert_eq!(
wheel_platform_from_filename("six-1.16.0-cp311-none-any.whl"),
(true, "cp311-none-any".to_string())
);
// Short stems fall back closed and surface the stem verbatim.
assert_eq!(
wheel_platform_from_filename("six.whl"),
Expand Down
29 changes: 21 additions & 8 deletions crates/socket-patch-core/src/vendor/pypi_distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ pub(crate) fn verify_members(
Ok(())
}

/// Whether a wheel filename binds an ABI or platform, and its tag triple
/// for messages. Shared by vendored mode (`vendor_platform_locked`) and the
/// hosted redirect (`redirect_pypi_platform_wheel`), so both modes call the
/// same wheels portable.
/// Whether a wheel filename binds an interpreter, ABI or platform, and its
/// tag triple for messages. Shared by vendored mode
/// (`vendor_platform_locked`) and the hosted redirect
/// (`redirect_pypi_platform_wheel`), so both modes call the same wheels
/// portable.
pub(crate) fn wheel_platform_from_filename(wheel_name: &str) -> (bool, String) {
let stem = wheel_name.strip_suffix(".whl").unwrap_or(wheel_name);
let parts: Vec<&str> = stem.split('-').collect();
Expand All @@ -107,18 +108,30 @@ pub(crate) fn wheel_platform_from_filename(wheel_name: &str) -> (bool, String) {
}
}

/// Platform-specific iff the tag triple binds an ABI or platform — `cp311-
/// none-any` is merely version-bound, `*-cp311-*` / `*-manylinux*` lock the
/// artifact to this machine's platform.
/// Platform-specific unless every Python 3 interpreter on every platform
/// installs the wheel: the ABI must be `none`, the platform `any`, and the
/// python tag set must hold a generic Python 3 tag. pip accepts `py3` and
/// `pyXY` (major 3) on any later 3.x, but an interpreter tag (`cp311`,
/// `pp310`) only on that interpreter version, and `py2` never on Python 3
/// (#1048). `*-cp311-*` / `*-manylinux*` lock the artifact to this
/// machine's platform.
pub(crate) fn tag_is_platform_specific(tag: &str) -> bool {
let parts: Vec<&str> = tag.split('-').collect();
match parts.as_slice() {
[_py, abi, plat] => *abi != "none" || *plat != "any",
[py, abi, plat] => {
*abi != "none" || *plat != "any" || !py.split('.').any(is_generic_py3_tag)
}
// Malformed tags can't prove portability — claim platform-locked.
_ => true,
}
}

/// `py3` or `py3<minor>`: a python tag every later Python 3 accepts.
fn is_generic_py3_tag(tag: &str) -> bool {
tag.strip_prefix("py3")
.is_some_and(|minor| minor.bytes().all(|b| b.is_ascii_digit()))
}

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