Skip to content
Draft
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
136 changes: 92 additions & 44 deletions bin/update_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import configparser
import dataclasses
from functools import cache
from pathlib import Path

import requests
Expand All @@ -33,7 +34,7 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None
super().__init__(manylinux_version, platforms, image_name, tag, True)


images = [
IMAGES = [
# manylinux2014 images
PyPAImage(
"manylinux2014",
Expand Down Expand Up @@ -88,49 +89,71 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None
),
]

config = configparser.ConfigParser()

for image in images:
# get the tag name whose digest matches 'latest'
if image.tag is not None:
# image has been pinned, do not update
tag_name = image.tag
elif image.image_name.startswith("quay.io/"):
_, _, repository_name = image.image_name.partition("/")
response = requests.get(
f"https://quay.io/api/v1/repository/{repository_name}?includeTags=true"
)
response.raise_for_status()
repo_info = response.json()
tags_dict = repo_info["tags"]

latest_tag = tags_dict.pop("latest")
@cache
def quay_lookup(image_name: str, tag_name: str) -> tuple[str, str]:
_, _, repository_name = image_name.partition("/")
if tag_name == "latest":
url = f"https://quay.io/api/v1/repository/{repository_name}?includeTags=true"
else:
url = f"https://quay.io/api/v1/repository/{repository_name}/tag?specificTag={tag_name}"
response = requests.get(url)
response.raise_for_status()
info = response.json()
if tag_name == "latest":
tags_dict = info["tags"]
tag_info = tags_dict.pop(tag_name)
# find the tag whose manifest matches 'latest'
tag_name = next(
name
tag_name, digest = next(
(name, info["manifest_digest"])
for (name, info) in tags_dict.items()
if info["manifest_digest"] == latest_tag["manifest_digest"]
)
elif image.image_name.startswith("ghcr.io/"):
repository = image.image_name[8:]
response = requests.get(
"https://ghcr.io/token", params={"scope": f"repository:{repository}:pull"}
if info["manifest_digest"] == tag_info["manifest_digest"]
)
response.raise_for_status()
token = response.json()["token"]
else:
tags_list = info["tags"]
tag_info = next(tag for tag in tags_list if tag["name"] == tag_name)
digest = tag_info["manifest_digest"]

return tag_name, digest


@cache
def ghcr_lookup(image_name: str, tag_name: str) -> tuple[str, str]:
repository = image_name[8:]
response = requests.get(
"https://ghcr.io/token", params={"scope": f"repository:{repository}:pull"}
)
response.raise_for_status()
token = response.json()["token"]
if tag_name == "latest":
response = requests.get(
f"https://ghcr.io/v2/{repository}/tags/list",
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
ghcr_tags = [(Version(tag), tag) for tag in response.json()["tags"] if tag != "latest"]
info = response.json()
ghcr_tags = [(Version(tag), tag) for tag in info["tags"] if tag != "latest"]
ghcr_tags.sort(reverse=True)
tag_name = ghcr_tags[0][1]
else:
response = requests.get(f"https://hub.docker.com/v2/repositories/{image.image_name}/tags")
response.raise_for_status()
tags = response.json()["results"]

response = requests.head(
f"https://ghcr.io/v2/{repository}/manifests/{tag_name}",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json",
},
)
response.raise_for_status()
digest = response.headers["Docker-Content-Digest"]
return tag_name, digest


@cache
def dockerhub_lookup(image_name: str, tag_name: str) -> tuple[str, str]:
response = requests.get(f"https://hub.docker.com/v2/repositories/{image_name}/tags")
response.raise_for_status()
tags = response.json()["results"]
if tag_name == "latest":
latest_tag = next(tag for tag in tags if tag["name"] == "latest")
# i don't know what it would mean to have multiple images per tag
assert len(latest_tag["images"]) == 1
Expand All @@ -139,15 +162,40 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None
pinned_tag = next(
tag for tag in tags if tag != latest_tag and tag["images"][0]["digest"] == digest
)
tag_name = pinned_tag["name"]

for platform in image.platforms:
if not config.has_section(platform):
config[platform] = {}
suffix = ""
if image.use_platform_suffix:
suffix = f"_{platform.removeprefix('pypy_')}"
config[platform][image.manylinux_version] = f"{image.image_name}{suffix}:{tag_name}"

with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
config.write(f)
else:
pinned_tag = next(tag for tag in tags if tag["name"] == tag_name)
digest = pinned_tag["images"][0]["digest"]
tag_name = pinned_tag["name"]
return tag_name, digest


def main() -> None:
config = configparser.ConfigParser()
for image in IMAGES:
# get the tag name whose digest matches 'latest'
# if image has been pinned, do not update
search_tag = image.tag or "latest"
if image.image_name.startswith("quay.io/"):
lookup = quay_lookup
elif image.image_name.startswith("ghcr.io/"):
lookup = ghcr_lookup
else:
lookup = dockerhub_lookup

tag_name, digest = lookup(image.image_name, search_tag)
for platform in image.platforms:
if not config.has_section(platform):
config[platform] = {}
image_name = image.image_name
if image.use_platform_suffix:
image_name = f"{image_name}_{platform.removeprefix('pypy_')}"
_, digest = lookup(image_name, tag_name)
assert digest.startswith("sha256:")
config[platform][image.manylinux_version] = f"{image_name}@{digest} # {tag_name}"

with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
config.write(f)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,6 @@ def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]:
'pypy_x86_64': {'manylinux2010': '...' }
... }
"""
all_pinned_images = configparser.ConfigParser()
all_pinned_images = configparser.ConfigParser(inline_comment_prefixes="#")
all_pinned_images.read(resources.PINNED_DOCKER_IMAGES)
return all_pinned_images
68 changes: 34 additions & 34 deletions cibuildwheel/resources/pinned_docker_images.cfg
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
[x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_x86_64@sha256:87b129514bde0520172d4e319fb579b36e0bf8aa6698d365ed4a64736585b27b # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64@sha256:893d0c9d73a8262503fa32f40160aae0299c10cecfc04396c9802a712cab1831 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64@sha256:e05e1c4b281f10dc4c3df2b6f546392a0dd4c6383d620c3f8a6c33e19069d056 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64@sha256:57c53870e12363dc7f707b4f62df2fe8309e773ecfc16891952d62e0c3488a92 # 2026.06.14-5

[i686]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_i686@sha256:9fcd86d7fac5d5bc9bfffa825aa0c14b5ac94edb40e5fb147ce5e3ca4701812b # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686@sha256:0c7ed2c1d5cd8c060093ab7f16cb75e2fc0d656ae2ccba9ee4061ac0aafddfd8 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686@sha256:69988569da4501fdcbde10e5cd0897d7ebbc8694f94c52f5c59ce9ee7397caf3 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686@sha256:f8bbd9fecd21dca43c2347711750afc83c0ff63c6fb0e9fea1df502a1ca3b4bb # 2026.06.14-5

[aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_aarch64@sha256:be5870aed7d375afffc7c6c32564fb813b0becd3bd3ee9a81e1252480e16c628 # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64@sha256:f926f192db8589bdfbc6d4af4820d8cb76661f1349ab136259b33b94f4606f05 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64@sha256:cfdf5d4ecf403fdaebca69f863552726d4ebd0154d8391ecae8fc27a6fe33ad5 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64@sha256:a0e24d12e51dd0e0fe55385692e9917ce438c3b3c56e0f3e3641eeb0cff2e4f6 # 2026.06.14-5

[ppc64le]
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le@sha256:ac39f949d8e915e5d7a0c63f9245e0a847d6a23b5de2448013d649d81f61f889 # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le@sha256:02d9d85d0509e73ee33e48277ca0faf8be31a15f4c93aa6deb0b428a493e71c4 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le@sha256:2da444dd7ebf5e0ff6479f9f229d1b6cd88c0a670867554aa472dd1f3e13d574 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le@sha256:c90bfd0e0016c6c169098303e91ea7410debf3429974b2b8306f751cc380ddff # 2026.06.14-5

[s390x]
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_s390x@sha256:c7906ff00cd2cf5535d8ee556ee5b0bb20c404b0ee24e427ab7f68cba4c71b51 # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x@sha256:e7aadd0bc6f29f8914cd7ba8f8accb0d769455c527df3e2499cb4bea07ff7e5a # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x@sha256:d16cc803324f99103a8407ee1635fc58d1cd04572f240bd86a9e9c98c7494cb4 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x@sha256:d2b8a6260c3c1846bf018e085760ead661b5291e09287dd543baea69809b11ea # 2026.06.14-5

[pypy_x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_x86_64@sha256:87b129514bde0520172d4e319fb579b36e0bf8aa6698d365ed4a64736585b27b # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64@sha256:893d0c9d73a8262503fa32f40160aae0299c10cecfc04396c9802a712cab1831 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64@sha256:e05e1c4b281f10dc4c3df2b6f546392a0dd4c6383d620c3f8a6c33e19069d056 # 2026.06.14-5

[pypy_i686]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_i686@sha256:9fcd86d7fac5d5bc9bfffa825aa0c14b5ac94edb40e5fb147ce5e3ca4701812b # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686@sha256:0c7ed2c1d5cd8c060093ab7f16cb75e2fc0d656ae2ccba9ee4061ac0aafddfd8 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686@sha256:69988569da4501fdcbde10e5cd0897d7ebbc8694f94c52f5c59ce9ee7397caf3 # 2026.06.14-5

[pypy_aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.06.04-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.06.04-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.06.04-1
manylinux2014 = quay.io/pypa/manylinux2014_aarch64@sha256:be5870aed7d375afffc7c6c32564fb813b0becd3bd3ee9a81e1252480e16c628 # 2026.06.14-5
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64@sha256:f926f192db8589bdfbc6d4af4820d8cb76661f1349ab136259b33b94f4606f05 # 2026.06.14-5
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64@sha256:cfdf5d4ecf403fdaebca69f863552726d4ebd0154d8391ecae8fc27a6fe33ad5 # 2026.06.14-5

[armv7l]
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2026.06.04-1
manylinux_2_35 = quay.io/pypa/manylinux_2_35_armv7l:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2026.06.04-1
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l@sha256:05ce975c0e6cff6accd57ab8fbd5742c3ad98185ecdb03c55e09915ba84c9cce # 2026.06.14-5
manylinux_2_35 = quay.io/pypa/manylinux_2_35_armv7l@sha256:c8cd90ae35ad7c00e823a642f0544842dfec435a1fa08f1f65cb7545be673105 # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l@sha256:523aea9cca934ee68e86d8705469de21bea5e24497875ee8b1fe0ccf2cf5d9ed # 2026.06.14-5

[riscv64]
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64:2026.06.04-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64:2026.06.04-1
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64@sha256:d10fe31ae6b9fb0530bcd8688b3fc3c5c03dfd009817608f57fc86f45f5c3ade # 2026.06.14-5
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64@sha256:493ed9ee713dca17e2217b07aaaf368b6d397edafb070e9a902b74f158829bbf # 2026.06.14-5

Loading
Loading