forked from winpython/winpython
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake.py
More file actions
361 lines (313 loc) · 18.6 KB
/
make.py
File metadata and controls
361 lines (313 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# -*- coding: utf-8 -*-
#
# WinPython build script
# Copyright © 2012 Pierre Raybaut
# Copyright © 2014-2025+ The Winpython development team https://github.com/winpython/
# Licensed under the terms of the MIT License
# (see winpython/__init__.py for details)
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from winpython import wppm, utils
# Local import
import diff
# Define constant paths for clarity
CHANGELOGS_DIRECTORY = Path(__file__).parent / "changelogs"
PORTABLE_DIRECTORY = Path(__file__).parent / "portable"
NODEJS_RELATIVE_PATH = "n" # Relative path within WinPython dir
# Ensure necessary directories exist at the start
assert CHANGELOGS_DIRECTORY.is_dir(), f"Changelogs directory not found: {CHANGELOGS_DIRECTORY}"
assert PORTABLE_DIRECTORY.is_dir(), f"Portable directory not found: {PORTABLE_DIRECTORY}"
def find_7zip_executable() -> str:
"""Locates the 7-Zip executable (7z.exe)."""
possible_program_files = [r"C:\Program Files", r"C:\Program Files (x86)", Path(sys.prefix).parent / "t"]
for base_dir in possible_program_files:
if (executable_path := Path(base_dir) / "7-Zip" / "7z.exe").is_file():
return str(executable_path)
raise RuntimeError("7ZIP is not installed on this computer.")
def copy_items(source_directories: list[Path], target_directory: Path, verbose: bool = False):
"""Copies items from source directories to the target directory."""
target_directory.mkdir(parents=True, exist_ok=True)
for source_dir in source_directories:
if not source_dir.is_dir():
print(f"Warning: Source directory not found: {source_dir}")
continue
for source_item in source_dir.iterdir():
target_item = target_directory / source_item.name
copy_function = shutil.copytree if source_item.is_dir() else shutil.copy2
try:
copy_function(source_item, target_item)
if verbose:
print(f"Copied: {source_item} -> {target_item}")
except Exception as e:
print(f"Error copying {source_item} to {target_item}: {e}")
def parse_list_argument(argument_value: str | list[str], separator=" ") -> list[str]:
"""Parse a separated list argument into a list of strings."""
if not argument_value:
return []
return argument_value.split(separator) if isinstance(argument_value, str) else list(argument_value)
class WinPythonDistributionBuilder:
"""Builds a WinPython distribution."""
def __init__(self, build_number: int, release_level: str, target_directory: Path, wheels_directory: Path,
tools_directories: list[Path] = None, documentation_directories: list[Path] = None, verbose: bool = False,
base_directory: Path = None, install_options: list[str] = None, flavor: str = ""):
"""
Initializes the WinPythonDistributionBuilder.
Args:
build_number: The build number (integer).
release_level: The release level (e.g., "beta", "").
target_directory: The base directory where WinPython will be created.
wheels_directory: Directory containing wheel files for packages.
tools_directories: List of directories containing development tools to include.
documentation_directories: List of directories containing documentation to include.
verbose: Enable verbose output.
base_directory: Base directory for building (optional, for relative paths).
install_options: Additional pip install options.
flavor: WinPython flavor (e.g., "Barebone").
"""
self.build_number = build_number
self.release_level = release_level
self.target_directory = Path(target_directory)
self.wheels_directory = Path(wheels_directory)
self.tools_directories = tools_directories or []
self.documentation_directories = documentation_directories or []
self.verbose = verbose
self.winpython_directory: Path | None = None
self.distribution: wppm.Distribution | None = None
self.base_directory = base_directory
self.install_options = install_options or []
self.flavor = flavor
self.python_zip_file: Path = self._get_python_zip_file()
self.python_name = self.python_zip_file.stem
self.python_directory_name = "python"
def _get_python_zip_file(self) -> Path:
"""Finds the Python .zip file in the wheels directory."""
for source_item in self.wheels_directory.iterdir():
if re.match(r"(pypy3|python-)([0-9]|[a-zA-Z]|.)*.zip", source_item.name):
return source_item
raise RuntimeError(f"Could not find Python zip package in {self.wheels_directory}")
@property
def package_index_markdown(self) -> str:
"""Generates a Markdown formatted package index page."""
return f"""## WinPython {self.winpyver2 + self.flavor}
The following packages are included in WinPython-{self.architecture_bits}bit v{self.winpyver2 + self.flavor} {self.release_level}.
<details>
### Tools
Name | Version | Description
-----|---------|------------
{utils.get_installed_tools_markdown(utils.get_python_executable(self.python_executable_directory))}
### Python packages
Name | Version | Description
-----|---------|------------
{self.distribution.get_installed_packages_markdown()}
### WheelHouse packages
Name | Version | Description
-----|---------|------------
{self.distribution.get_wheelhouse_packages_markdown()}
</details>
"""
@property
def winpython_version_name(self) -> str:
"""Returns the full WinPython version string."""
return f"{self.python_full_version}.{self.build_number}{self.flavor}{self.release_level}"
@property
def python_full_version(self) -> str:
"""Retrieves the Python full version string from the distribution."""
return utils.get_python_long_version(self.distribution.target) if self.distribution else "0.0.0"
@property
def python_executable_directory(self) -> str:
"""Returns the directory containing the Python executable."""
if self.winpython_directory:
python_path_directory = self.winpython_directory / self.python_directory_name
return str(python_path_directory) if python_path_directory.is_dir() else str(self.winpython_directory / self.python_name)
return ""
@property
def architecture_bits(self) -> int:
"""Returns the architecture (32 or 64 bits) of the distribution."""
return self.distribution.architecture if self.distribution else 64
def create_installer_7zip(self, installer_type: str = "exe", compression= "mx5"):
"""Creates a WinPython installer using 7-Zip: "exe", "7z", "zip")"""
self._print_action(f"Creating WinPython installer ({installer_type})")
if installer_type not in ["exe", "7z", "zip"]:
return
DISTDIR = self.winpython_directory
filename_stem = f"Winpython{self.architecture_bits}-{self.python_full_version}.{self.build_number}{self.flavor}{self.release_level}"
fullfilename = DISTDIR.parent / (filename_stem + "." + installer_type)
sfx_option = "-sfx7z.sfx" if installer_type == "exe" else ""
zip_option = "-tzip" if installer_type == "zip" else ""
compress_level = "mx5" if compression == "" else compression
command = f'"{find_7zip_executable()}" {zip_option} -{compress_level} a "{fullfilename}" "{DISTDIR}" {sfx_option}'
print(f'Executing 7-Zip script: "{command}"')
try:
subprocess.run(command, shell=True, check=True, stderr=sys.stderr, stdout=sys.stderr)
except subprocess.CalledProcessError as e:
print(f"Error executing 7-Zip script: {e}", file=sys.stderr)
def _print_action(self, text: str):
"""Prints an action message with progress indicator."""
if self.verbose:
utils.print_box(text)
else:
print(f"{text}... ", end="", flush=True)
def _extract_python_archive(self):
"""Extracts the Python zip archive to create the base Python environment."""
self._print_action("Extracting Python archive")
utils.extract_archive(self.python_zip_file, self.winpython_directory)
# Relocate to /python subfolder if needed (for newer structure) #2024-12-22 to /python
expected_python_directory = self.winpython_directory / self.python_directory_name
if self.python_directory_name != self.python_name and not expected_python_directory.is_dir():
os.rename(self.winpython_directory / self.python_name, expected_python_directory)
def _copy_essential_files(self):
"""Copies pre-made objects"""
self._print_action("Copying default scripts")
copy_items([PORTABLE_DIRECTORY / "scripts"], self.winpython_directory / "scripts", self.verbose)
self._print_action("Copying launchers")
copy_items([PORTABLE_DIRECTORY / "launchers_final"], self.winpython_directory, self.verbose)
docs_target_directory = self.winpython_directory / "notebooks" / "docs"
self._print_action(f"Copying documentation to {docs_target_directory}")
copy_items(self.documentation_directories, docs_target_directory, self.verbose)
tools_target_directory = self.winpython_directory / "t"
self._print_action(f"Copying tools to {tools_target_directory}")
copy_items(self.tools_directories, tools_target_directory, self.verbose)
if (nodejs_current_directory := tools_target_directory / "n").is_dir():
self._print_action(f"Moving tools from {nodejs_current_directory} to {tools_target_directory.parent / NODEJS_RELATIVE_PATH}")
try:
shutil.move(nodejs_current_directory, tools_target_directory.parent / NODEJS_RELATIVE_PATH)
except Exception as e:
print(f"Error moving Node.js directory: {e}")
def _create_initial_batch_scripts(self):
"""Creates initial batch scripts, including environment setup."""
self._print_action("Creating initial batch scripts")
# Replacements for batch scripts (PyPy compatibility)
executable_name = self.distribution.short_exe if self.distribution else "python.exe" # default to python.exe if distribution is not yet set
init_variables = [('WINPYthon_exe', executable_name), ('WINPYthon_subdirectory_name', self.python_directory_name), ('WINPYVER', self.winpython_version_name)]
init_variables += [('WINPYVER2', f"{self.python_full_version}.{self.build_number}"), ('WINPYFLAVOR', self.flavor), ('WINPYARCH', self.architecture_bits)]
with open(self.winpython_directory / "scripts" / "env.ini", "w") as f:
f.writelines([f'{a}={b}\n' for a, b in init_variables])
def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirname: str = None):
"""Make or finalise WinPython distribution in the target directory"""
print(f"Building WinPython with Python archive: {self.python_zip_file.name}")
if winpy_dirname is None:
raise RuntimeError("WinPython base directory to create is undefined")
self.winpython_directory = self.target_directory / winpy_dirname
if rebuild:
self._print_action(f"Creating WinPython {self.winpython_directory} base directory")
if self.winpython_directory.is_dir():
shutil.rmtree(self.winpython_directory)
os.makedirs(self.winpython_directory, exist_ok=True)
# preventive re-Creation of settings directory
(self.winpython_directory / "settings" / "AppData" / "Roaming").mkdir(parents=True, exist_ok=True)
self._extract_python_archive()
self.distribution = wppm.Distribution(self.python_executable_directory, verbose=self.verbose)
if rebuild:
self._copy_essential_files()
self._create_initial_batch_scripts()
utils.python_execmodule("ensurepip", self.distribution.target)
self.distribution.patch_standard_packages("pip")
essential_packages = ["pip", "setuptools", "wheel", "winpython"]
for package_name in essential_packages:
actions = ["install", "--upgrade", "--pre", package_name] + self.install_options
self._print_action(f"Piping: {' '.join(actions)}")
self.distribution.do_pip_action(actions)
self.distribution.patch_standard_packages(package_name)
if requirements_files_list:
for req in requirements_files_list:
actions = ["install", "-r", req] + (self.install_options or [])
self._print_action(f"Piping: {' '.join(actions)}")
self.distribution.do_pip_action(actions)
self.distribution.patch_standard_packages()
self._print_action("Writing package index")
self.winpyver2 = f"{self.python_full_version}.{self.build_number}"
output_markdown_filename = str(self.winpython_directory.parent / f"WinPython{self.flavor}-{self.distribution.architecture}bit-{self.winpyver2}.md")
with open(output_markdown_filename, "w", encoding='utf-8') as f:
f.write(self.package_index_markdown)
self._print_action("Writing changelog")
shutil.copyfile(output_markdown_filename, str(Path(CHANGELOGS_DIRECTORY) / Path(output_markdown_filename).name))
diff.write_changelog(self.winpyver2, None, self.base_directory, self.flavor, self.distribution.architecture)
def rebuild_winpython_package(source_directory: Path, target_directory: Path, architecture: int = 64, verbose: bool = False):
"""Rebuilds the winpython package from source using flit."""
for file in target_directory.glob("winpython-*"):
if file.suffix in (".exe", ".whl", ".gz"):
file.unlink()
utils.buildflit_wininst(source_directory, copy_to=target_directory, verbose=True)
def make_all(build_number: int, release_level: str, pyver: str, architecture: int, basedir: Path,
verbose: bool = False, rebuild: bool = True, create_installer: str = "True", install_options=["--no-index"],
flavor: str = "", requirements: str | list[Path] = None, find_links: str | list[Path] = None,
source_dirs: Path = None, toolsdirs: str | list[Path] = None, docsdirs: str | list[Path] = None,
python_target_release: str = None, # e.g. "37101" for 3.7.10
):
"""
Make a WinPython distribution for a given set of parameters:
Args:
build_number: build number [int]
release_level: release level (e.g. 'beta1', '') [str]
pyver: python version ('3.4' or 3.5')
architecture: [int] (32 or 64)
basedir: where to create the build (r'D:\Winpython\basedir34')
verbose: Enable verbose output (bool).
rebuild: Whether to rebuild the distribution (bool).
create_installer: Type of installer to create (str).
install_options: pip options (r'--no-index --pre --trusted-host=None')
flavor: WinPython flavor (str).
requirements: package lists for pip (r'D:\requirements.txt')
find_links: package directories (r'D:\Winpython\packages.srcreq')
source_dirs: the python.zip + rebuilt winpython wheel package directory
toolsdirs: Directory with development tools r'D:\WinPython\basedir34\t.Slim'
docsdirs: Directory with documentation r'D:\WinPython\basedir34\docs.Slim'
python_target_release: Target Python release (str).
"""
assert basedir is not None, "The *basedir* directory must be specified"
assert architecture in (32, 64)
tools_dirs_list = parse_list_argument(toolsdirs, ",")
docs_dirs_list = parse_list_argument(docsdirs, ",")
install_options_list = parse_list_argument(install_options, " ")
find_links_dirs_list = parse_list_argument(find_links, ",")
requirements_files_list = [Path(f) for f in parse_list_argument(requirements, ",") if f]
find_links_options = [f"--find-links={link}" for link in find_links_dirs_list + [source_dirs]]
build_directory = Path(basedir) / ("bu" + flavor)
if rebuild:
utils.print_box(f"Making WinPython {architecture}bits at {Path(basedir) / ('bu' + flavor)}")
os.makedirs(build_directory, exist_ok=True)
# use source_dirs as the directory to re-build Winpython wheel
winpython_source_dir = Path(__file__).resolve().parent
rebuild_winpython_package(winpython_source_dir, Path(source_dirs), architecture, verbose)
builder = WinPythonDistributionBuilder(
build_number, release_level, build_directory, wheels_directory=source_dirs,
tools_directories=[Path(d) for d in tools_dirs_list],
documentation_directories=[Path(d) for d in docs_dirs_list],
verbose=verbose, base_directory=basedir,
install_options=install_options_list + find_links_options,
flavor=flavor
)
# define the directory where to create the distro
python_minor_version_str = "".join(builder.python_name.replace(".amd64", "").split(".")[-2:-1])
while not python_minor_version_str.isdigit() and len(python_minor_version_str) > 0:
python_minor_version_str = python_minor_version_str[:-1]
# simplify for PyPy
if python_target_release is not None:
winpython_dirname = f"WPy{architecture}-{python_target_release}{build_number}{release_level}"
else:
winpython_dirname = f"WPy{architecture}-{pyver.replace('.', '')}{python_minor_version_str}{build_number}{release_level}"
builder.build(rebuild=rebuild, requirements_files_list=requirements_files_list, winpy_dirname=winpython_dirname)
for commmand in create_installer.lower().replace("7zip",".exe").split('.'):
installer_type, compression = (commmand + "-").split("-")[:2]
builder.create_installer_7zip(installer_type, compression)
if __name__ == "__main__":
# DO create only one Winpython distribution at a time
make_all(
build_number=1,
release_level="build3",
pyver="3.4",
basedir=r"D:\Winpython\basedir34",
verbose=True,
architecture=64,
flavor="Barebone",
requirements=r"D:\Winpython\basedir34\barebone_requirements.txt",
install_options=r"--no-index --pre --trusted-host=None",
find_links=r"D:\Winpython\packages.srcreq",
source_dirs=r"D:\WinPython\basedir34\packages.win-amd64",
toolsdirs=r"D:\WinPython\basedir34\t.Slim",
docsdirs=r"D:\WinPython\basedir34\docs.Slim",
)