|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# |
| 3 | +# WinPython build script |
| 4 | +# Copyright © 2012 Pierre Raybaut |
| 5 | +# Copyright © 2014-2025+ The Winpython development team https://github.com/winpython/ |
| 6 | +# Licensed under the terms of the MIT License |
| 7 | +# (see wppm/__init__.py for details) |
| 8 | + |
| 9 | +import os |
| 10 | +import re |
| 11 | +import shutil |
| 12 | +from pathlib import Path |
| 13 | +from wppm import wppm, utils |
| 14 | + |
| 15 | +PORTABLE_DIRECTORY = Path(__file__).parent / "portable" |
| 16 | +assert PORTABLE_DIRECTORY.is_dir(), f"Portable directory not found: {PORTABLE_DIRECTORY}" |
| 17 | + |
| 18 | +def copy_items(source_directories: list[Path], target_directory: Path, verbose: bool = False): |
| 19 | + """Copies items from source directories to the target directory.""" |
| 20 | + target_directory.mkdir(parents=True, exist_ok=True) |
| 21 | + for source_dir in source_directories: |
| 22 | + if not source_dir.is_dir(): |
| 23 | + print(f"Warning: Source directory not found: {source_dir}") |
| 24 | + continue |
| 25 | + for source_item in source_dir.iterdir(): |
| 26 | + target_item = target_directory / source_item.name |
| 27 | + copy_function = shutil.copytree if source_item.is_dir() else shutil.copy2 |
| 28 | + try: |
| 29 | + copy_function(source_item, target_item) |
| 30 | + if verbose: |
| 31 | + print(f"Copied: {source_item} -> {target_item}") |
| 32 | + except Exception as e: |
| 33 | + print(f"Error copying {source_item} to {target_item}: {e}") |
| 34 | + |
| 35 | +def parse_list_argument(argument_value: str | list[str], separator=" ") -> list[str]: |
| 36 | + """Parse a separated list argument into a list of strings.""" |
| 37 | + if not argument_value: return [] |
| 38 | + return argument_value.split(separator) if isinstance(argument_value, str) else list(argument_value) |
| 39 | + |
| 40 | +class WinPythonDistributionBuilder: |
| 41 | + """Builds a WinPython distribution.""" |
| 42 | + |
| 43 | + def __init__(self, build_number: int, release_level: str, basedir_wpy: Path, |
| 44 | + source_dirs: Path, tools_directories: list[Path] = None, |
| 45 | + verbose: bool = False, flavor: str = ""): |
| 46 | + """ |
| 47 | + Initializes the WinPythonDistributionBuilder. |
| 48 | + Args: |
| 49 | + build_number: The build number (integer). |
| 50 | + release_level: The release level (e.g., "beta", ""). |
| 51 | + basedir_wpy: top directory of the build (c:\...\Wpy...) |
| 52 | + source_dirs: Directory containing wheel files for packages. |
| 53 | + tools_directories: List of directories containing development tools to include. |
| 54 | + verbose: Enable verbose output. |
| 55 | + flavor: WinPython flavor (e.g., "Barebone"). |
| 56 | + """ |
| 57 | + self.build_number = build_number |
| 58 | + self.release_level = release_level |
| 59 | + self.winpython_directory = Path(basedir_wpy) |
| 60 | + self.target_directory = self.winpython_directory.parent |
| 61 | + self.source_dirs = Path(source_dirs) |
| 62 | + self.tools_directories = tools_directories or [] |
| 63 | + self.verbose = verbose |
| 64 | + self.distribution: wppm.Distribution | None = None |
| 65 | + self.flavor = flavor |
| 66 | + self.python_zip_file: Path = self._get_python_zip_file() |
| 67 | + self.python_name = self.python_zip_file.stem |
| 68 | + self.python_directory_name = "python" |
| 69 | + |
| 70 | + def _get_python_zip_file(self) -> Path: |
| 71 | + """Finds the Python .zip file in the wheels directory.""" |
| 72 | + for source_item in self.source_dirs.iterdir(): |
| 73 | + if re.match(r"(pypy3|python-).*\.zip", source_item.name): |
| 74 | + return source_item |
| 75 | + raise RuntimeError(f"Could not find Python zip package in {self.source_dirs}") |
| 76 | + |
| 77 | + @property |
| 78 | + def winpython_version_name(self) -> str: |
| 79 | + """Returns the full WinPython version string.""" |
| 80 | + return f"{self.python_full_version}.{self.build_number}{self.flavor}{self.release_level}" |
| 81 | + |
| 82 | + @property |
| 83 | + def python_full_version(self) -> str: |
| 84 | + """Retrieves the Python full version string from the distribution.""" |
| 85 | + return utils.get_python_long_version(self.distribution.target) if self.distribution else "0.0.0" |
| 86 | + |
| 87 | + def _print_action(self, text: str): |
| 88 | + """Prints an action message with progress indicator.""" |
| 89 | + utils.print_box(text) if self.verbose else print(f"{text}...", end="", flush=True) |
| 90 | + |
| 91 | + def _extract_python_archive(self): |
| 92 | + """Extracts the Python zip archive to create the base Python environment.""" |
| 93 | + self._print_action("Extracting Python archive") |
| 94 | + utils.extract_archive(self.python_zip_file, self.winpython_directory) |
| 95 | + # Relocate to /python subfolder if needed (for newer structure) #2024-12-22 to /python |
| 96 | + expected_python_directory = self.winpython_directory / self.python_directory_name |
| 97 | + if self.python_directory_name != self.python_name and not expected_python_directory.is_dir(): |
| 98 | + os.rename(self.winpython_directory / self.python_name, expected_python_directory) |
| 99 | + |
| 100 | + def _copy_essential_files(self): |
| 101 | + """Copies pre-made objects""" |
| 102 | + self._print_action("Copying launchers") |
| 103 | + copy_items([PORTABLE_DIRECTORY / "launchers_final"], self.winpython_directory, self.verbose) |
| 104 | + |
| 105 | + tools_target_directory = self.winpython_directory / "t" |
| 106 | + self._print_action(f"Copying tools to {tools_target_directory}") |
| 107 | + copy_items(self.tools_directories, tools_target_directory, self.verbose) |
| 108 | + |
| 109 | + def _create_env_config(self): |
| 110 | + """Creates environment setup""" |
| 111 | + executable_name = self.distribution.short_exe if self.distribution else "python.exe" |
| 112 | + config = { |
| 113 | + "WINPYthon_exe": executable_name, |
| 114 | + "WINPYthon_subdirectory_name": self.python_directory_name, |
| 115 | + "WINPYVER": self.winpython_version_name, |
| 116 | + "WINPYVER2": f"{self.python_full_version}.{self.build_number}", |
| 117 | + "WINPYFLAVOR": self.flavor, |
| 118 | + "WINPYARCH": self.distribution.architecture if self.distribution else 64, |
| 119 | + } |
| 120 | + env_path = self.winpython_directory / "scripts" / "env.ini" |
| 121 | + env_path.parent.mkdir(parents=True, exist_ok=True) |
| 122 | + self._print_action(f"Creating env.ini environment {env_path}") |
| 123 | + env_path.write_text("\n".join(f"{k}={v}" for k, v in config.items())) |
| 124 | + |
| 125 | + def build(self): |
| 126 | + """Make or finalise WinPython distribution in the target directory""" |
| 127 | + print(f"Building WinPython with Python archive: {self.python_zip_file.name}") |
| 128 | + self._print_action(f"Creating WinPython {self.winpython_directory} base directory") |
| 129 | + if self.winpython_directory.is_dir() and len(self.winpython_directory.parts)>=4: |
| 130 | + shutil.rmtree(self.winpython_directory) |
| 131 | + # preventive re-Creation of settings directory |
| 132 | + (self.winpython_directory / "settings" / "AppData" / "Roaming").mkdir(parents=True, exist_ok=True) |
| 133 | + |
| 134 | + self._extract_python_archive() |
| 135 | + self.distribution = wppm.Distribution(self.winpython_directory / self.python_directory_name, verbose=self.verbose) |
| 136 | + self._copy_essential_files() |
| 137 | + self._create_env_config() |
| 138 | + |
| 139 | +def make_all(build_number: int, release_level: str, basedir_wpy: Path = None, |
| 140 | + source_dirs: Path = None, toolsdirs: str | list[Path] = None, |
| 141 | + verbose: bool = False, flavor: str = ""): |
| 142 | + """ |
| 143 | + Make a WinPython distribution for a given set of parameters: |
| 144 | + Args: |
| 145 | + build_number: build number [int] |
| 146 | + release_level: release level (e.g. 'beta1', '') [str] |
| 147 | + basedir_wpy: top directory of the build (c:\...\Wpy...) |
| 148 | + verbose: Enable verbose output (bool). |
| 149 | + flavor: WinPython flavor (str). |
| 150 | + source_dirs: the python.zip |
| 151 | + toolsdirs: Directory with development tools r'D:\WinPython\basedir34\t.Slim' |
| 152 | + """ |
| 153 | + assert basedir_wpy is not None, "The *winpython_dirname* directory must be specified" |
| 154 | + |
| 155 | + tools_directories = [Path(d) for d in parse_list_argument(toolsdirs, ",")] |
| 156 | + utils.print_box(f"Making WinPython at {basedir_wpy}") |
| 157 | + os.makedirs(basedir_wpy, exist_ok=True) |
| 158 | + |
| 159 | + builder = WinPythonDistributionBuilder( |
| 160 | + build_number, release_level, Path(basedir_wpy), |
| 161 | + verbose=verbose, flavor=flavor, |
| 162 | + source_dirs=source_dirs, tools_directories=tools_directories) |
| 163 | + builder.build() |
| 164 | + |
| 165 | +if __name__ == "__main__": |
| 166 | + make_all( |
| 167 | + build_number=1, |
| 168 | + release_level="b3", |
| 169 | + basedir_wpy=r"D:\WinPython\bd314\budot\WPy64-31401b3", |
| 170 | + verbose=True, |
| 171 | + flavor="dot", |
| 172 | + source_dirs=r"D:\WinPython\bd314\packages.win-amd64", |
| 173 | + toolsdirs=r"D:\WinPython\bd314\t.Slim", |
| 174 | + ) |
0 commit comments