|
| 1 | +# build_winpython.py |
| 2 | +import os, sys, argparse, datetime, subprocess, shutil |
| 3 | +from pathlib import Path |
| 4 | + |
| 5 | +# --- logging, helpers (same as Step 2) --- |
| 6 | +def log_section(logfile, message): |
| 7 | + ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 8 | + section = f"\n{'-'*40}\n({ts}) {message}\n{'-'*40}\n" |
| 9 | + print(section); open(logfile, 'a', encoding='utf-8').write(section) |
| 10 | + |
| 11 | +# --- Helpers --- |
| 12 | +def get_target_release(python_target): |
| 13 | + mapping = { |
| 14 | + '311': ('3119', '2'), |
| 15 | + '312': ('31210', '2'), |
| 16 | + '313': ('3135', '1'), |
| 17 | + '314': ('3140', '1') |
| 18 | + } |
| 19 | + return mapping.get(python_target, (None, None)) |
| 20 | + |
| 21 | +def delete_folder_if_exists(folder: Path,check_flavor: str=""): |
| 22 | + check_last = folder.parent.name if not folder.is_dir() else folder.name |
| 23 | + if folder.exists() and folder.is_dir() and check_last=="bu" + check_flavor: |
| 24 | + print ("hello", folder) |
| 25 | + folder_old = Path(str(folder)+'.old') |
| 26 | + if folder_old.exists(): |
| 27 | + shutil.rmtree(Path(str(folder)+'.old')) |
| 28 | + os.rename(folder, folder_old) |
| 29 | + shutil.rmtree(Path(str(folder)+'.old')) |
| 30 | + return |
| 31 | + for item in folder_old: |
| 32 | + if item.is_dir(): |
| 33 | + pass |
| 34 | + # delete_folder_if_exists(item) |
| 35 | + else: |
| 36 | + pass |
| 37 | + # item.unlink() |
| 38 | + # folder.rmdir() |
| 39 | + |
| 40 | +def activate_env(env_path): |
| 41 | + # Windows-specific virtual env activation |
| 42 | + env_script = Path(env_path) / "scripts" / "env.bat" |
| 43 | + if not env_script.exists(): |
| 44 | + raise FileNotFoundError(f"Cannot find env.bat at {env_script}") |
| 45 | + # Note: This step is simplified here. Full environment activation logic will be added later. |
| 46 | + |
| 47 | + |
| 48 | +def run_make_py(build_python, winpydirbase, args, logfile): |
| 49 | + cmd = [ |
| 50 | + build_python, "-c", |
| 51 | + ( |
| 52 | + "from wppm import make; " |
| 53 | + f"make.make_all({args.release}, '{args.release_level}', basedir_wpy=r'{winpydirbase}', " |
| 54 | + f"verbose=True, flavor='{args.flavor}', source_dirs=r'{args.source_dirs}', " |
| 55 | + f"toolsdirs=r'{args.tools_dirs}')" #, portable_dir=r'{args.portable_dir}')" |
| 56 | + ) |
| 57 | + ] |
| 58 | + print(cmd) |
| 59 | + from . import make |
| 60 | + make.make_all( args.release , args.release_level , basedir_wpy= winpydirbase , |
| 61 | + verbose=True, flavor= args.flavor , source_dirs= args.source_dirs , |
| 62 | + toolsdirs= args.tools_dirs) #, portable_dir= args.portable_dir ) |
| 63 | + #subprocess.run(cmd, stdout=open(logfile, 'a'), stderr=subprocess.STDOUT, check=True) |
| 64 | + |
| 65 | +def pip_install(python_exe: Path, req_file: str, constraints: str, find_links: str, logfile: Path, label: str): |
| 66 | + if req_file and Path(req_file).exists(): |
| 67 | + cmd = [ |
| 68 | + str(python_exe), "-m", "pip", "install", |
| 69 | + "-r", req_file, "-c", constraints, |
| 70 | + "--pre", "--no-index", f"--find-links={find_links}" |
| 71 | + ] |
| 72 | + log_section(logfile, f"Pip‑install {label}") |
| 73 | + subprocess.run(cmd, stdout=open(logfile, 'a'), stderr=subprocess.STDOUT, check=True) |
| 74 | + else: |
| 75 | + log_section(logfile, f"No {label} specified/skipped") |
| 76 | + |
| 77 | +def run_command(cmd, env=None, log_file=None): |
| 78 | + print(f"[RUN] {cmd}") |
| 79 | + with subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, universal_newlines=True) as proc: |
| 80 | + for line in proc.stdout: |
| 81 | + print(line, end="") |
| 82 | + if log_file: |
| 83 | + with open(log_file, "a", encoding="utf-8") as logf: |
| 84 | + logf.write(line) |
| 85 | + |
| 86 | +def patch_winpython(python_exe: Path, logfile: Path): |
| 87 | + cmd = [str(python_exe), "-c" , |
| 88 | + ( |
| 89 | + "from wppm import wppm;" |
| 90 | + "wppm.Distribution().patch_standard_packages('', to_movable=True)" |
| 91 | + )] |
| 92 | + print(cmd) |
| 93 | + subprocess.run(cmd, stdout=open(logfile, 'a'), stderr=subprocess.STDOUT, check=True) |
| 94 | + #run_command(f'{activate_env(WINPYDIRBASE)} python -c "from wppm import wppm; wppm.Distribution(r\'{WINPYDIRBASE}\').patch_standard_packages(\'\', to_movable=True)"') |
| 95 | + |
| 96 | + |
| 97 | +def check_env_bat(winpydirbase: Path): |
| 98 | + envbat = winpydirbase / "scripts" / "env.bat" |
| 99 | + if not envbat.exists(): |
| 100 | + raise FileNotFoundError(f"Missing env.bat at {envbat}") |
| 101 | + |
| 102 | +def generate_lockfiles(target_python: Path, winpydirbase: Path, constraints: str, find_links: str, logfile: Path, file_postfix: str): |
| 103 | + pip_req = winpydirbase.parent / "requirement_temp.txt" |
| 104 | + with subprocess.Popen([str(target_python), "-m", "pip", "freeze"], stdout=subprocess.PIPE) as proc: |
| 105 | + packages = [l for l in proc.stdout if b"winpython" not in l] |
| 106 | + pip_req.write_bytes(b"".join(packages)) |
| 107 | + # Lock to web and local (scaffolding) |
| 108 | + # if local --no-index --trusted-host=None --find-links="%my_find_links%" |
| 109 | + for kind in ("", "local"): |
| 110 | + out = winpydirbase.parent / f"pylock.{file_postfix}_{kind}.toml" |
| 111 | + outreq = winpydirbase.parent / f"requir.{file_postfix}_{kind}.txt" |
| 112 | + print( |
| 113 | + [str(target_python), "-m", "pip", "lock", "--no-deps", "-c", constraints] + |
| 114 | + (["--find-links",find_links] if kind =="local" else []) + |
| 115 | + ["-r", str(pip_req), "-o", str(out)] |
| 116 | + ) |
| 117 | + subprocess.run( |
| 118 | + [str(target_python), "-m", "pip", "lock", "--no-deps", "-c", constraints] + |
| 119 | + (["--find-links",find_links] if kind =="local" else []) + |
| 120 | + ["-r", str(pip_req), "-o", str(out)], |
| 121 | + stdout=open(logfile, 'a'), stderr=subprocess.STDOUT, check=True |
| 122 | + ) |
| 123 | + # Convert both locks to requirement.txt with hash256 |
| 124 | + cmd = f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{out}', r'{outreq}')" |
| 125 | + print( |
| 126 | + [str(target_python), "-c", cmd] |
| 127 | + ) |
| 128 | + subprocess.run( |
| 129 | + [str(target_python), "-c", cmd], |
| 130 | + stdout=open(logfile, 'a'), stderr=subprocess.STDOUT, check=False |
| 131 | + ) |
| 132 | + # check equality |
| 133 | + from filecmp import cmp |
| 134 | + web, local = "", "local" |
| 135 | + if not cmp(winpydirbase.parent / f"requir.{file_postfix}_{web}.txt", winpydirbase.parent / f"requir.{file_postfix}_{local}.txt"): |
| 136 | + print("ALARM differences in ", winpydirbase.parent / f"requir.{file_postfix}_{web}.txt", winpydirbase.parent / f"requir.{file_postfix}_{local}.txt") |
| 137 | + raise os.error |
| 138 | + else: |
| 139 | + print ("match ok ",winpydirbase.parent / f"requir.{file_postfix}_{web}.txt", winpydirbase.parent / f"requir.{file_postfix}_{local}.txt") |
| 140 | + |
| 141 | +# --- main --- |
| 142 | +def main(): |
| 143 | + parser = argparse.ArgumentParser() |
| 144 | + |
| 145 | + parser.add_argument('--python-target', required=True, help='Target Python version, e.g. 311') |
| 146 | + parser.add_argument('--release', default='', help='Release') |
| 147 | + parser.add_argument('--flavor', default='', help='Build flavor') |
| 148 | + parser.add_argument('--arch', default='64', help='Architecture') |
| 149 | + |
| 150 | + parser.add_argument('--release-level', default='b1', help='Release level (e.g., b1, rc)') |
| 151 | + parser.add_argument('--winpydirbase', required=True, help='Path to put environment') |
| 152 | + parser.add_argument('--source_dirs', required=True, help='Path to directory with python zip') |
| 153 | + |
| 154 | + parser.add_argument('--tools_dirs', required=True, help='Path to directory with python zip') |
| 155 | + #parser.add_argument('--portable_dir', required=True, help='Path to normal make.py') |
| 156 | + |
| 157 | + parser.add_argument('--buildenv', required=True, help='Path to build environment') |
| 158 | + parser.add_argument('--constraints', default='constraints.txt', help='Constraints file') |
| 159 | + parser.add_argument('--requirements', help='Main requirements.txt file') |
| 160 | + parser.add_argument('--find-links', default='wheelhouse', help='Path to local wheelhouse') |
| 161 | + parser.add_argument('--log-dir', default='WinPython_build_logs', help='Directory for logs') |
| 162 | + parser.add_argument('--mandatory-req', help='Mandatory requirements file') |
| 163 | + parser.add_argument('--pre-req', help='Pre requirements file') |
| 164 | + parser.add_argument('--wheelhousereq', help='Wheelhouse requirements file') |
| 165 | + parser.add_argument('--create-installer', default='', help='default installer to create') |
| 166 | + args = parser.parse_args() |
| 167 | + |
| 168 | + # compute paths (same as Step2)... |
| 169 | + build_python = Path(args.buildenv) / "python.exe" |
| 170 | + winpydirbase = Path(args.winpydirbase) # from Step2 logic |
| 171 | + target_python = winpydirbase / "python" / "python.exe" |
| 172 | + |
| 173 | + # Setup paths and logs |
| 174 | + now = datetime.datetime.now() |
| 175 | + log_dir = Path(args.log_dir) |
| 176 | + log_dir.mkdir(exist_ok=True) |
| 177 | + time_str = now.strftime("%Y-%m-%d_at_%H%M") |
| 178 | + log_file = log_dir / f"build_{args.python_target}_{args.flavor}_{args.release_level}_{time_str}.txt" |
| 179 | + |
| 180 | + #logs termination |
| 181 | + z = Path(winpydirbase).name[(4+len(args.arch)):-len(args.release_level)] |
| 182 | + tada = f"{z[:1]}_{z[1:3]}_{z[3]}_{args.release}" |
| 183 | + winpyver2 = tada.replace('_','.') |
| 184 | + file_postfix=f"{args.arch}-{args.python_target[:1]}_{args.python_target[1:]}_{args.release}{args.flavor}{args.release_level}" |
| 185 | + file_postfix=f"{args.arch}-{tada}{args.flavor}{args.release_level}" |
| 186 | + |
| 187 | + log_section(log_file, f"Preparing build for Python {args.python_target} ({args.arch}-bit)") |
| 188 | + |
| 189 | + log_section(log_file, f"🙏 Step 0: displace old {Path(winpydirbase)}") |
| 190 | + # O) Pre-clear |
| 191 | + |
| 192 | + #delete_folder_if_exists(Path(winpydirbase)) |
| 193 | + delete_folder_if_exists(winpydirbase.parent, check_flavor=args.flavor) #bu{flavor]} |
| 194 | + |
| 195 | + log_section(log_file, f"🙏 Step 1: make.py Python with {str(build_python)} at ({winpydirbase}") |
| 196 | + # 1) run make.py |
| 197 | + run_make_py(str(build_python), winpydirbase, args, log_file) |
| 198 | + |
| 199 | + # 2) env.bat exists |
| 200 | + check_env_bat(winpydirbase) |
| 201 | + |
| 202 | + # 3) pip install in built environment |
| 203 | + log_section(log_file, "🙏 Step 3: install requirements") |
| 204 | + |
| 205 | + for label, req in [ |
| 206 | + ("Mandatory", args.mandatory_req), |
| 207 | + ("Pre", args.pre_req), |
| 208 | + ("Main", args.requirements), |
| 209 | + ]: |
| 210 | + pip_install(target_python, req, args.constraints, args.find_links, log_file, label) |
| 211 | + |
| 212 | + # 4) patch Winpython |
| 213 | + log_section(log_file, "🙏 Step 4: Patch Winpython") |
| 214 | + patch_winpython(target_python, log_file) |
| 215 | + |
| 216 | + # 5) Install Wheelhouse |
| 217 | + if args.wheelhousereq: |
| 218 | + log_section(log_file, f"🙏 Step 5: install wheelhouse requirements {args.wheelhousereq}") |
| 219 | + wheelhousereq = Path(args.wheelhousereq) |
| 220 | + kind = "local" |
| 221 | + out = winpydirbase.parent / f"pylock.{file_postfix}_wheels{kind}.toml" |
| 222 | + outreq = winpydirbase.parent / f"requir.{file_postfix}_wheels{kind}.txt" |
| 223 | + if wheelhousereq.is_file(): |
| 224 | + # Generate pylock from wheelhousereq |
| 225 | + cmd = [str(target_python), "-m" , "pip", "lock","--no-index", "--trusted-host=None", |
| 226 | + "--find-links", args.find_links, "-c", args.constraints, "-r", wheelhousereq, |
| 227 | + "-o", out ] |
| 228 | + subprocess.run(cmd, stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=True) |
| 229 | + # Convert pylock to requirements with hash |
| 230 | + cmd = f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{out}', r'{outreq}')" |
| 231 | + print( [str(target_python), "-c", cmd] ) |
| 232 | + subprocess.run([str(target_python), "-c", cmd], |
| 233 | + stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=False |
| 234 | + ) |
| 235 | + |
| 236 | + kind = "" |
| 237 | + outw = winpydirbase.parent / f"pylock.{file_postfix}_wheels{kind}.toml" |
| 238 | + outreqw = winpydirbase.parent / f"requir.{file_postfix}_wheels{kind}.txt" |
| 239 | + # Generate web pylock from local frozen hashes |
| 240 | + cmd = [str(target_python), "-m" , "pip", "lock","--no-deps", "--require-hashes", |
| 241 | + "-r", str(outreq), "-o", str(outw) ] |
| 242 | + subprocess.run(cmd, stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=True) |
| 243 | + cmd = f"from wppm import wheelhouse as wh; wh.pylock_to_req(r'{outw}', r'{outreqw}')" |
| 244 | + print( [str(target_python), "-c", cmd] ) |
| 245 | + subprocess.run([str(target_python), "-c", cmd], |
| 246 | + stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=False |
| 247 | + ) |
| 248 | + |
| 249 | + # Use wppm to download local from req made with web hashes |
| 250 | + wheelhouse = winpydirbase / "wheelhouse" / "included.wheels" |
| 251 | + cmd = [str(target_python), "-X", "utf8", "-m", "wppm", str(out), "-ws", args.find_links, |
| 252 | + "-wd", str(wheelhouse) |
| 253 | + ] |
| 254 | + print(cmd) |
| 255 | + subprocess.run(cmd, stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=False) |
| 256 | + |
| 257 | + |
| 258 | + |
| 259 | + # 6) lock files |
| 260 | + log_section(log_file, "🙏 Step 6: install lockfiles") |
| 261 | + print(target_python, winpydirbase, args.constraints, args.find_links, log_file) |
| 262 | + print(' - - -') |
| 263 | + generate_lockfiles(target_python, winpydirbase, args.constraints, args.find_links, log_file, file_postfix) |
| 264 | + |
| 265 | + # 5b) Archive lock files |
| 266 | + |
| 267 | + # 6) generate changelog |
| 268 | + mdn = f"WinPython{args.flavor}-{args.arch}bit-{winpyver2}.md" |
| 269 | + out = f"WinPython{args.flavor}-{args.arch}bit-{winpyver2}_History.md" |
| 270 | + changelog_dir = log_dir.parent/ "changelogs" |
| 271 | + |
| 272 | + log_section(log_file, f"🙏 Step 6: generate changelog {mdn}") |
| 273 | + |
| 274 | + cmd = ["set", f"WINPYVER2={winpyver2}&", "set", f"WINPYFLAVOR={args.flavor}&", |
| 275 | + "set", f"WINPYVER={winpyver2}{args.flavor}{args.release_level}&", |
| 276 | + str(target_python), "-c" , |
| 277 | + ( |
| 278 | + "from wppm import wppm;" |
| 279 | + "result = wppm.Distribution().generate_package_index_markdown();" |
| 280 | + f"open(r'{winpydirbase.parent / mdn}', 'w', encoding='utf-8').write(result)" |
| 281 | + )] |
| 282 | + print(cmd) |
| 283 | + subprocess.run(cmd, stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=True, shell=True) |
| 284 | + shutil.copyfile (winpydirbase.parent / mdn, changelog_dir / mdn) |
| 285 | + |
| 286 | + cmd = [str(target_python), "-c", |
| 287 | + ( |
| 288 | + "from wppm import diff;" |
| 289 | + f"result = diff.compare_package_indexes('{winpyver2}', searchdir=r'{log_dir}', flavor=r'{args.flavor}', architecture={args.arch});" |
| 290 | + f"open(r'{winpydirbase.parent / out}', 'w', encoding='utf-8').write(result)" |
| 291 | + )] |
| 292 | + subprocess.run(cmd, stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=True) |
| 293 | + shutil.copyfile (winpydirbase.parent / out, changelog_dir / out) |
| 294 | + log_section(log_file, "✅ Step 6 complete") |
| 295 | + |
| 296 | + # 7) Create Installers |
| 297 | + if args.create_installer != "": |
| 298 | + log_section(log_file, "🙏 Step 7 Create Installer") |
| 299 | + stem = f"WinPython{args.arch}-{winpyver2}{args.flavor}{args.release_level}" |
| 300 | + cmd = f"from wppm import utils; utils.command_installer_7zip(r'{winpydirbase}', r'{winpydirbase.parent}', r'{stem}', r'{args.create_installer}')" |
| 301 | + print( [str(target_python), "-c", cmd] ) |
| 302 | + subprocess.run([str(target_python), "-c", cmd], |
| 303 | + stdout=open(log_file, 'a'), stderr=subprocess.STDOUT, check=False) |
| 304 | + |
| 305 | +if __name__ == '__main__': |
| 306 | + main() |
0 commit comments