From ed41a7e6786e3089c76cd7c94ad921cdb7100d36 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Sun, 13 Sep 2026 11:21:09 +0200 Subject: [PATCH] PTD-23.1.3: Project wheel platform policy Centralize portable Python wheel platform projection and enforce bounded manylinux architecture floors. Validate catalog records through the shared projection and cover accepted and rejected platform families. --- ...portable-python-wheel-platform-policy.yaml | 2 + internal/portabletool/record_validate.go | 107 +++++++++++++++--- internal/portabletool/record_validate_test.go | 85 ++++++++++++++ 3 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 .changes/unreleased/+portable-python-wheel-platform-policy.yaml diff --git a/.changes/unreleased/+portable-python-wheel-platform-policy.yaml b/.changes/unreleased/+portable-python-wheel-platform-policy.yaml new file mode 100644 index 00000000..b928698a --- /dev/null +++ b/.changes/unreleased/+portable-python-wheel-platform-policy.yaml @@ -0,0 +1,2 @@ +kind: Fixed +body: Reject unsupported manylinux policy versions and architecture floors in portable Python binding artifacts. diff --git a/internal/portabletool/record_validate.go b/internal/portabletool/record_validate.go index 16ddb64f..73247e0f 100644 --- a/internal/portabletool/record_validate.go +++ b/internal/portabletool/record_validate.go @@ -788,33 +788,108 @@ func portableToolCatalogWheelTagComponentV1(value string) bool { } func portableToolCatalogWheelPlatformCompatibleV1(tag, platform string) bool { + _, err := ProjectWheelPlatformForTargetV1(tag, platform) + return err == nil +} + +// WheelPlatformProjectionV1 is the single portable-tool wheel platform +// policy projection. It deliberately contains only the policy facts needed +// by record validation and provider runtime guards; pip remains the runtime +// compatibility authority. +type WheelPlatformProjectionV1 struct { + Kind string + Architecture string + MinimumGlibcMajor string + MinimumGlibcMinor string +} + +const ( + WheelPlatformAnyV1 = "any" + WheelPlatformLinuxV1 = "linux" + WheelPlatformManylinuxV1 = "manylinux" +) + +// ProjectWheelPlatformV1 validates and projects one canonical wheel platform +// tag. Unsupported policy aliases, numeric versions, architectures, and +// floors are rejected so all consumers share this one fail-closed table. +func ProjectWheelPlatformV1(tag string) (WheelPlatformProjectionV1, error) { if tag == "any" { - return true + return WheelPlatformProjectionV1{Kind: WheelPlatformAnyV1}, nil } architecture := "" - switch platform { - case "linux/amd64": + if strings.HasSuffix(tag, "_x86_64") { architecture = "x86_64" - case "linux/arm64": + } else if strings.HasSuffix(tag, "_aarch64") { architecture = "aarch64" + } else { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q has an unsupported architecture", tag) + } + policy := strings.TrimSuffix(tag, "_"+architecture) + projection := WheelPlatformProjectionV1{Architecture: architecture} + switch policy { + case "linux": + projection.Kind = WheelPlatformLinuxV1 + case "manylinux1": + if architecture != "x86_64" { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is unsupported on %s", tag, architecture) + } + projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "5" + case "manylinux2010": + if architecture != "x86_64" { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is unsupported on %s", tag, architecture) + } + projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "12" + case "manylinux2014": + projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "17" default: - return false + version, found := strings.CutPrefix(policy, "manylinux_") + if !found { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q uses an unsupported policy", tag) + } + parts := strings.Split(version, "_") + if len(parts) != 2 || validatePortableToolCatalogDecimalV1("manylinux major", parts[0], false) != nil || validatePortableToolCatalogDecimalV1("manylinux minor", parts[1], false) != nil { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q has an invalid manylinux version", tag) + } + if parts[0] != "2" { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q uses unsupported manylinux major %q", tag, parts[0]) + } + minimum := uint64(5) + if architecture == "aarch64" { + minimum = 17 + } + minor, _ := strconv.ParseUint(parts[1], 10, 63) + if minor < minimum { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is below the %s architecture floor", tag, architecture) + } + projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, parts[0], parts[1] } - if !strings.HasSuffix(tag, "_"+architecture) { - return false + return projection, nil +} + +// ProjectWheelPlatformForTargetV1 additionally proves that a projected +// architecture belongs to the selected Linux target. The any policy is +// architecture independent. +func ProjectWheelPlatformForTargetV1(tag, target string) (WheelPlatformProjectionV1, error) { + projection, err := ProjectWheelPlatformV1(tag) + if err != nil { + return WheelPlatformProjectionV1{}, err } - policy := strings.TrimSuffix(tag, "_"+architecture) - if policy == "linux" || policy == "manylinux2014" { - return true + architecture := "" + switch target { + case "linux/amd64": + architecture = "x86_64" + case "linux/arm64": + architecture = "aarch64" + default: + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel target %q is unsupported", target) } - if policy == "manylinux1" || policy == "manylinux2010" { - return architecture == "x86_64" + if projection.Kind == WheelPlatformAnyV1 { + return projection, nil } - if version, found := strings.CutPrefix(policy, "manylinux_"); found { - parts := strings.Split(version, "_") - return len(parts) == 2 && validatePortableToolCatalogDecimalV1("manylinux major", parts[0], false) == nil && validatePortableToolCatalogDecimalV1("manylinux minor", parts[1], false) == nil + if projection.Architecture != architecture { + return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q does not match target %q", tag, target) } - return false + return projection, nil } func portableToolCatalogStringsEqualV1(left, right []string) bool { diff --git a/internal/portabletool/record_validate_test.go b/internal/portabletool/record_validate_test.go index 56f19257..e091f2e3 100644 --- a/internal/portabletool/record_validate_test.go +++ b/internal/portabletool/record_validate_test.go @@ -132,6 +132,91 @@ func TestValidateRecordEnvelopeV1RejectsShortPayloadID(t *testing.T) { } } +func TestProjectWheelPlatformV1UsesOneBoundedManylinuxPolicy(t *testing.T) { + t.Parallel() + accepted := []struct { + tag, kind, architecture, major, minor string + }{ + {"any", portabletool.WheelPlatformAnyV1, "", "", ""}, + {"linux_x86_64", portabletool.WheelPlatformLinuxV1, "x86_64", "", ""}, + {"linux_aarch64", portabletool.WheelPlatformLinuxV1, "aarch64", "", ""}, + {"manylinux1_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "5"}, + {"manylinux2010_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "12"}, + {"manylinux2014_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "17"}, + {"manylinux2014_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "17"}, + {"manylinux_2_5_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "5"}, + {"manylinux_2_6_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "6"}, + {"manylinux_2_17_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "17"}, + {"manylinux_2_40_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "40"}, + } + for _, test := range accepted { + test := test + t.Run(test.tag, func(t *testing.T) { + t.Parallel() + projection, err := portabletool.ProjectWheelPlatformV1(test.tag) + if err != nil { + t.Fatal(err) + } + if projection.Kind != test.kind || projection.Architecture != test.architecture || projection.MinimumGlibcMajor != test.major || projection.MinimumGlibcMinor != test.minor { + t.Fatalf("projection = %#v", projection) + } + }) + } + for _, tag := range []string{ + "manylinux1_aarch64", "manylinux2010_aarch64", + "manylinux_2_4_x86_64", "manylinux_2_16_aarch64", + "manylinux_1_17_aarch64", "manylinux_3_17_aarch64", + "manylinux_02_17_aarch64", "manylinux_2_017_aarch64", + "manylinux_x_17_aarch64", "musllinux_1_2_x86_64", "linux_ppc64le", + } { + if _, err := portabletool.ProjectWheelPlatformV1(tag); err == nil { + t.Errorf("unsupported platform %q was accepted", tag) + } + } + if _, err := portabletool.ProjectWheelPlatformForTargetV1("linux_x86_64", "linux/arm64"); err == nil { + t.Fatal("x86_64 wheel platform was accepted for arm64 target") + } + direct, err := portabletool.ProjectWheelPlatformV1("manylinux_2_17_aarch64") + if err != nil { + t.Fatal(err) + } + targeted, err := portabletool.ProjectWheelPlatformForTargetV1("manylinux_2_17_aarch64", "linux/arm64") + if err != nil { + t.Fatalf("matching manylinux platform was rejected: %v", err) + } + if targeted != direct { + t.Fatalf("target projection %#v differs from direct projection %#v", targeted, direct) + } + if projection, err := portabletool.ProjectWheelPlatformForTargetV1("any", "linux/amd64"); err != nil || projection.Kind != portabletool.WheelPlatformAnyV1 { + t.Fatalf("any platform did not project for a supported target: %#v, %v", projection, err) + } + if _, err := portabletool.ProjectWheelPlatformForTargetV1("any", "darwin/amd64"); err == nil { + t.Fatal("any wheel platform was accepted for an unsupported target") + } +} + +func TestValidateRecordEnvelopeV1UsesWheelPlatformProjection(t *testing.T) { + t.Parallel() + value := readDefinitionObjectV1(t, "playwright/releases/1.61.0/bindings/python/linux-amd64.json") + value["filename"] = "playwright-1.61.0-py3-none-manylinux_2_5_x86_64.whl" + value["tags"] = []any{"py3-none-manylinux_2_5_x86_64"} + if err := portabletool.ValidateRecordEnvelopeV1(canonical.Envelope{ + Schema: portabletool.BindingArtifactSchemaV1, + Value: value, + }); err != nil { + t.Fatalf("supported projected platform was rejected: %v", err) + } + + value["filename"] = "playwright-1.61.0-py3-none-manylinux_3_17_x86_64.whl" + value["tags"] = []any{"py3-none-manylinux_3_17_x86_64"} + if err := portabletool.ValidateRecordEnvelopeV1(canonical.Envelope{ + Schema: portabletool.BindingArtifactSchemaV1, + Value: value, + }); err == nil { + t.Fatal("record validation accepted an unsupported manylinux policy major") + } +} + func TestToolcatalogCompatibilityAliasPreservesCanonicalIdentity(t *testing.T) { t.Parallel() shared := &portabletool.BindingContractV1{