|
| 1 | +"""Helper functions to parse python-build-standalone manifests.""" |
| 2 | + |
| 3 | +def parse_filename(filename): |
| 4 | + """Parses a python-build-standalone filename (or URL) into its components. |
| 5 | +
|
| 6 | + See https://gregoryszorc.com/docs/python-build-standalone/main/running.html |
| 7 | +
|
| 8 | + Example: cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst |
| 9 | +
|
| 10 | + Args: |
| 11 | + filename: The filename or URL of the python-build-standalone release asset. |
| 12 | +
|
| 13 | + Returns: |
| 14 | + A dictionary of parsed components if parsed successfully, else None. |
| 15 | + """ |
| 16 | + basename = filename.rpartition("/")[-1] |
| 17 | + if basename.endswith(".tar.zst"): |
| 18 | + name = basename.removesuffix(".tar.zst") |
| 19 | + elif basename.endswith(".tar.gz"): |
| 20 | + name = basename.removesuffix(".tar.gz") |
| 21 | + else: |
| 22 | + return None |
| 23 | + |
| 24 | + if not name.startswith("cpython-"): |
| 25 | + return None |
| 26 | + name = name.removeprefix("cpython-") |
| 27 | + |
| 28 | + left, plus, tail = name.partition("+") |
| 29 | + if plus: |
| 30 | + python_version = left |
| 31 | + build_version, sep, rest = tail.partition("-") |
| 32 | + if not sep: |
| 33 | + return None |
| 34 | + else: |
| 35 | + python_version, sep, rest = left.partition("-") |
| 36 | + if not sep: |
| 37 | + return None |
| 38 | + build_version = "" |
| 39 | + |
| 40 | + arch, sep, rest = rest.partition("-") |
| 41 | + if not sep: |
| 42 | + return None |
| 43 | + |
| 44 | + microarch = "" |
| 45 | + arch_base, sep_v, microarch_num = arch.partition("_v") |
| 46 | + if sep_v: |
| 47 | + arch = arch_base |
| 48 | + microarch = "v" + microarch_num |
| 49 | + |
| 50 | + vendor, sep, rest = rest.partition("-") |
| 51 | + if not sep: |
| 52 | + return None |
| 53 | + |
| 54 | + os, sep, rest = rest.partition("-") |
| 55 | + if not sep: |
| 56 | + return None |
| 57 | + |
| 58 | + libc = "" |
| 59 | + next_part, _, remaining = rest.partition("-") |
| 60 | + if os == "linux" and next_part in ["gnu", "musl"]: |
| 61 | + libc = next_part |
| 62 | + flavor = remaining |
| 63 | + elif os == "windows" and next_part == "msvc": |
| 64 | + libc = next_part |
| 65 | + flavor = remaining |
| 66 | + else: |
| 67 | + libc = "" |
| 68 | + flavor = rest |
| 69 | + |
| 70 | + freethreaded = False |
| 71 | + if flavor.startswith("freethreaded+"): |
| 72 | + freethreaded = True |
| 73 | + flavor = flavor.removeprefix("freethreaded+") |
| 74 | + elif flavor.startswith("freethreaded-"): |
| 75 | + freethreaded = True |
| 76 | + flavor = flavor.removeprefix("freethreaded-") |
| 77 | + elif flavor == "freethreaded": |
| 78 | + freethreaded = True |
| 79 | + flavor = "" |
| 80 | + |
| 81 | + archive_flavor = "" |
| 82 | + if flavor.endswith("-full"): |
| 83 | + archive_flavor = "full" |
| 84 | + flavor = flavor.removesuffix("-full") |
| 85 | + elif flavor == "full": |
| 86 | + archive_flavor = "full" |
| 87 | + flavor = "" |
| 88 | + elif flavor.endswith("-install_only_stripped"): |
| 89 | + archive_flavor = "install_only_stripped" |
| 90 | + flavor = flavor.removesuffix("-install_only_stripped") |
| 91 | + elif flavor == "install_only_stripped": |
| 92 | + archive_flavor = "install_only_stripped" |
| 93 | + flavor = "" |
| 94 | + elif flavor.endswith("-install_only"): |
| 95 | + archive_flavor = "install_only" |
| 96 | + flavor = flavor.removesuffix("-install_only") |
| 97 | + elif flavor == "install_only": |
| 98 | + archive_flavor = "install_only" |
| 99 | + flavor = "" |
| 100 | + |
| 101 | + return { |
| 102 | + "arch": arch, |
| 103 | + "archive_flavor": archive_flavor, |
| 104 | + "build_version": build_version, |
| 105 | + "flavor": flavor, |
| 106 | + "freethreaded": freethreaded, |
| 107 | + "libc": libc, |
| 108 | + "location": filename, |
| 109 | + "microarch": microarch, |
| 110 | + "os": os, |
| 111 | + "python_version": python_version, |
| 112 | + "vendor": vendor, |
| 113 | + } |
| 114 | + |
| 115 | +def parse_sha_manifest(content): |
| 116 | + """Parses the SHA256SUMS file content into a list of structs. |
| 117 | +
|
| 118 | + Args: |
| 119 | + content: The raw content of the manifest file. |
| 120 | +
|
| 121 | + Returns: |
| 122 | + A list of structs capturing the parsed components of each valid entry. |
| 123 | + Each struct contains the following fields: |
| 124 | + - arch: CPU architecture (e.g., "x86_64"). |
| 125 | + - archive_flavor: Release asset archive type (e.g., "full", "install_only"). |
| 126 | + - build_version: Standalone release date (e.g., "20260414"). |
| 127 | + - location: Full package filename or URL (e.g., "cpython-3.11.15..." or "https://..."). |
| 128 | + - flavor: Build configuration flavor (e.g., "install_only"). |
| 129 | + - freethreaded: Whether the build is free-threaded (boolean). |
| 130 | + - libc: C library type (e.g., "gnu", "musl", "msvc", or ""). |
| 131 | + - microarch: Microarchitecture level (e.g., "v2", "v3", or ""). |
| 132 | + - os: Operating system (e.g., "linux", "darwin", "windows"). |
| 133 | + - python_version: Python semver version (e.g., "3.11.15"). |
| 134 | + - sha256: SHA256 integrity hash of the release asset. |
| 135 | + - vendor: Platform vendor (e.g., "unknown", "apple"). |
| 136 | + """ |
| 137 | + results = [] |
| 138 | + for line in content.split("\n"): |
| 139 | + line = line.strip() |
| 140 | + if not line: |
| 141 | + continue |
| 142 | + parts = [p for p in line.split(" ") if p] |
| 143 | + if len(parts) != 2: |
| 144 | + continue |
| 145 | + sha256, filename = parts |
| 146 | + |
| 147 | + parsed = parse_filename(filename) |
| 148 | + if parsed: |
| 149 | + results.append(struct( |
| 150 | + sha256 = sha256, |
| 151 | + **parsed |
| 152 | + )) |
| 153 | + return results |
0 commit comments