From ecb7d4d5d49c78832a1824ccd247193d9c84fdbe Mon Sep 17 00:00:00 2001 From: Neal Joslin Date: Thu, 20 Jun 2024 23:10:17 -0400 Subject: [PATCH] Extended poetry include with plugin settings --- README.md | 85 +++++++++++++++++++++++++++++ poetry_pyinstaller_plugin/plugin.py | 67 ++++++++++++++++++++--- pyproject.toml | 2 +- 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 04c8af6..d9c815f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Are listed in this sections all options available to configure `poetry-pyinstall * `version` (string) * Version of PyInstaller to use during build * Does not support version constraint + * `exclude-include` (boolean) + * Exclude poetry include. Default: `False` * `scripts` (dictionary) * Where key is the program name and value a path to script or a `PyInstallerTarget` spec * Example: `prog-name = "my_package/script.py"` @@ -40,6 +42,10 @@ Are listed in this sections all options available to configure `poetry-pyinstall * `all` (list): Collect all submodules, data files, and binaries for specified package(s) or module(s) * `copy-metadata` (list) : list of packages for which metadata must be copied * `recursive-copy-metadata` (list) : list of packages for which metadata must be copied (including dependencies) + * `include` (dictionary) : + * Data file(s) to include. `{source: destination}` + * `package` (dictionary) : + * File(s) to include with executable. `{source: destination}` `PyinstallerTarget` spec: * `source` (string): Path to your program entrypoint @@ -174,3 +180,82 @@ $ pip install my-package[with-deps] ``` Bundled binaries must be built with all dependencies installed in build environment. + +## Packaging additional files + +This plugin by default supports `tool.poetry.include`, but it can be disabled +for more control. You can also add files *next* to the executable by using the +`package` setting + +### Example +```toml +[tool.poetry] +name = "my_project" +include = [ + { path = "README.md", format = ["sdist"] }, +] + +[tool.poetry-pyinstaller-plugin] +# Disable [tool.poetry.include] and use plugin settings instead +exclude-include = true + +[tool.poetry-pyinstaller-plugin.scripts] +hello-world = "my_package/main.py" + +[tool.poetry-pyinstaller-plugin.package] +# 1-1 next to executable +"README.md" = "." + +# renaming next to executable +"user/README.md" = "USER_README.md" + +# directory next to executable +"docs" = "." + +[tool.poetry-pyinstaller-plugin.include] +# loose files in bundle +"icons/*" = "." + +# entire directory in bundle +"images/*" = "element_images" +``` + +Expected directory structure: +```text +. +├── build ...................... PyInstaller intermediate build directory +├── dist ....................... Result of `poetry build` command +│ ├── pyinstaller ............. PyInstaller output +│ │ ├── .specs/ ............ Specs files +│ │ └── hello-world/ +│ │ ├── docs/ ............ Packaged Docs +│ │ │ ├── how_to.md +│ │ │ └── if_breaks.md +│ │ ├── my_project_internal/ ............ Onedir bundle +│ │ │ ├── main_icon.svg ............... Included icon +│ │ │ ├── extra_icon.svg .............. Included icon +│ │ │ └── element_images/ ............. Included images +│ │ │ ├── footer_icon.svg +│ │ │ └── header_icon.svg +│ │ ├── my_project.exe ............ Bundled program +│ │ ├── README.md ................. Packaged Readme +│ │ └── USER_README.md. ........... Packaged User Readme +│ ├── my_package-0.0.0-py3-none-manylinux_2_35_x86_64.whl ... Wheel with bundled binaries +│ └── my_package-0.0.0.tar.gz ............................... Source archive, includes README.md +├── docs/ +│ ├── how_to.md +│ └── if_breaks.md +├── user/ +│ └── README.md +├── icons/ +│ ├── main_icon.svg +│ └── extra_icon.svg +├── images/ +│ ├── footer_icon.md +│ └── header_icon.svg +├── pyproject.toml +├── README.py +└── my_package + ├── __init__.py + └── main.py +``` diff --git a/poetry_pyinstaller_plugin/plugin.py b/poetry_pyinstaller_plugin/plugin.py index 96d1fc4..8a079e2 100644 --- a/poetry_pyinstaller_plugin/plugin.py +++ b/poetry_pyinstaller_plugin/plugin.py @@ -31,6 +31,9 @@ from importlib import reload from pathlib import Path from typing import List, Dict, Optional +from shutil import copytree, copy +from errno import ENOTDIR, EINVAL + # Reload logging after PyInstaller import (conflicts with poetry logging) reload(logging) @@ -110,10 +113,12 @@ def _validate_type(self, type: str): def build(self, venv: VirtualEnv, platform: str, - include_config: List, collect_config: Dict, copy_metadata: List, - recursive_copy_metadata: List + recursive_copy_metadata: List, + poetry_include_config: List, + include_config: Dict, + package_config: Dict ): self._platform = platform work_path = Path("build", platform) @@ -142,11 +147,17 @@ def build(self, args += collect_args - if include_config: + + if include_config or poetry_include_config: include_args = [] sep = ";" if "win" in platform else ":" - for item in include_config: + for source, target in include_config.items(): + if source and target: + include_args.append("--add-data") + include_args.append(f"{Path(source).resolve()}{sep}{target}") + + for item in poetry_include_config: path = item if isinstance(item, str) else item.get("path") if path: include_args.append("--add-data") @@ -194,6 +205,20 @@ def build(self, venv.run(str(Path(venv.script_dirs[0]) / "pyinstaller"), *args) + if package_config: + package_path = Path("dist", "pyinstaller", platform, self.prog) + for source, target in package_config.items(): + destination = Path(package_path / (target if target != "." else source)) + try: + copytree(source, destination) + except OSError as exc: # python >2.5 or is file + if exc.errno in (ENOTDIR, EINVAL, EEXIST): + copy(source, destination) + else: + raise + + + def bundle_wheel(self, io): wheels = glob.glob("*-py3-none-any.whl", root_dir="dist") for wheel in wheels: @@ -256,13 +281,35 @@ def collect_opt_block(self) -> Dict: raise RuntimeError("Error while retrieving pyproject.toml data.") @property - def include_opt_block(self) -> List: + def poetry_include_opt_block(self) -> List: """ - Get include config + Get poetry include config """ data = self._app.poetry.pyproject.data if data: - return data.get("tool", {}).get("poetry", {}).get("include", []) + if not data.get("tool", {}).get("poetry-pyinstaller-plugin", {}).get("exclude-include", False): + return data.get("tool", {}).get("poetry", {}).get("include", []) + return [] + raise RuntimeError("Error while retrieving pyproject.toml data.") + + @property + def include_opt_block(self) -> Dict: + """ + Get pyinstaller include config + """ + data = self._app.poetry.pyproject.data + if data: + return data.get("tool", {}).get("poetry-pyinstaller-plugin", {}).get("include", {}) + raise RuntimeError("Error while retrieving pyproject.toml data.") + + @property + def package_opt_block(self) -> Dict: + """ + Get package config + """ + data = self._app.poetry.pyproject.data + if data: + return data.get("tool", {}).get("poetry-pyinstaller-plugin", {}).get("package", {}) raise RuntimeError("Error while retrieving pyproject.toml data.") @property @@ -426,10 +473,12 @@ def _build_binaries(self, event: ConsoleCommandEvent, event_name: str, dispatche for t in self._targets: io.write_line(f" - Building {t.prog} {t.type.name}{' BUNDLED' if t.bundled else ''}{' NOUPX' if t.noupx else ''}") t.build(venv=venv, platform=platform, - include_config=self.include_opt_block, collect_config=self.collect_opt_block, copy_metadata=self.copy_metadata_opt_block, - recursive_copy_metadata=self.recursive_copy_metadata_opt_block + recursive_copy_metadata=self.recursive_copy_metadata_opt_block, + poetry_include_config=self.poetry_include_opt_block, + include_config=self.include_opt_block, + package_config=self.package_opt_block, ) io.write_line(f" - Built {t.prog} -> '{Path('dist', 'pyinstaller', platform, t.prog)}'") diff --git a/pyproject.toml b/pyproject.toml index abd572b..0d94107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-pyinstaller-plugin" -version = "1.1.11" +version = "1.1.12" description = "Poetry plugin to build and/or bundle executable binaries with PyInstaller" authors = ["Thomas Mahé "] license = "MIT"