Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 55 additions & 108 deletions make.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,18 @@
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 = [
Path(r"C:\Program Files"),
Path(r"C:\Program Files (x86)"),
Path(sys.prefix).parent / "t" ,
Path(sys.prefix).parent / "t",
]
for base_dir in possible_program_files:
executable_path = base_dir / "7-Zip" / "7z.exe"
if executable_path.is_file():
if (executable_path := base_dir / "7-Zip" / "7z.exe").is_file():
return str(executable_path)
raise RuntimeError("7ZIP is not installed on this computer.")


def replace_lines_in_file(filepath: Path, replacements: list[tuple[str, str]]):
"""
Replaces lines in a file that start with a given prefix.
Expand Down Expand Up @@ -99,10 +96,8 @@ def build_installer_7zip(script_template_path: Path, output_script_path: Path, r
except subprocess.CalledProcessError as e:
print(f"Error executing 7-Zip script: {e}", file=sys.stderr)


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():
Expand All @@ -119,7 +114,6 @@ def _copy_items(source_directories: list[Path], target_directory: Path, verbose:
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 argument_value is None:
Expand All @@ -128,25 +122,14 @@ def _parse_list_argument(argument_value: str | list[str], separator=" ") -> list
return argument_value.split(separator)
return list(argument_value)


class WinPythonDistributionBuilder:
"""Builds a WinPython distribution."""

NODEJS_RELATIVE_PATH = r"\n" # Relative path within WinPython dir

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 = "",
):
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.

Expand All @@ -164,25 +147,24 @@ def __init__(
"""
self.build_number = build_number
self.release_level = release_level
self.target_directory = Path(target_directory) # Ensure Path object
self.wheels_directory = Path(wheels_directory) # Ensure Path object
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 # Will be set during build
self.distribution: wppm.Distribution | None = None # Will be set during build
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 # Filename without extension
self.python_directory_name = "python" # Standardized Python directory name
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."""
pattern = r"(pypy3|python-)([0-9]|[a-zA-Z]|.)*.zip"
for filename in os.listdir(self.wheels_directory):
if re.match(pattern, filename):
if re.match("(pypy3|python-)([0-9]|[a-zA-Z]|.)*.zip", filename):
return self.wheels_directory / filename
raise RuntimeError(f"Could not find Python zip package in {self.wheels_directory}")

Expand Down Expand Up @@ -265,10 +247,7 @@ def winpython_version_name(self) -> str:

@property
def python_full_version(self) -> str:
"""
Retrieves the Python full version string from the distribution.
Will be set after _extract_python is called and distribution is initialized.
"""
"""Retrieves the Python full version string from the distribution."""
if self.distribution is None:
return "0.0.0" # Placeholder before initialization
return utils.get_python_long_version(self.distribution.target)
Expand All @@ -280,15 +259,15 @@ def python_executable_directory(self) -> str:
if python_path_directory and python_path_directory.is_dir():
return str(python_path_directory)
else:
python_path_executable = self.winpython_directory / self.python_name if self.winpython_directory else None # Fallback for older structure
python_path_executable = self.winpython_directory / self.python_name if self.winpython_directory else None
return str(python_path_executable) if python_path_executable else ""

@property
def architecture_bits(self) -> int:
"""Returns the architecture (32 or 64 bits) of the distribution."""
if self.distribution:
return self.distribution.architecture
return 64 # Default to 64 if distribution is not initialized yet
return 64

@property
def pre_path_entries(self) -> list[str]:
Expand All @@ -302,23 +281,11 @@ def pre_path_entries(self) -> list[str]:
r".." + self.NODEJS_RELATIVE_PATH,
]

@property
def documentation_directories_list(self) -> list[Path]:
"""Returns the list of documentation directories to include."""
default_docs_directory = Path(__file__).resolve().parent / "docs"
if default_docs_directory.is_dir():
return [default_docs_directory] + self.documentation_directories
return self.documentation_directories

def create_installer_7zip(self, installer_type: str = ".exe"):
"""
Creates a WinPython installer using 7-Zip.

