ksp-builder 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ksp_builder/__init__.py +222 -0
- ksp_builder/_build.py +250 -0
- ksp_builder/_compile_kv.py +158 -0
- ksp_builder/_cythonize.py +204 -0
- ksp_builder/_gradle.py +102 -0
- ksp_builder/_java.py +16 -0
- ksp_builder/_kivy_school.py +43 -0
- ksp_builder/_packages.py +225 -0
- ksp_builder/_pre_post_build.py +128 -0
- ksp_builder-0.0.0.dist-info/METADATA +264 -0
- ksp_builder-0.0.0.dist-info/RECORD +14 -0
- ksp_builder-0.0.0.dist-info/WHEEL +5 -0
- ksp_builder-0.0.0.dist-info/licenses/LICENSE +21 -0
- ksp_builder-0.0.0.dist-info/top_level.txt +1 -0
ksp_builder/__init__.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""ksp_builder — PEP 517 build backend for KSProject-based packages.
|
|
2
|
+
|
|
3
|
+
Wraps setuptools and combines functionality from pyjnius-builder and
|
|
4
|
+
pyswiftkit-builder into a single backend. User-supplied ``before_build`` /
|
|
5
|
+
``after_build`` scripts configured under ``[tool.ksp-builder]`` bracket every
|
|
6
|
+
build, package sources can be compiled with Cython (``cythonize`` /
|
|
7
|
+
``py_to_pyx``, replacing the old cythonized-app setup.py), Kivy ``.kv`` files
|
|
8
|
+
can be compiled into the modules beside them (``compile_kv``), and three
|
|
9
|
+
optional injection steps run after the base setuptools wheel/sdist is
|
|
10
|
+
produced:
|
|
11
|
+
|
|
12
|
+
1. **Java sources** — if ``[tool.pyjnius]`` is present, Java files from the
|
|
13
|
+
configured ``java-paths`` are injected under ``.java/`` (same convention
|
|
14
|
+
as pyjnius-builder).
|
|
15
|
+
|
|
16
|
+
2. **Swift artifacts** — if ``[tool.pyswiftkit]`` is present *and*
|
|
17
|
+
``pyswiftkit_builder`` is installed, ``swift build`` is executed and the
|
|
18
|
+
compiled ``.so`` / ``.dylib`` files are injected into the wheel.
|
|
19
|
+
|
|
20
|
+
3. **Android gradle config** — if ``[tool.kivy-school.android]`` is present,
|
|
21
|
+
a ``.gradle/<package_name>.json`` file containing ``gradle_dependencies``,
|
|
22
|
+
``permissions``, and ``meta_data`` is injected into the wheel/sdist.
|
|
23
|
+
``ksproject`` can then discover and merge these JSON files from all
|
|
24
|
+
installed packages to assemble the final Gradle build configuration.
|
|
25
|
+
|
|
26
|
+
The backend's own section::
|
|
27
|
+
|
|
28
|
+
[tool.ksp-builder]
|
|
29
|
+
before_build = "path/to/before_build_script.py"
|
|
30
|
+
after_build = "path/to/after_build_script.py"
|
|
31
|
+
cythonize = true
|
|
32
|
+
py_to_pyx = true
|
|
33
|
+
compile_kv = true
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from setuptools import build_meta as _setuptools_backend
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"build_wheel",
|
|
43
|
+
"build_sdist",
|
|
44
|
+
"get_requires_for_build_wheel",
|
|
45
|
+
"get_requires_for_build_sdist",
|
|
46
|
+
"prepare_metadata_for_build_wheel",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Pass-through hooks that need no extra logic.
|
|
50
|
+
get_requires_for_build_wheel = _setuptools_backend.get_requires_for_build_wheel
|
|
51
|
+
get_requires_for_build_sdist = _setuptools_backend.get_requires_for_build_sdist
|
|
52
|
+
prepare_metadata_for_build_wheel = _setuptools_backend.prepare_metadata_for_build_wheel
|
|
53
|
+
|
|
54
|
+
_st_build_editable = getattr(_setuptools_backend, "build_editable", None)
|
|
55
|
+
_st_get_requires_for_build_editable = getattr(
|
|
56
|
+
_setuptools_backend, "get_requires_for_build_editable", None
|
|
57
|
+
)
|
|
58
|
+
_st_prepare_metadata_for_build_editable = getattr(
|
|
59
|
+
_setuptools_backend, "prepare_metadata_for_build_editable", None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if _st_get_requires_for_build_editable is not None:
|
|
63
|
+
get_requires_for_build_editable = _st_get_requires_for_build_editable
|
|
64
|
+
__all__.append("get_requires_for_build_editable")
|
|
65
|
+
|
|
66
|
+
if _st_prepare_metadata_for_build_editable is not None:
|
|
67
|
+
prepare_metadata_for_build_editable = _st_prepare_metadata_for_build_editable
|
|
68
|
+
__all__.append("prepare_metadata_for_build_editable")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Build hooks
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def build_wheel(
|
|
76
|
+
wheel_directory: str,
|
|
77
|
+
config_settings: dict | None = None,
|
|
78
|
+
metadata_directory: str | None = None,
|
|
79
|
+
) -> str:
|
|
80
|
+
project_dir = Path.cwd()
|
|
81
|
+
|
|
82
|
+
from ._pre_post_build import run_before_build, run_after_build
|
|
83
|
+
run_before_build(project_dir, "wheel")
|
|
84
|
+
|
|
85
|
+
# Swift build must happen before setuptools assembles the wheel so that
|
|
86
|
+
# the compiled .so/.dylib files can be picked up as package data.
|
|
87
|
+
swift_config = _load_swift_config(project_dir)
|
|
88
|
+
if swift_config is not None:
|
|
89
|
+
_run_swift_build(project_dir, swift_config)
|
|
90
|
+
_force_platform_wheel()
|
|
91
|
+
|
|
92
|
+
from ._build import ksp_build
|
|
93
|
+
with ksp_build(project_dir):
|
|
94
|
+
wheel_name = _setuptools_backend.build_wheel(
|
|
95
|
+
wheel_directory, config_settings, metadata_directory
|
|
96
|
+
)
|
|
97
|
+
wheel_path = Path(wheel_directory) / wheel_name
|
|
98
|
+
|
|
99
|
+
if swift_config is not None:
|
|
100
|
+
_inject_swift_artifacts(wheel_path, swift_config, project_dir)
|
|
101
|
+
|
|
102
|
+
from ._java import get_java_source_dirs, add_java_sources_to_wheel
|
|
103
|
+
java_dirs = get_java_source_dirs(
|
|
104
|
+
config_settings=config_settings, project_root=project_dir
|
|
105
|
+
)
|
|
106
|
+
add_java_sources_to_wheel(wheel_path, java_dirs)
|
|
107
|
+
|
|
108
|
+
from ._kivy_school import read_android_config
|
|
109
|
+
from ._gradle import inject_gradle_config_to_wheel
|
|
110
|
+
android_config = read_android_config(project_dir)
|
|
111
|
+
if android_config is not None:
|
|
112
|
+
inject_gradle_config_to_wheel(wheel_path, android_config)
|
|
113
|
+
|
|
114
|
+
run_after_build(project_dir, "wheel", wheel_path)
|
|
115
|
+
|
|
116
|
+
return wheel_name
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_sdist(sdist_directory: str, config_settings: dict | None = None) -> str:
|
|
120
|
+
project_dir = Path.cwd()
|
|
121
|
+
|
|
122
|
+
from ._pre_post_build import run_before_build, run_after_build
|
|
123
|
+
run_before_build(project_dir, "sdist")
|
|
124
|
+
|
|
125
|
+
sdist_name = _setuptools_backend.build_sdist(sdist_directory, config_settings)
|
|
126
|
+
sdist_path = Path(sdist_directory) / sdist_name
|
|
127
|
+
|
|
128
|
+
from ._java import get_java_source_dirs, add_java_sources_to_sdist
|
|
129
|
+
java_dirs = get_java_source_dirs(
|
|
130
|
+
config_settings=config_settings, project_root=project_dir
|
|
131
|
+
)
|
|
132
|
+
add_java_sources_to_sdist(sdist_path, java_dirs)
|
|
133
|
+
|
|
134
|
+
from ._kivy_school import read_android_config
|
|
135
|
+
from ._gradle import inject_gradle_config_to_sdist
|
|
136
|
+
android_config = read_android_config(project_dir)
|
|
137
|
+
if android_config is not None:
|
|
138
|
+
inject_gradle_config_to_sdist(sdist_path, android_config)
|
|
139
|
+
|
|
140
|
+
run_after_build(project_dir, "sdist", sdist_path)
|
|
141
|
+
|
|
142
|
+
return sdist_name
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if _st_build_editable is not None:
|
|
146
|
+
def build_editable(
|
|
147
|
+
wheel_directory: str,
|
|
148
|
+
config_settings: dict | None = None,
|
|
149
|
+
metadata_directory: str | None = None,
|
|
150
|
+
) -> str:
|
|
151
|
+
project_dir = Path.cwd()
|
|
152
|
+
|
|
153
|
+
from ._pre_post_build import run_before_build, run_after_build
|
|
154
|
+
run_before_build(project_dir, "editable")
|
|
155
|
+
|
|
156
|
+
swift_config = _load_swift_config(project_dir)
|
|
157
|
+
if swift_config is not None:
|
|
158
|
+
_run_swift_build(project_dir, swift_config)
|
|
159
|
+
_force_platform_wheel()
|
|
160
|
+
if _st_build_editable is None:
|
|
161
|
+
raise RuntimeError("Editable builds are not supported by this setuptools version.")
|
|
162
|
+
|
|
163
|
+
# Editable installs expose the source tree, so .py modules and .kv
|
|
164
|
+
# files are left alone; only real .pyx sources are compiled.
|
|
165
|
+
from ._build import ksp_build
|
|
166
|
+
with ksp_build(project_dir, editable=True):
|
|
167
|
+
wheel_name = _st_build_editable(
|
|
168
|
+
wheel_directory, config_settings, metadata_directory
|
|
169
|
+
)
|
|
170
|
+
wheel_path = Path(wheel_directory) / wheel_name
|
|
171
|
+
|
|
172
|
+
if swift_config is not None:
|
|
173
|
+
_inject_swift_artifacts(wheel_path, swift_config, project_dir)
|
|
174
|
+
|
|
175
|
+
from ._java import get_java_source_dirs, add_java_sources_to_wheel
|
|
176
|
+
java_dirs = get_java_source_dirs(
|
|
177
|
+
config_settings=config_settings, project_root=project_dir
|
|
178
|
+
)
|
|
179
|
+
add_java_sources_to_wheel(wheel_path, java_dirs)
|
|
180
|
+
|
|
181
|
+
from ._kivy_school import read_android_config
|
|
182
|
+
from ._gradle import inject_gradle_config_to_wheel
|
|
183
|
+
android_config = read_android_config(project_dir)
|
|
184
|
+
if android_config is not None:
|
|
185
|
+
inject_gradle_config_to_wheel(wheel_path, android_config)
|
|
186
|
+
|
|
187
|
+
run_after_build(project_dir, "editable", wheel_path)
|
|
188
|
+
|
|
189
|
+
return wheel_name
|
|
190
|
+
|
|
191
|
+
__all__.append("build_editable")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Swift helpers (pyswiftkit_builder is an optional runtime dependency)
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def _load_swift_config(project_dir: Path):
|
|
199
|
+
"""Return a PSKConfig if [tool.pyswiftkit] is configured, else None."""
|
|
200
|
+
try:
|
|
201
|
+
from pyswiftkit_builder._config import read_config # type: ignore[import-untyped]
|
|
202
|
+
return read_config(project_dir)
|
|
203
|
+
except ImportError:
|
|
204
|
+
return None
|
|
205
|
+
except (FileNotFoundError, KeyError, ValueError):
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _run_swift_build(project_dir: Path, config) -> None:
|
|
210
|
+
from pyswiftkit_builder._swift_build import build_swift # type: ignore[import-untyped]
|
|
211
|
+
build_swift(project_dir, config)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _inject_swift_artifacts(wheel_path: Path, config, project_dir: Path) -> None:
|
|
215
|
+
from pyswiftkit_builder._swift_build import inject_artifacts # type: ignore[import-untyped]
|
|
216
|
+
inject_artifacts(wheel_path, config, project_dir)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _force_platform_wheel() -> None:
|
|
220
|
+
"""Tell setuptools this is a binary wheel (has native extensions)."""
|
|
221
|
+
import setuptools
|
|
222
|
+
setuptools.Distribution.has_ext_modules = lambda self: True # type: ignore[method-assign]
|
ksp_builder/_build.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""ksp_builder._build — hooks the generated sources into the setuptools build.
|
|
2
|
+
|
|
3
|
+
``setuptools.build_meta`` runs ``from setuptools import setup; setup()`` when a
|
|
4
|
+
project has no setup.py, so there is no place to declare ``ext_modules`` or a
|
|
5
|
+
``cmdclass``. :func:`ksp_build` wraps ``setuptools.setup`` for the duration of
|
|
6
|
+
the build and passes them in there.
|
|
7
|
+
|
|
8
|
+
It is also the one place that sees both compilation steps at once, which is
|
|
9
|
+
what the three-switch combination needs::
|
|
10
|
+
|
|
11
|
+
[tool.ksp-builder]
|
|
12
|
+
cythonize = true
|
|
13
|
+
py_to_pyx = true
|
|
14
|
+
compile_kv = true
|
|
15
|
+
|
|
16
|
+
With all three on, a ``.kv`` is compiled into a module, staged as a ``.pyx``
|
|
17
|
+
and compiled to an extension; the ``.py`` that fed it is not shipped, and
|
|
18
|
+
neither is the ``.kv``.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import os
|
|
24
|
+
import warnings
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Iterator
|
|
27
|
+
|
|
28
|
+
from ._compile_kv import KvModule, compile_kv_modules, read_compile_kv
|
|
29
|
+
from ._cythonize import CyModule, build_extensions, collect_modules, read_cythonize_config
|
|
30
|
+
from ._packages import find_package_assets, prune_staging, read_section
|
|
31
|
+
|
|
32
|
+
__all__ = ["ksp_build"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.contextmanager
|
|
36
|
+
def ksp_build(project_root: Path, *, editable: bool = False) -> Iterator[None]:
|
|
37
|
+
"""Compile what ``[tool.ksp-builder]`` asks for, for this block's build.
|
|
38
|
+
|
|
39
|
+
A no-op when neither Cython nor KV compilation is configured. An editable
|
|
40
|
+
install exposes the source tree itself, so nothing generated would ever be
|
|
41
|
+
imported: ``.py`` modules are left alone there and ``.kv`` files stay as
|
|
42
|
+
they are, and only hand-written ``.pyx`` sources are still compiled.
|
|
43
|
+
"""
|
|
44
|
+
section = read_section(project_root)
|
|
45
|
+
config = read_cythonize_config(project_root)
|
|
46
|
+
compile_kv = read_compile_kv(project_root) and not editable
|
|
47
|
+
|
|
48
|
+
if config is None and not compile_kv:
|
|
49
|
+
yield
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if config is not None and config.py_to_pyx and not config.cythonize:
|
|
53
|
+
print(
|
|
54
|
+
"ksp-builder: py_to_pyx is set but cythonize is not — "
|
|
55
|
+
"nothing will be compiled.",
|
|
56
|
+
flush=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
convert_py = (
|
|
60
|
+
config is not None and config.cythonize and config.py_to_pyx and not editable
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
kv_modules: list[KvModule] = []
|
|
64
|
+
if compile_kv:
|
|
65
|
+
kv_modules = compile_kv_modules(
|
|
66
|
+
project_root,
|
|
67
|
+
as_pyx=convert_py,
|
|
68
|
+
exclude=config.exclude if config is not None else (),
|
|
69
|
+
)
|
|
70
|
+
for module in kv_modules:
|
|
71
|
+
print(f"ksp-builder: compile_kv {module.kv} -> {module.dotted}", flush=True)
|
|
72
|
+
|
|
73
|
+
cy_modules: list[CyModule] = []
|
|
74
|
+
if config is not None:
|
|
75
|
+
cy_modules = collect_modules(
|
|
76
|
+
project_root,
|
|
77
|
+
config,
|
|
78
|
+
allow_py_to_pyx=not editable,
|
|
79
|
+
skip={(m.package, m.name) for m in kv_modules},
|
|
80
|
+
)
|
|
81
|
+
cy_modules += [
|
|
82
|
+
CyModule(
|
|
83
|
+
package=module.package,
|
|
84
|
+
name=module.name,
|
|
85
|
+
source=module.source,
|
|
86
|
+
origin=module.kv,
|
|
87
|
+
converted=True,
|
|
88
|
+
)
|
|
89
|
+
for module in kv_modules
|
|
90
|
+
if module.as_pyx
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
prune_staging(
|
|
94
|
+
project_root,
|
|
95
|
+
{project_root / m.source for m in cy_modules if m.converted}
|
|
96
|
+
| {project_root / m.source for m in kv_modules},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not cy_modules and not kv_modules:
|
|
100
|
+
yield
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
for module in cy_modules:
|
|
104
|
+
print(f"ksp-builder: cythonize {module.origin} -> {module.dotted}", flush=True)
|
|
105
|
+
|
|
106
|
+
ext_modules = build_extensions(cy_modules, config) if cy_modules else []
|
|
107
|
+
|
|
108
|
+
keep_py = config is not None and config.keep_py
|
|
109
|
+
excluded: set[tuple[str, str]] = set()
|
|
110
|
+
if not keep_py:
|
|
111
|
+
excluded = {(m.package, m.name) for m in cy_modules if m.converted}
|
|
112
|
+
|
|
113
|
+
# A KV module that was not staged for Cython ships as source, generated
|
|
114
|
+
# from the .kv plus the .py beside it — so it replaces that .py, and adds a
|
|
115
|
+
# module of its own when there was no .py to begin with.
|
|
116
|
+
replacements = {
|
|
117
|
+
(m.package, m.name): str(project_root / m.source)
|
|
118
|
+
for m in kv_modules
|
|
119
|
+
if not m.as_pyx
|
|
120
|
+
}
|
|
121
|
+
# The .kv has been compiled into the module; shipping it too would only
|
|
122
|
+
# invite a second, conflicting set of rules at runtime.
|
|
123
|
+
compiled_kv = {os.path.abspath(project_root / m.kv) for m in kv_modules}
|
|
124
|
+
|
|
125
|
+
with _patched_setup(
|
|
126
|
+
ext_modules,
|
|
127
|
+
excluded=excluded,
|
|
128
|
+
replacements=replacements,
|
|
129
|
+
dropped_data=compiled_kv,
|
|
130
|
+
include_assets=bool(section.get("include_assets", True)),
|
|
131
|
+
):
|
|
132
|
+
yield
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@contextlib.contextmanager
|
|
136
|
+
def _patched_setup(
|
|
137
|
+
ext_modules: list,
|
|
138
|
+
*,
|
|
139
|
+
excluded: set[tuple[str, str]],
|
|
140
|
+
replacements: dict[tuple[str, str], str],
|
|
141
|
+
dropped_data: set[str],
|
|
142
|
+
include_assets: bool,
|
|
143
|
+
) -> Iterator[None]:
|
|
144
|
+
import setuptools
|
|
145
|
+
|
|
146
|
+
original = setuptools.setup
|
|
147
|
+
|
|
148
|
+
def setup(**attrs):
|
|
149
|
+
attrs["ext_modules"] = list(attrs.get("ext_modules") or []) + list(ext_modules)
|
|
150
|
+
cmdclass = dict(attrs.get("cmdclass") or {})
|
|
151
|
+
cmdclass.setdefault(
|
|
152
|
+
"build_py",
|
|
153
|
+
_make_build_py(excluded, replacements, dropped_data, include_assets),
|
|
154
|
+
)
|
|
155
|
+
attrs["cmdclass"] = cmdclass
|
|
156
|
+
return original(**attrs)
|
|
157
|
+
|
|
158
|
+
setuptools.setup = setup
|
|
159
|
+
try:
|
|
160
|
+
with _quiet_data_directory_warning(include_assets):
|
|
161
|
+
yield
|
|
162
|
+
finally:
|
|
163
|
+
setuptools.setup = original
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@contextlib.contextmanager
|
|
167
|
+
def _quiet_data_directory_warning(active: bool) -> Iterator[None]:
|
|
168
|
+
"""Silence setuptools' "Package would be ignored" notice for asset dirs.
|
|
169
|
+
|
|
170
|
+
Shipping ``images/`` or ``fonts/`` as package data is the whole point here,
|
|
171
|
+
but the files reach egg-info's manifest, where setuptools sees importable
|
|
172
|
+
directories missing from ``packages`` and emits ~30 lines of advice for
|
|
173
|
+
each one. Only that one warning is filtered, and only during our build.
|
|
174
|
+
"""
|
|
175
|
+
category = None
|
|
176
|
+
if active:
|
|
177
|
+
from setuptools.command import build_py as _build_py_module
|
|
178
|
+
|
|
179
|
+
abuse = getattr(_build_py_module, "_IncludePackageDataAbuse", None)
|
|
180
|
+
category = getattr(abuse, "_Warning", None)
|
|
181
|
+
|
|
182
|
+
if category is None:
|
|
183
|
+
yield
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
with warnings.catch_warnings():
|
|
187
|
+
warnings.filterwarnings("ignore", category=category)
|
|
188
|
+
yield
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _make_build_py(
|
|
192
|
+
excluded: set[tuple[str, str]],
|
|
193
|
+
replacements: dict[tuple[str, str], str],
|
|
194
|
+
dropped_data: set[str],
|
|
195
|
+
include_assets: bool,
|
|
196
|
+
):
|
|
197
|
+
"""A ``build_py`` that ships what this build generated, not what it read."""
|
|
198
|
+
from setuptools.command.build_py import build_py as _build_py
|
|
199
|
+
|
|
200
|
+
class build_py(_build_py):
|
|
201
|
+
ksp_excluded = excluded
|
|
202
|
+
ksp_replacements = replacements
|
|
203
|
+
ksp_dropped_data = dropped_data
|
|
204
|
+
ksp_include_assets = include_assets
|
|
205
|
+
|
|
206
|
+
def find_package_modules(self, package, package_dir):
|
|
207
|
+
found = [
|
|
208
|
+
entry
|
|
209
|
+
for entry in super().find_package_modules(package, package_dir)
|
|
210
|
+
if (entry[0], entry[1]) not in excluded
|
|
211
|
+
]
|
|
212
|
+
generated = {
|
|
213
|
+
name: path
|
|
214
|
+
for (owner, name), path in replacements.items()
|
|
215
|
+
if owner == package
|
|
216
|
+
}
|
|
217
|
+
if not generated:
|
|
218
|
+
return found
|
|
219
|
+
|
|
220
|
+
modules = [
|
|
221
|
+
(pkg, name, generated.get(name, source)) for pkg, name, source in found
|
|
222
|
+
]
|
|
223
|
+
present = {name for _, name, _ in found}
|
|
224
|
+
modules += [
|
|
225
|
+
(package, name, path)
|
|
226
|
+
for name, path in sorted(generated.items())
|
|
227
|
+
if name not in present
|
|
228
|
+
]
|
|
229
|
+
return modules
|
|
230
|
+
|
|
231
|
+
def find_data_files(self, package, src_dir):
|
|
232
|
+
files = list(super().find_data_files(package, src_dir))
|
|
233
|
+
if include_assets and src_dir:
|
|
234
|
+
# exclude_data_files applies the project's exclude-package-data,
|
|
235
|
+
# which super() has already applied to its own half of the list.
|
|
236
|
+
found = self.exclude_data_files(
|
|
237
|
+
package, src_dir, find_package_assets(Path(src_dir))
|
|
238
|
+
)
|
|
239
|
+
seen = set(files)
|
|
240
|
+
for path in found:
|
|
241
|
+
if path not in seen:
|
|
242
|
+
seen.add(path)
|
|
243
|
+
files.append(path)
|
|
244
|
+
if dropped_data:
|
|
245
|
+
files = [
|
|
246
|
+
path for path in files if os.path.abspath(path) not in dropped_data
|
|
247
|
+
]
|
|
248
|
+
return files
|
|
249
|
+
|
|
250
|
+
return build_py
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""ksp_builder._compile_kv — compiles Kivy ``.kv`` files into Python modules.
|
|
2
|
+
|
|
3
|
+
Configured under ``[tool.ksp-builder]``::
|
|
4
|
+
|
|
5
|
+
[tool.ksp-builder]
|
|
6
|
+
compile_kv = true
|
|
7
|
+
|
|
8
|
+
Every ``.kv`` sitting in a package directory is handed to the ``compilekv``
|
|
9
|
+
library together with the ``.py`` of the same name beside it, and the module
|
|
10
|
+
the two produce is written to the staging directory. That generated module is
|
|
11
|
+
what ships: it carries the hand-written code *and* the widget classes the KV
|
|
12
|
+
rules describe, so the ``.kv`` itself is no longer needed at runtime and is
|
|
13
|
+
left out of the wheel.
|
|
14
|
+
|
|
15
|
+
With ``cythonize`` and ``py_to_pyx`` also on, the generated module is staged as
|
|
16
|
+
a ``.pyx`` and compiled to an extension instead of shipping as source.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Sequence
|
|
23
|
+
|
|
24
|
+
from ._packages import (
|
|
25
|
+
NEVER_CONVERTED,
|
|
26
|
+
STAGING_DIR,
|
|
27
|
+
find_packages,
|
|
28
|
+
is_excluded,
|
|
29
|
+
stage_file,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"KvModule",
|
|
34
|
+
"read_compile_kv",
|
|
35
|
+
"find_kv_sources",
|
|
36
|
+
"compile_kv_modules",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class KvModule:
|
|
42
|
+
"""One module compiled out of a ``.kv`` file."""
|
|
43
|
+
|
|
44
|
+
package: str # dotted package name, e.g. "myapp.screens"
|
|
45
|
+
name: str # module name without extension, e.g. "home"
|
|
46
|
+
source: Path # the generated module, relative to the project root
|
|
47
|
+
kv: Path # the .kv it was compiled from, relative to the project root
|
|
48
|
+
py: Path | None # the hand-written .py merged into it, if there was one
|
|
49
|
+
as_pyx: bool # True when the module was staged for Cython
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def dotted(self) -> str:
|
|
53
|
+
return f"{self.package}.{self.name}" if self.package else self.name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_compile_kv(project_root: Path) -> bool:
|
|
57
|
+
"""Whether ``[tool.ksp-builder] compile_kv`` is on."""
|
|
58
|
+
from ._packages import read_section
|
|
59
|
+
|
|
60
|
+
return bool(read_section(project_root).get("compile_kv", False))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _compilekv():
|
|
64
|
+
"""The ``compilekv`` library, with a build-time error if it is missing."""
|
|
65
|
+
try:
|
|
66
|
+
import compilekv
|
|
67
|
+
except ImportError as exc: # pragma: no cover - compilekv is a dependency
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
"ksp-builder: compile_kv is enabled but compilekv is not installed."
|
|
70
|
+
) from exc
|
|
71
|
+
return compilekv
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_kv_sources(project_root: Path) -> list[tuple[str, Path]]:
|
|
75
|
+
"""The ``(package, kv path)`` pairs this project can compile.
|
|
76
|
+
|
|
77
|
+
Only ``.kv`` files directly inside a package are collected — a compiled KV
|
|
78
|
+
file becomes an importable module, and there is no module name for one
|
|
79
|
+
sitting in a plain data directory. Those stay ordinary package data.
|
|
80
|
+
"""
|
|
81
|
+
find_kv_files = _compilekv().find_kv_files
|
|
82
|
+
|
|
83
|
+
sources: list[tuple[str, Path]] = []
|
|
84
|
+
for package, directory in sorted(find_packages(project_root).items()):
|
|
85
|
+
for kv_path in find_kv_files(directory, recursive=False):
|
|
86
|
+
sources.append((package, kv_path))
|
|
87
|
+
return sources
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compile_kv_modules(
|
|
91
|
+
project_root: Path,
|
|
92
|
+
*,
|
|
93
|
+
as_pyx: bool = False,
|
|
94
|
+
exclude: Sequence[str] = (),
|
|
95
|
+
) -> list[KvModule]:
|
|
96
|
+
"""Compile every package ``.kv`` into the staging directory.
|
|
97
|
+
|
|
98
|
+
``as_pyx`` stages the result for Cython instead of shipping it as source.
|
|
99
|
+
``exclude`` is ``cythonize_exclude``: a module it names is still compiled
|
|
100
|
+
out of its KV file, it just stays a plain ``.py``.
|
|
101
|
+
"""
|
|
102
|
+
sources = find_kv_sources(project_root)
|
|
103
|
+
if not sources:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
compilekv = _compilekv()
|
|
107
|
+
KvCompileError = compilekv.KvCompileError
|
|
108
|
+
source_path_for = compilekv.source_path_for
|
|
109
|
+
|
|
110
|
+
compiler = compilekv.default_compiler()
|
|
111
|
+
patterns = list(exclude)
|
|
112
|
+
modules: list[KvModule] = []
|
|
113
|
+
|
|
114
|
+
for package, kv_path in sources:
|
|
115
|
+
relative_kv = kv_path.relative_to(project_root)
|
|
116
|
+
py_path = source_path_for(kv_path)
|
|
117
|
+
has_py = py_path.is_file()
|
|
118
|
+
py_source = py_path.read_text(encoding="utf-8") if has_py else ""
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
generated = compiler.compile_source(
|
|
122
|
+
kv_path.read_text(encoding="utf-8"), py_source
|
|
123
|
+
)
|
|
124
|
+
except KvCompileError as exc:
|
|
125
|
+
raise RuntimeError(f"ksp-builder: cannot compile {relative_kv}: {exc}") from exc
|
|
126
|
+
|
|
127
|
+
relative_py = py_path.relative_to(project_root) if has_py else None
|
|
128
|
+
staged_as_pyx = as_pyx and _compilable(kv_path.stem, relative_kv, relative_py, patterns)
|
|
129
|
+
suffix = ".pyx" if staged_as_pyx else ".py"
|
|
130
|
+
staged = Path(STAGING_DIR, *package.split("."), f"{kv_path.stem}{suffix}")
|
|
131
|
+
stage_file(project_root / staged, generated.encode("utf-8"))
|
|
132
|
+
|
|
133
|
+
modules.append(
|
|
134
|
+
KvModule(
|
|
135
|
+
package=package,
|
|
136
|
+
name=kv_path.stem,
|
|
137
|
+
source=staged,
|
|
138
|
+
kv=relative_kv,
|
|
139
|
+
py=relative_py,
|
|
140
|
+
as_pyx=staged_as_pyx,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return modules
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _compilable(
|
|
148
|
+
stem: str,
|
|
149
|
+
relative_kv: Path,
|
|
150
|
+
relative_py: Path | None,
|
|
151
|
+
patterns: list[str],
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Whether the module built from this KV file may become an extension."""
|
|
154
|
+
if stem in NEVER_CONVERTED:
|
|
155
|
+
return False
|
|
156
|
+
if is_excluded(relative_kv, patterns):
|
|
157
|
+
return False
|
|
158
|
+
return relative_py is None or not is_excluded(relative_py, patterns)
|