# build_winpython_meta.py # Local builds are made with Python 3.11 or later. import os import sys import tomllib # stdlib since 3.11; we choose toml over yaml, more python standard import subprocess from pathlib import Path # Flavor paths are names, not full paths: these say what they hang from. UNDER_BASEDIR = ("requirements", "source_dirs", "wheelhousereq") UNDER_ROOT = ("toolsdirs",) def load_builds(config_file): with open(config_file, "rb") as f: config = tomllib.load(f) python_versions = config.get("pythons", {}) # A file may still spell out one [[builds]] block per build; otherwise the # builds are the (python, flavor) pairs [pythons] asks for. builds = config["builds"] if "builds" in config else expand_builds(config) return builds, python_versions def expand_builds(config): """One build per flavor listed by each [pythons."3XX"], paths derived. A flavor names its files relative to the build directory, so a new Python minor is one [pythons] block. Where a Python needs its own file - a requirements list under another name, say - [pythons."3XX".overrides.flavor] replaces that flavor's entries for that Python alone. """ defaults = config.get("defaults", {}) flavors = config.get("flavors", {}) builds = [] for target, vinfo in config.get("pythons", {}).items(): root = Path(vinfo.get("root_dir_for_builds", defaults.get("root_dir_for_builds", ""))) basedir = root / f"bd{target}" overrides = vinfo.get("overrides", {}) for flavor in vinfo.get("builds", []): if flavor not in flavors: raise KeyError(f"python {target} builds {flavor!r}, which has no [flavors.{flavor}]") build = {**defaults, "name": flavor, "python_target": target, "flavor": flavor} for key, value in {**flavors[flavor], **overrides.get(flavor, {})}.items(): if key in UNDER_BASEDIR: value = str(basedir / value) elif key in UNDER_ROOT: value = str(root / value) build[key] = value builds.append(build) return builds def select_builds(builds, wanted): """Builds asked for on the command line: "315", "315:slim", ":slim".""" if not wanted: return builds kept = [] for spec in wanted: target, _, flavor = spec.partition(":") matching = [b for b in builds if (not target or b["python_target"] == target) and (not flavor or b["flavor"] == flavor)] if not matching: raise SystemExit(f"no build matches {spec!r}") kept += [b for b in matching if b not in kept] return kept def must_exist(path, build, key): """pip_install() skips a missing requirements file without failing, which would drop those packages silently. Say so instead.""" if path and not Path(path).exists(): raise FileNotFoundError(f"build {build['name']!r} needs {key} = {path!r}, which does not exist") return path def declared_file(build, key, default): """Path named by the build for `key`, else `default`.""" declared = build.get(key) return str(default) if declared is None else must_exist(str(declared), build, key) def run_build(build, python_versions, dry_run=False): print(f"\n=== Building WinPython: {build['python_target']} {build['name']} ===") print(build) root_dir_for_builds = build["root_dir_for_builds"] my_python_target = build["python_target"] my_flavor = build["flavor"] my_arch = str(build["arch"]) my_create_installer = build.get("create_installer", "True") my_requirements = must_exist(build.get("requirements", ""), build, "requirements") my_source_dirs = build.get("source_dirs", "") my_find_links = build.get("find_links", "") my_toolsdirs = build.get("toolsdirs", "") wheelhousereq = must_exist(build.get("wheelhousereq", ""), build, "wheelhousereq") # "pip" (default, what ships) | "none" | "parallel" | "parallel-N" my_bytecode = build.get("bytecode", "pip") # Get Python release info from TOML [pythons] py_target = my_python_target vinfo = python_versions.get(py_target, {}) my_python_target_release = vinfo.get("python_target_release", "") my_release = vinfo.get("release", "") my_release_level = vinfo.get("my_release_level", "b0") # A build may name its own file for a specific problem; otherwise the one # shipped beside this script is used. here = Path(__file__).parent mandatory_requirements = declared_file(build, "mandatory_requirements", here / "mandatory_requirements.txt") my_constraints = declared_file(build, "constraints", here / "constraints.txt") # Build directory logic my_basedir = f"{root_dir_for_builds}\\bd{my_python_target}" my_WINPYDIRBASE = f"{my_basedir}\\bu{my_flavor}\\WPy{my_arch}-{my_python_target_release}{my_release}{my_release_level}" # Build env paths (customize as needed) already defined per the launcher of that script... my_python_exe = Path(sys.executable) my_buildenvi = str(my_python_exe.parent) my_archive_dir = os.path.join(os.getcwd(), "WinPython_build_logs") os.makedirs(my_archive_dir, exist_ok=True) # Build command build_cmd = [ str(my_python_exe), "-m", "winpython.build_winpython", "--buildenv", my_buildenvi, "--python-target", my_python_target, "--release", my_release, "--release-level", my_release_level, "--winpydirbase", my_WINPYDIRBASE, "--flavor", my_flavor, "--source_dirs", my_source_dirs, "--tools_dirs", my_toolsdirs, "--log-dir", my_archive_dir, "--mandatory-req", mandatory_requirements, "--requirements", my_requirements, "--constraints", my_constraints, "--find-links", my_find_links, "--wheelhousereq", wheelhousereq, "--bytecode", my_bytecode, "--create-installer", my_create_installer, ] print("Dry run, build command:" if dry_run else "Running build command:") print(" ".join(build_cmd)) if not dry_run: subprocess.run(build_cmd, check=False) def main(): args = [a for a in sys.argv[1:] if a != "--dry-run"] dry_run = len(args) != len(sys.argv) - 1 config_file = args[0] if args else "winpython_builds.toml" builds, python_versions = load_builds(config_file) for build in select_builds(builds, args[1:]): run_build(build, python_versions, dry_run) if __name__ == "__main__": main()