feat(pypi): mkPyPiWheelPublishMulti — publish all versions per tag (#197)

Single-version mkPyPiWheelPublish made consumers ship only the default
version per tag. Add a multi-version builder that loops over a fixed
{version,wheel} list (version parsed from the wheel filename, idempotent
409-skip), plus shared parity_pypi_post/parity_wheel_version helpers.
This commit is contained in:
Oleks
2026-06-02 05:23:42 +03:00
parent 9107923c5a
commit cda7a190c0
4 changed files with 152 additions and 0 deletions
+111
View File
@@ -153,6 +153,116 @@ let
"push-staged" = pushStaged;
};
# =========================================================================
# PyPI wheels, MULTI-version (cluster #197). Same archetype as the single
# builder, but publishes a fixed list of {version, wheel} per tag (the old
# per-repo CI behaviour several *-s390x repos rely on) instead of just the
# default version. The real version of each wheel is read from its filename
# (PEP 427), so stage/publish/push-staged need no side-channel map and a
# re-run is idempotent (409-skip per version). No dev-tag guard: the versions
# are an explicit fixed list of real releases, gated by the dry-run default
# (--publish / PUBLISH=1 is the deliberate intent), matching the old "publish
# all on a v* tag" pipeline.
# Args: { pname, versions = [ { version; wheel; } ... ], arch ? "s390x", registry* }
# =========================================================================
mkPyPiWheelPublishMulti =
{
pname,
versions,
arch ? "s390x",
registryHost ? "git.oleks.space",
registryOwner ? "oleks",
passEntry ? "infra/gitea/personal_access_token_packages_rw",
}:
let
head = preamble { inherit registryHost registryOwner passEntry; };
nVersions = builtins.length versions;
# One copy block per version's wheel (drv dir we glob, or explicit .whl).
stageCopies = lib.concatMapStringsSep "\n" (e: ''
src=${lib.escapeShellArg "${e.wheel}"}
if [ -d "$src" ]; then
# shellcheck disable=SC2044
for w in $(find "$src" -name '*.whl'); do cp "$w" "$d/"; done
else
cp "$src" "$d/"
fi
'') versions;
stageText = ''
${head}
echo " staging ${pname} (${toString nVersions} versions, ${arch}, no registry contact)"
d="$(parity_stage_reset ${lib.escapeShellArg arch})"
${stageCopies}
n="$(find "$d" -name '*.whl' | wc -l)"
if [ "$n" -eq 0 ]; then echo "BLOCKER(no-wheel): no *.whl staged under $d" >&2; exit 1; fi
printf 'pypi-multi %s %s\n' ${lib.escapeShellArg pname} "$n" >"$d/.parity-meta"
echo " staged $n wheel(s) under $d"
'';
stage = pkgs.writeShellApplication {
name = "stage-${arch}";
runtimeInputs = baseInputs ++ [ pkgs.nix ];
text = stageText;
};
# Shared publish loop body, reused by publish and push-staged: POST every
# staged wheel, version parsed from its filename; aggregate failures.
uploadLoop = ''
tok="$(parity_resolve_token)"
parity_registry_preflight
rc=0
for whl in "$d"/*.whl; do
ver="$(parity_wheel_version "$whl")"
parity_pypi_post ${lib.escapeShellArg pname} "$ver" "$whl" "$tok" || rc=1
done
exit "$rc"
'';
dryList = ''
echo "[dry-run] would POST these wheels -> https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/pypi (name=${pname}):"
for whl in "$d"/*.whl; do echo " - $(basename "$whl") (version $(parity_wheel_version "$whl"))"; done
echo "[dry-run] re-run with --publish / PUBLISH=1 to upload."
'';
publish = pkgs.writeShellApplication {
name = "publish-${arch}";
runtimeInputs = baseInputs ++ [ pkgs.nix ];
text = ''
${head}
parity_parse_args "Build + publish all ${toString nVersions} ${pname} ${arch} wheels to the Gitea PyPI registry" "$@"
${lib.getExe stage}
d="$(parity_stage_path ${lib.escapeShellArg arch})"
if [ "$PARITY_PUBLISH" != "1" ]; then
${dryList}
exit 0
fi
${uploadLoop}
'';
};
pushStaged = pkgs.writeShellApplication {
name = "push-staged";
runtimeInputs = baseInputs ++ [ pkgs.nix ];
text = ''
${head}
parity_parse_args "Replay the staged ${pname} ${arch} wheels from .parity-stage" "$@"
d="$(parity_stage_path ${lib.escapeShellArg arch})"
if ! find "$d" -name '*.whl' -print -quit | grep -q .; then
echo "BLOCKER(no-stage): no staged wheels under $d run 'nix run .#stage-${arch}' first." >&2
exit 1
fi
if [ "$PARITY_PUBLISH" != "1" ]; then
${dryList}
exit 0
fi
${uploadLoop}
'';
};
in
{
"stage-${arch}" =
mkApp stage "Stage all ${nVersions} ${pname} ${arch} wheels into .parity-stage (no registry contact).";
"publish-${arch}" =
mkApp publish "Stage + publish all ${pname} ${arch} wheels (dry-run by default; --publish to push).";
"publish" = mkApp publish "Publish all ${pname} ${arch} wheels (${arch} only for this PyPI repo).";
"push-staged" =
mkApp pushStaged "Replay the staged ${pname} ${arch} wheels from .parity-stage (dry-run by default; --publish to push).";
};
# =========================================================================
# s390x npm native addon (single-arch). Stage a ready-to-publish package dir
# (the .node + a generated package.json) then `npm publish`.
@@ -745,6 +855,7 @@ in
{
inherit
mkPyPiWheelPublish
mkPyPiWheelPublishMulti
mkS390xNpmPublish
mkGenericBinaryPublish
mkNix2ContainerPublish