From 56e00b6e74858a24fa85d35d96a9ac45a99fee96 Mon Sep 17 00:00:00 2001 From: James Butler Date: Wed, 3 Sep 2025 23:44:16 -0400 Subject: [PATCH 1/2] Utilities: Add automation to add python patch releases --- Utilities/check_python_patches.py | 212 ++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 Utilities/check_python_patches.py diff --git a/Utilities/check_python_patches.py b/Utilities/check_python_patches.py new file mode 100644 index 00000000..8da4bb90 --- /dev/null +++ b/Utilities/check_python_patches.py @@ -0,0 +1,212 @@ +import os +import re +import requests +from packaging.version import Version, parse + +PATCHES_DIR = os.path.join(os.path.dirname(__file__), '..', 'patches') +PYTHON_RELEASES_URL = 'https://www.python.org/api/v2/downloads/release/' + +# Helper: get supported versions from CMakeLists.txt md5 variables +def get_supported_versions(): + cmake_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'CMakeLists.txt')) + versions = set() + with open(cmake_path, 'r', encoding='utf-8') as f: + for line in f: + m = re.match(r'set\(_download_(\d+)\.(\d+)\.(\d+)_md5', line) + if m: + minor = f"{m.group(1)}.{m.group(2)}" + versions.add(minor) + def version_key(v): + major, minor = v.split('.') + return (int(major), int(minor)) + return sorted(versions, key=version_key) + +# Helper: get latest patch for a given minor version +def get_latest_patch_version(minor_version): + # Query Python.org for all releases + resp = requests.get(PYTHON_RELEASES_URL) + resp.raise_for_status() + data = resp.json() + candidates = [] + # The API returns a list of releases, not a dict + for rel in data: + # Each rel should be a dict with 'name' key + v = rel.get('name', '').lstrip('Python ').strip() + if v.startswith(minor_version + '.'): + try: + candidates.append(parse(v)) + except Exception: + continue + if not candidates: + return None + return str(max(candidates)) + +# Helper: get all patch folders for a minor version +def get_patch_folders(minor_version): + return [d for d in os.listdir(PATCHES_DIR) if d.startswith(minor_version + '.')] + +def main(): + + supported = get_supported_versions() + # --- Update CI.yml and config.yml to use latest patch releases --- + # Map: minor_version -> latest_patch_version + latest_patches = {} + for minor in supported: + latest = get_latest_patch_version(minor) + if latest: + latest_patches[minor] = latest + + # Update .github/workflows/CI.yml + ci_yml_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.github', 'workflows', 'CI.yml')) + if os.path.exists(ci_yml_path): + with open(ci_yml_path, 'r', encoding='utf-8') as f: + ci_lines = f.readlines() + # Update python-version matrix + for idx, line in enumerate(ci_lines): + m = re.match(r'(\s*python-version:\s*)\[(.*?)\]', line) + if m: + orig_versions = re.findall(r'(\d+\.\d+\.\d+)', m.group(2)) + new_versions = [] + for v in orig_versions: + minor = '.'.join(v.split('.')[:2]) + if minor in latest_patches: + new_versions.append(latest_patches[minor]) + else: + new_versions.append(v) + new_versions_str = ', '.join(new_versions) + ci_lines[idx] = f'{m.group(1)}[{new_versions_str}]\n' + break + # Update job names containing python version + version_pat = re.compile(r'(python-)(\d+\.\d+\.\d+)([-\w]*)') + for idx, line in enumerate(ci_lines): + def repl(m): + minor = '.'.join(m.group(2).split('.')[:2]) + new_version = latest_patches.get(minor, m.group(2)) + return f'{m.group(1)}{new_version}{m.group(3)}' + new_line = version_pat.sub(repl, line) + if new_line != line: + ci_lines[idx] = new_line + with open(ci_yml_path, 'w', encoding='utf-8') as f: + f.writelines(ci_lines) + print(f"Updated python-version matrix and job names in {ci_yml_path}") + + # Update .circleci/config.yml + circleci_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.circleci', 'config.yml')) + if os.path.exists(circleci_path): + with open(circleci_path, 'r', encoding='utf-8') as f: + circle_lines = f.readlines() + # Replace all python_version: lines in workflows with latest patch + for idx, line in enumerate(circle_lines): + m = re.match(r'(\s*python_version:\s*)(\d+\.\d+\.\d+)', line) + if m: + minor = '.'.join(m.group(2).split('.')[:2]) + if minor in latest_patches: + new_line = f'{m.group(1)}{latest_patches[minor]}\n' + if circle_lines[idx] != new_line: + circle_lines[idx] = new_line + # Update job names containing python version + version_pat = re.compile(r'(python-)(\d+\.\d+\.\d+)([-\w]*)') + for idx, line in enumerate(circle_lines): + def repl(m): + minor = '.'.join(m.group(2).split('.')[:2]) + new_version = latest_patches.get(minor, m.group(2)) + return f'{m.group(1)}{new_version}{m.group(3)}' + new_line = version_pat.sub(repl, line) + if new_line != line: + circle_lines[idx] = new_line + with open(circleci_path, 'w', encoding='utf-8') as f: + f.writelines(circle_lines) + print(f"Updated python_version fields and job names in {circleci_path}") + updated = False + supported = get_supported_versions() + cmake_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'CMakeLists.txt')) + with open(cmake_path, 'r', encoding='utf-8') as f: + cmake_lines = f.readlines() + + for minor in supported: + latest = get_latest_patch_version(minor) + if not latest: + continue + # Check if CMakeLists.txt already has this version + cmake_var = f'_download_{latest}_md5' + if any(cmake_var in line for line in cmake_lines): + continue + + # Get MD5 hash from the Python release page + release_url = f'https://www.python.org/downloads/release/python-{latest.replace(".", "")}/' + try: + resp = requests.get(release_url) + resp.raise_for_status() + html = resp.text + # Try to find the MD5 for the .tgz file in the files table + # First, try a robust regex for the .tgz row and MD5 + pattern = rf']*>.*?Python-{latest}\.tgz.*?]*>.*?([a-fA-F0-9]{{32}}).*?.*?' + match = re.search(pattern, html, re.DOTALL) + if not match: + # Fallback: try to find the MD5 in a tag near the .tgz link + pattern2 = rf'Python-{latest}\.tgz.*?([a-fA-F0-9]{{32}})' + match2 = re.search(pattern2, html, re.DOTALL) + if match2: + md5 = match2.group(1) + else: + print(f"MD5 not found on release page for {latest}") + continue + else: + md5 = match.group(1) + except Exception as e: + print(f"Failed to fetch MD5 for {latest} from release page: {e}") + continue + + # Find where to insert: after the last md5 line for the same minor version + all_md5_lines = [] + md5_pattern = re.compile(r'set\(_download_(\d+)\.(\d+)\.(\d+)_md5') + for idx, line in enumerate(cmake_lines): + m = md5_pattern.match(line.strip()) + if m: + all_md5_lines.append((int(m.group(1)), int(m.group(2)), int(m.group(3)), idx)) + major, minor_num = map(int, minor.split('.')) + this_minor_lines = [t for t in all_md5_lines if (t[0], t[1]) == (major, minor_num)] + if this_minor_lines: + # Insert after the highest patch for this minor version + insert_idx = max(this_minor_lines, key=lambda t: t[2])[3] + 1 + else: + # If no md5 for this minor, insert after the last md5 for any earlier version + earlier_lines = [t for t in all_md5_lines if (t[0], t[1]) < (major, minor_num)] + if earlier_lines: + insert_idx = max(earlier_lines, key=lambda t: (t[0], t[1], t[2]))[3] + 1 + else: + # Otherwise, insert at the top after all comments + insert_idx = 0 + for idx, line in enumerate(cmake_lines): + if not line.strip().startswith('#'): + insert_idx = idx + break + new_line = f'set(_download_{latest}_md5 "{md5}")\n' + cmake_lines.insert(insert_idx, new_line) + print(f"Added {new_line.strip()} to CMakeLists.txt after line {insert_idx}") + updated = True + + # --- Update default PYTHON_VERSION to latest with md5 variable --- + md5_versions = [] + for line in cmake_lines: + m = re.match(r'set\(_download_(\d+\.\d+\.\d+)_md5', line) + if m: + md5_versions.append(m.group(1)) + if md5_versions: + latest_md5_version = str(max(md5_versions, key=lambda v: tuple(map(int, v.split('.'))))) + # Update set(PYTHON_VERSION ...) line + for idx, line in enumerate(cmake_lines): + if line.strip().startswith('set(PYTHON_VERSION '): + old = cmake_lines[idx] + cmake_lines[idx] = f'set(PYTHON_VERSION "{latest_md5_version}" CACHE STRING "The version of Python to build.")\n' + print(f"Updated default PYTHON_VERSION in CMakeLists.txt: {old.strip()} -> {cmake_lines[idx].strip()}") + updated = True + break + if updated: + with open(cmake_path, 'w', encoding='utf-8') as f: + f.writelines(cmake_lines) + else: + print("No new patch releases found.") + +if __name__ == '__main__': + main() From 55339b1fbb7ef4ec7a024793a0719972c76f305d Mon Sep 17 00:00:00 2001 From: James Butler Date: Wed, 3 Sep 2025 23:54:13 -0400 Subject: [PATCH 2/2] feat: Add support for python 3.9.23, 3.10.18, 3.11.13, 3.12.11 --- .circleci/config.yml | 48 ++++++++++++++++++++-------------------- .github/workflows/CI.yml | 2 +- CMakeLists.txt | 6 ++++- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 00fae535..6adc748e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,46 +88,46 @@ workflows: jobs: # 3.12.10 - build-test-python: - name: python-3.12.10-x64 - python_version: 3.12.10 + name: python-3.12.11-x64 + python_version: 3.12.11 python_arch: x64 - build-test-python: - name: python-3.12.10-x86 - python_version: 3.12.10 + name: python-3.12.11-x86 + python_version: 3.12.11 python_arch: x86 # 3.11.12 - build-test-python: - name: python-3.11.12-x64 - python_version: 3.11.12 + name: python-3.11.13-x64 + python_version: 3.11.13 python_arch: x64 - build-test-python: - name: python-3.11.12-x86 - python_version: 3.11.12 + name: python-3.11.13-x86 + python_version: 3.11.13 python_arch: x86 # 3.10.17 - build-test-python: - name: python-3.10.17-x64 - python_version: 3.10.17 + name: python-3.10.18-x64 + python_version: 3.10.18 python_arch: x64 - build-test-python: - name: python-3.10.17-x86 - python_version: 3.10.17 + name: python-3.10.18-x86 + python_version: 3.10.18 python_arch: x86 # 3.9.22 - build-test-python: - name: python-3.9.22-x64 - python_version: 3.9.22 + name: python-3.9.23-x64 + python_version: 3.9.23 python_arch: x64 - build-test-python: - name: python-3.9.22-x86 - python_version: 3.9.22 + name: python-3.9.23-x86 + python_version: 3.9.23 python_arch: x86 # 3.8.20 @@ -168,29 +168,29 @@ workflows: jobs: # 3.12.10 - build-test-python-win: - name: python-3.12.10-win-x64 - python_version: 3.12.10 + name: python-3.12.11-win-x64 + python_version: 3.12.11 python_arch: x64 generator: "Visual Studio 16 2019" # 3.11.12 - build-test-python-win: - name: python-3.11.12-win-x64 - python_version: 3.11.12 + name: python-3.11.13-win-x64 + python_version: 3.11.13 python_arch: x64 generator: "Visual Studio 16 2019" # 3.10.17 - build-test-python-win: - name: python-3.10.17-win-x64 - python_version: 3.10.17 + name: python-3.10.18-win-x64 + python_version: 3.10.18 python_arch: x64 generator: "Visual Studio 16 2019" # 3.9.22 - build-test-python-win: - name: python-3.9.22-win-x64 - python_version: 3.9.22 + name: python-3.9.23-win-x64 + python_version: 3.9.23 python_arch: x64 generator: "Visual Studio 16 2019" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 07cd1db0..c5f1a350 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: runs-on: [macos-latest] - python-version: [3.7.17, 3.8.20, 3.9.22, 3.10.17, 3.11.12, 3.12.10] + python-version: [3.7.17, 3.8.20, 3.9.23, 3.10.18, 3.11.13, 3.12.11] include: - runs-on: macos-latest c-compiler: "clang" diff --git a/CMakeLists.txt b/CMakeLists.txt index 730e0fa2..8cd0b713 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20.6) -set(PYTHON_VERSION "3.12.10" CACHE STRING "The version of Python to build.") +set(PYTHON_VERSION "3.12.11" CACHE STRING "The version of Python to build.") string(REPLACE "." ";" VERSION_LIST ${PYTHON_VERSION}) list(GET VERSION_LIST 0 PY_VERSION_MAJOR) @@ -278,6 +278,7 @@ set(_download_3.9.19_md5 "b4d723903d0a8266b110c3da2f992416") set(_download_3.9.20_md5 "896c19e5815ba990a3d1261502ea9f83") set(_download_3.9.21_md5 "e61b3568082b57d55fd74cfc7ca020b4") set(_download_3.9.22_md5 "8fe76e248a0e149ac23e8e4886397475") +set(_download_3.9.23_md5 "e6c3c5ba679cc6a1e2932b2fdcafbc3d") # 3.10.x set(_download_3.10.0_md5 "729e36388ae9a832b01cf9138921b383") set(_download_3.10.1_md5 "91822157a97da16203877400c810d93e") @@ -297,6 +298,7 @@ set(_download_3.10.14_md5 "f67d78c8323a18fe6b945914c51a7aa6") set(_download_3.10.15_md5 "b6a2b570ea75ef55f50bfe79d778eb01") set(_download_3.10.16_md5 "2515d8571c6fdd7fc620aa9e1cc6d202") set(_download_3.10.17_md5 "763324aa2b396ee10a51bfa6c645d8e9") +set(_download_3.10.18_md5 "035ff701ad6c9183dc4c6de817924892") # 3.11.x set(_download_3.11.0_md5 "c5f77f1ea256dc5bdb0897eeb4d35bb0") set(_download_3.11.1_md5 "5c986b2865979b393aa50a31c65b64e8") @@ -311,6 +313,7 @@ set(_download_3.11.9_md5 "bfd4d3bfeac4216ce35d7a503bf02d5c") set(_download_3.11.10_md5 "35c36069a43dd57a7e9915deba0f864e") set(_download_3.11.11_md5 "9a5b43fcc06810b8ae924b0a080e6569") set(_download_3.11.12_md5 "b8bb496014f05f5be180fab74810f40b") +set(_download_3.11.13_md5 "8abf1b1a9237f01b54572e2ecb246262") # 3.12.x set(_download_3.12.0_md5 "d6eda3e1399cef5dfde7c4f319b0596c") set(_download_3.12.1_md5 "51c5c22dcbc698483734dff5c8028606") @@ -323,6 +326,7 @@ set(_download_3.12.7_md5 "5d0c0e4c6a022a87165a9addcd869109") set(_download_3.12.8_md5 "304473cf367fa65e450edf4b06b55fcc") set(_download_3.12.9_md5 "ce613c72fa9b32fb4f109762d61b249b") set(_download_3.12.10_md5 "35c03f014408e26e2b06d576c19cac54") +set(_download_3.12.11_md5 "45bda920329568dd6650b0ac556d17db") set(_extracted_dir "Python-${PY_VERSION}")