# build_winpython.py
import os, sys, argparse, datetime, subprocess, shutil
import logging
from pathlib import Path
from filecmp import cmp
def setup_logging(log_file: Path, LOG_FORMAT="%(asctime)s %(levelname)s: %(message)s"):
"""Initialize logging to both file and stdout."""
logging.basicConfig(
level=logging.INFO,
format=LOG_FORMAT,
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(str(log_file), encoding="utf-8", mode='a')
]
)
def log_section(message: str, highlight_bar="-"*40):
logging.info("\n" + highlight_bar)
logging.info(message)
logging.info(highlight_bar)
def delete_folder_if_exists(folder: Path, check_flavor: str = ""):
check_last = folder.parent.name if not folder.is_dir() else folder.name
expected_name = "bu" + check_flavor
if folder.exists() and folder.is_dir() and check_last == expected_name:
logging.info(f"Removing old backup: {folder}")
folder_old = folder.with_suffix('.old')
if folder_old.exists():
shutil.rmtree(folder_old)
folder.rename(folder_old)
shutil.rmtree(folder_old)
def run_command(cmd, shell=False, check=True):
logging.info(f"[RUNNING] {' '.join(cmd) if isinstance(cmd, list) else cmd}")
with subprocess.Popen(
cmd, shell=shell, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, universal_newlines=True
) as proc:
for line in proc.stdout:
logging.info(line.rstrip())
if check and proc.wait() != 0:
raise subprocess.CalledProcessError(proc.returncode, cmd)
def pip_install(python_exe: Path, req_file: str, constraints: str, find_links: str, label: str):
if req_file and Path(req_file).exists():
cmd = [
str(python_exe), "-m", "pip", "install",
"-r", req_file, "-c", constraints,
"--pre", "--no-index", f"--find-links={find_links}"
]
log_section(f"Pip-install {label}")
run_command(cmd)
else:
log_section(f"No {label} specified/skipped")
def patch_winpython(python_exe):
cmd = [
str(python_exe), "-c",
"from wppm import wppm; wppm.Distribution().patch_standard_packages('', to_movable=True)"
]
run_command(cmd)
def check_env_bat(winpydirbase: Path):
envbat = winpydirbase / "scripts" / "env.bat"
if not envbat.exists():
raise FileNotFoundError(f"Missing env.bat at {envbat}")
def generate_lockfiles(target_python: Path, winpydirbase: Path, constraints: str, find_links: str, file_postfix: str):
pip_req = winpydirbase.parent / "requirement_temp.txt"
with subprocess.Popen([str(target_python), "-m", "pip", "freeze"], stdout=subprocess.PIPE) as proc:
packages = [l for l in proc.stdout if b"winpython" not in l]
pip_req.write_bytes(b"".join(packages))
# Lock to web and local (scaffolding)
for kind in ("", "local"):
out = winpydirbase.parent / f"pylock.{file_postfix}{kind}.toml"
outreq = winpydirbase.parent / f"requir.{file_postfix}{kind}.txt"
cmd = [str(target_python), "-m", "pip", "lock", "--no-deps", "-c", constraints]
if kind == "local":
cmd += ["--find-links", find_links]
cmd += ["-r", str(pip_req), "-o", str(out)]
run_command(cmd)
# Convert both locks to requirement.txt with hash256
cmd = [str(target_python), "-X", "utf8", "-c", f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{out}', r'{outreq}')"]
run_command(cmd)
# check equality
web, local = "", "local"
if not cmp(winpydirbase.parent / f"requir.{file_postfix}{web}.txt", winpydirbase.parent / f"requir.{file_postfix}{local}.txt"):
print("⚠️⚠️⚠️⚠️⚠️⚠️ ALARM ⚠️⚠️⚠️⚠️⚠️⚠️differences in ", winpydirbase.parent / f"requir.{file_postfix}{web}.txt", winpydirbase.parent / f"requir.{file_postfix}{local}.txt")
raise os.error
else:
print ("💖💖💖 match 💖💖💖 ok ",winpydirbase.parent / f"requir.{file_postfix}{web}.txt", winpydirbase.parent / f"requir.{file_postfix}{local}.txt")
# --- Main Logic ---
def run_make_py(build_python, winpydirbase, args):
from . import make
make.make_all(
args.release, args.release_level, basedir_wpy=winpydirbase,
verbose=True, flavor=args.flavor,
source_dirs=args.source_dirs, toolsdirs=args.tools_dirs
)
def process_wheelhouse_requirements(target_python: Path, winpydirbase: Path,args: argparse.Namespace,file_postfix: str):
"""
Handle installation and conversion of wheelhouse requirements.
"""
wheelhousereq = Path(args.wheelhousereq)
kind = "local"
out = winpydirbase.parent / f"pylock.{file_postfix}_wheels{kind}.toml"
outreq = winpydirbase.parent / f"requir.{file_postfix}_wheels{kind}.txt"
if wheelhousereq.is_file():
# Generate pylock from wheelhousereq
cmd = [
str(target_python), "-m", "pip", "lock", "--no-index", "--trusted-host=None", "--pre",
"--find-links", args.find_links, "-c", args.constraints, "-r", str(wheelhousereq),
"-o", str(out)
]
run_command(cmd)
# Convert pylock to requirements with hash
pylock_to_req_cmd = [
str(target_python), "-X", "utf8", "-c",
f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{out}', r'{outreq}')"
]
run_command(pylock_to_req_cmd, check=False)
kind = ""
outw = winpydirbase.parent / f"pylock.{file_postfix}_wheels{kind}.toml"
outreqw = winpydirbase.parent / f"requir.{file_postfix}_wheels{kind}.txt"
# Generate web pylock from local frozen hashes
web_lock_cmd = [
str(target_python), "-m", "pip", "lock", "--no-deps", "--require-hashes",
"-r", str(outreq), "-o", str(outw)
]
run_command(web_lock_cmd)
pylock_to_req_cmd2 = [
str(target_python), "-X", "utf8", "-c",
f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{outw}', r'{outreqw}')"
]
run_command(pylock_to_req_cmd2, check=False)
# Use wppm to download local from req made with web hashes
wheelhouse = winpydirbase / "wheelhouse" / "included.wheels"
wppm_cmd = [
str(target_python), "-X", "utf8", "-m", "wppm", str(out), "-ws", args.find_links,
"-wd", str(wheelhouse)
]
run_command(wppm_cmd, check=False)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--python-target', required=True, help='Target Python version, e.g. 311')
parser.add_argument('--release', default='', help='Release')
parser.add_argument('--flavor', default='', help='Build flavor')
parser.add_argument('--arch', default='64', help='Architecture')
parser.add_argument('--release-level', default='', help='Release level (e.g., b1, b2)')
parser.add_argument('--winpydirbase', required=True, help='Path to put environment')
parser.add_argument('--source_dirs', required=True, help='Path to directory with python zip')
parser.add_argument('--tools_dirs', required=True, help='Path to directory with python zip')
parser.add_argument('--buildenv', required=True, help='Path to build environment')
parser.add_argument('--constraints', default='constraints.txt', help='Constraints file')
parser.add_argument('--requirements', help='Main requirements.txt file')
parser.add_argument('--find-links', default='wheelhouse', help='Path to local wheelhouse')
parser.add_argument('--log-dir', default='WinPython_build_logs', help='Directory for logs')
parser.add_argument('--mandatory-req', help='Mandatory requirements file')
parser.add_argument('--pre-req', help='Pre requirements file')
parser.add_argument('--wheelhousereq', help='Wheelhouse requirements file')
parser.add_argument('--create-installer', default='', help='default installer to create')
args = parser.parse_args()
# compute paths (same as Step2)...
build_python = Path(args.buildenv) / "python.exe"
winpydirbase = Path(args.winpydirbase)
target_python = winpydirbase / "python" / "python.exe"
# Setup paths and logs
now = datetime.datetime.now()
log_dir = Path(args.log_dir)
log_dir.mkdir(exist_ok=True)
time_str = now.strftime("%Y-%m-%d_at_%H%M")
log_file = log_dir / f"build_{args.python_target}_{args.flavor}_{args.release_level}_{time_str}.txt"
setup_logging(log_file)
# Logs termination and version naming
if len(args.release_level) > 0:
z = Path(winpydirbase).name[(4+len(args.arch)):-len(args.release_level)]
else:
z = Path(winpydirbase).name[(4+len(args.arch)):]
tada = f"{z[:1]}_{z[1:3]}_{z[3]}_{args.release}"
winpyver2 = tada.replace('_', '.')
file_postfix = f"{args.arch}-{tada}{args.flavor}{args.release_level}"
log_section(f"Preparing build for Python {args.python_target} ({args.arch}-bit)")
log_section(f"🙏 Step 1: displace old {Path(winpydirbase)}")
delete_folder_if_exists(winpydirbase.parent, check_flavor=args.flavor) #bu{flavor]}
log_section(f"🙏 Step 2: make.py Python with {str(build_python)} at ({winpydirbase}")
run_make_py(str(build_python), winpydirbase, args)
check_env_bat(winpydirbase)
log_section("🙏 Step 3: install requirements")
for label, req in [
("Mandatory", args.mandatory_req),
("Pre", args.pre_req),
("Main", args.requirements),
]:
pip_install(target_python, req, args.constraints, args.find_links, label)
log_section("🙏 Step 4: Patch Winpython")
patch_winpython(target_python)
log_section(f"🙏 Step 5: install wheelhouse requirements {args.wheelhousereq}")
if args.wheelhousereq:
process_wheelhouse_requirements(target_python, winpydirbase, args, file_postfix)
log_section("🙏 Step 6: install lockfiles")
print(target_python, winpydirbase, args.constraints, args.find_links, file_postfix)
generate_lockfiles(target_python, winpydirbase, args.constraints, args.find_links, file_postfix)
log_section(f"🙏 Step 7: generate changelog")
mdn = f"WinPython{args.flavor}-{args.arch}bit-{winpyver2}.md"
out = f"WinPython{args.flavor}-{args.arch}bit-{winpyver2}_History.md"
changelog_dir = log_dir.parent/ "changelogs"
cmd = ["set", f"WINPYVER2={winpyver2}&", "set", f"WINPYFLAVOR={args.flavor}&",
"set", f"WINPYVER={winpyver2}{args.flavor}{args.release_level}&",
str(target_python), "-X", "utf8", "-c" ,
(
"from wppm import wppm;"
"result = wppm.Distribution().generate_package_index_markdown();"
f"open(r'{winpydirbase.parent / mdn}', 'w', encoding='utf-8').write(result)"
)]
run_command(cmd, shell=True)
shutil.copyfile (winpydirbase.parent / mdn, changelog_dir / mdn)
cmd = [str(target_python), "-X", "utf8", "-c",
(
"from wppm import diff;"
f"result = diff.compare_package_indexes('{winpyver2}', searchdir=r'{changelog_dir}', flavor=r'{args.flavor}', architecture={args.arch});"
f"open(r'{winpydirbase.parent / out}', 'w', encoding='utf-8').write(result)"
)]
run_command(cmd, check=False)
shutil.copyfile (winpydirbase.parent / out, changelog_dir / out)
if args.create_installer != "":
log_section("🙏 Step 8: Create Installer")
stem = f"WinPython{args.arch}-{winpyver2}{args.flavor}{args.release_level}"
cmd = [str(target_python), "-c",
f"from wppm import utils; utils.command_installer_7zip(r'{winpydirbase}', r'{winpydirbase.parent}', r'{stem}', r'{args.create_installer}')" ]
run_command(cmd, check=False)
if __name__ == '__main__':
main()