Args: installer_type: Type of installer to create (".exe", ".7z", ".zip").
"""
"""Creates a WinPython installer using 7-Zip: ".exe", ".7z", ".zip")"""
self._print_action(f"Creating WinPython installer ({installer_type})")
template_name = "installer_7zip.bat"
output_name = "installer_7zip-tmp.bat" # temp file to avoid overwriting template
output_name = "installer_7zip-tmp.bat"
if installer_type not in [".exe", ".7z", ".zip"]:
print(f"Warning: Unsupported installer type '{installer_type}'. Defaulting to .exe")
installer_type = ".exe"
Expand All @@ -329,14 +296,10 @@ def create_installer_7zip(self, installer_type: str = ".exe"):
("VERSION", f"{self.python_full_version}.{self.build_number}{self.flavor}"),
("VERSION_INSTALL", f'{self.python_full_version.replace(".", "")}{self.build_number}'),
("RELEASELEVEL", self.release_level),
("INSTALLER_OPTION", installer_type), # Pass installer type as option to bat script
("INSTALLER_OPTION", installer_type),
]

build_installer_7zip(
PORTABLE_DIRECTORY / template_name,
PORTABLE_DIRECTORY / output_name,
replacements
)
build_installer_7zip(PORTABLE_DIRECTORY / template_name, PORTABLE_DIRECTORY / output_name, replacements)

def _print_action(self, text: str):
"""Prints an action message with progress indicator."""
Expand All @@ -348,15 +311,12 @@ def _print_action(self, text: str):
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(
str(self.python_zip_file),
targetdir=str(self.winpython_directory), # Extract directly to winpython_directory
)
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")
Expand All @@ -367,18 +327,16 @@ def _copy_essential_files(self):

docs_target_directory = self.winpython_directory / "notebooks" / "docs"
self._print_action(f"Copying documentation to {docs_target_directory}")
_copy_items(self.documentation_directories_list, docs_target_directory, self.verbose)
_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, self.winpython_directory / "t", self.verbose)
_copy_items(self.tools_directories, tools_target_directory, self.verbose)

# Special handling for Node.js to move it up one level
nodejs_current_directory = tools_target_directory / "n"
nodejs_target_directory = self.winpython_directory / self.NODEJS_RELATIVE_PATH
if nodejs_current_directory != nodejs_target_directory and nodejs_current_directory.is_dir():
if (nodejs_current_directory := tools_target_directory / "n").is_dir():
try:
shutil.move(nodejs_current_directory, nodejs_target_directory)
shutil.move(nodejs_current_directory, self.winpython_directory / self.NODEJS_RELATIVE_PATH)
except Exception as e:
print(f"Error moving Node.js directory: {e}")

Expand Down Expand Up @@ -407,14 +365,13 @@ def _create_initial_batch_scripts(self):

def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirname: str = None):
"""Make or finalise WinPython distribution in the target directory"""

python_zip_filename = self.python_zip_file.name
print(f"Building WinPython with Python archive: {python_zip_filename}")
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")
else:
self.winpython_directory = self.target_directory / winpy_dirname # Create/re-create the WinPython base directory

if rebuild:
self._print_action(f"Creating WinPython {self.winpython_directory} base directory")
if self.winpython_directory.is_dir():
Expand Down Expand Up @@ -475,37 +432,32 @@ def rebuild_winpython_package(source_directory: Path, target_directory: Path, ar
utils.buildflit_wininst(source_directory, copy_to=target_directory, verbose=verbose)


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
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:
`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')
`requirements`: package lists for pip (r'D:\requirements.txt')
`install_options`: pip options (r'--no-index --pre --trusted-host=None')
`find_links`: package directories (r'D:\Winpython\packages.srcreq')
`source_dirs`: the python.zip + rebuilt winpython wheel package directory
`toolsdirs`: r'D:\WinPython\basedir34\t.Slim'
`docsdirs`: r'D:\WinPython\basedir34\docs.Slim'"""

"""
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)

Expand All @@ -519,24 +471,19 @@ def make_all(
build_directory = str(Path(basedir) / ("bu" + flavor))

if rebuild:
# Rebuild Winpython Wheel Package
utils.print_box(f"Making WinPython {architecture}bits at {Path(basedir) / ('bu' + flavor)}")
os.makedirs(Path(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, source_dirs, architecture, verbose)

builder = WinPythonDistributionBuilder(
build_number,
release_level,
build_directory,
wheels_directory=source_dirs,
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,
verbose=verbose, base_directory=basedir,
install_options=install_options_list + find_links_options,
flavor=flavor,
flavor=flavor
)
# define the directory where to create the distro
python_minor_version_str = "".join(builder.python_name.replace(".amd64", "").split(".")[-2:-1])
Expand Down