qyro-cli 1.0.0a1__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.
- qyro_cli/__main__.py +29 -0
- qyro_cli/adapters/cli/console_ui.py +224 -0
- qyro_cli/adapters/cli/progress.py +46 -0
- qyro_cli/adapters/freeze/__init__.py +6 -0
- qyro_cli/adapters/freeze/framework_hooks.py +352 -0
- qyro_cli/adapters/freeze/hooks/__init__.py +0 -0
- qyro_cli/adapters/freeze/hooks/kivy_hook.py +75 -0
- qyro_cli/adapters/freeze/mac.py +48 -0
- qyro_cli/adapters/freeze/optimizer.py +141 -0
- qyro_cli/adapters/freeze/pyinstaller_adapter.py +836 -0
- qyro_cli/adapters/freeze/windows.py +30 -0
- qyro_cli/adapters/package/compiled_app_signer.py +513 -0
- qyro_cli/adapters/package/metadata.py +10 -0
- qyro_cli/adapters/package/release_bundler.py +822 -0
- qyro_cli/adapters/package/templates/nsis_installer.nsi +143 -0
- qyro_cli/adapters/persistence/storage.py +272 -0
- qyro_cli/adapters/process/dependencies.py +89 -0
- qyro_cli/adapters/process/runners.py +92 -0
- qyro_cli/adapters/templates/bundled.py +47 -0
- qyro_cli/adapters/templates/fallback.py +41 -0
- qyro_cli/adapters/templates/github_cached.py +432 -0
- qyro_cli/application/guards.py +35 -0
- qyro_cli/application/ports.py +357 -0
- qyro_cli/application/use_cases/bundle.py +130 -0
- qyro_cli/application/use_cases/clean.py +51 -0
- qyro_cli/application/use_cases/freeze.py +538 -0
- qyro_cli/application/use_cases/init.py +278 -0
- qyro_cli/application/use_cases/sign.py +176 -0
- qyro_cli/application/use_cases/start.py +63 -0
- qyro_cli/application/use_cases/version.py +23 -0
- qyro_cli/cmdline/__init__.py +4 -0
- qyro_cli/cmdline/app.py +43 -0
- qyro_cli/cmdline/commands/__init__.py +24 -0
- qyro_cli/cmdline/commands/build.py +159 -0
- qyro_cli/cmdline/commands/bundle.py +93 -0
- qyro_cli/cmdline/commands/clean.py +32 -0
- qyro_cli/cmdline/commands/init.py +130 -0
- qyro_cli/cmdline/commands/sign.py +107 -0
- qyro_cli/cmdline/commands/start.py +34 -0
- qyro_cli/cmdline/commands/version.py +25 -0
- qyro_cli/cmdline/models.py +33 -0
- qyro_cli/cmdline/parser.py +46 -0
- qyro_cli/cmdline/registry.py +68 -0
- qyro_cli/container.py +161 -0
- qyro_cli/domain/build.py +194 -0
- qyro_cli/domain/errors.py +257 -0
- qyro_cli/domain/mobile.py +31 -0
- qyro_cli/domain/project.py +223 -0
- qyro_cli/domain/version.py +76 -0
- qyro_cli-1.0.0a1.dist-info/LICENSE +21 -0
- qyro_cli-1.0.0a1.dist-info/METADATA +818 -0
- qyro_cli-1.0.0a1.dist-info/NOTICE +159 -0
- qyro_cli-1.0.0a1.dist-info/RECORD +55 -0
- qyro_cli-1.0.0a1.dist-info/WHEEL +4 -0
- qyro_cli-1.0.0a1.dist-info/entry_points.txt +3 -0
qyro_cli/__main__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module entry point.
|
|
3
|
+
|
|
4
|
+
Allows invoking the CLI directly via:
|
|
5
|
+
|
|
6
|
+
python -m qyro <command> [options]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from qyro_cli.cmdline import main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _configure_stdio() -> None:
|
|
15
|
+
"""Use UTF-8 output when possible to avoid Windows codepage crashes."""
|
|
16
|
+
for stream_name in ("stdout", "stderr"):
|
|
17
|
+
stream = getattr(sys, stream_name, None)
|
|
18
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
19
|
+
if callable(reconfigure):
|
|
20
|
+
try:
|
|
21
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
except Exception:
|
|
23
|
+
# Keep default stream settings if the runtime disallows reconfigure.
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
_configure_stdio()
|
|
29
|
+
sys.exit(main())
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete presentation adapter implementing UserInteractionPort
|
|
3
|
+
using Rich and Questionary.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import random
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
import questionary
|
|
11
|
+
from prompt_toolkit.styles import Style
|
|
12
|
+
from rich.theme import Theme
|
|
13
|
+
from rich.align import Align
|
|
14
|
+
from rich.console import Console, Group
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.prompt import Confirm, Prompt
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
QUESTIONARY_STYLE = Style([
|
|
21
|
+
("qmark", "fg:#c08457 bold"),
|
|
22
|
+
("question", "fg:#f1f5f9 bold"),
|
|
23
|
+
("answer", "fg:#fdba74 bold"),
|
|
24
|
+
("pointer", "fg:#fb923c bold"),
|
|
25
|
+
("highlighted", "fg:#fb923c bg:default bold"),
|
|
26
|
+
("selected", "fg:#fdba74 bg:default"),
|
|
27
|
+
("instruction", "fg:#9ca3af"),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
custom_theme = Theme(
|
|
32
|
+
{
|
|
33
|
+
"prompt.choices": "bold white",
|
|
34
|
+
"prompt.default": "bold #ff8c00",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
class RichConsoleUI:
|
|
38
|
+
def __init__(self, console: Console | None = None):
|
|
39
|
+
self._console = console or Console(
|
|
40
|
+
force_terminal=True,
|
|
41
|
+
color_system="truecolor",
|
|
42
|
+
theme=custom_theme,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _ascii_fallback(self, message: str) -> str:
|
|
46
|
+
safe = (
|
|
47
|
+
message
|
|
48
|
+
.replace("⏳", "...")
|
|
49
|
+
.replace("💔", "Error")
|
|
50
|
+
.replace("🚀", "*")
|
|
51
|
+
.replace("✓", "OK")
|
|
52
|
+
.replace("⚠️", "WARN")
|
|
53
|
+
.replace("⚠", "WARN")
|
|
54
|
+
)
|
|
55
|
+
return safe.encode("ascii", errors="replace").decode("ascii")
|
|
56
|
+
|
|
57
|
+
def _print(self, renderable: object) -> None:
|
|
58
|
+
try:
|
|
59
|
+
self._console.print(renderable)
|
|
60
|
+
except UnicodeEncodeError:
|
|
61
|
+
if isinstance(renderable, str):
|
|
62
|
+
self._console.print(self._ascii_fallback(renderable))
|
|
63
|
+
return
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def welcome(self) -> None:
|
|
67
|
+
messages = [
|
|
68
|
+
"What are we building today?",
|
|
69
|
+
"What will you build today?",
|
|
70
|
+
"Let's build something great.",
|
|
71
|
+
"Your next project starts here.",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
message = random.choice(messages)
|
|
75
|
+
directory = Path.cwd().name
|
|
76
|
+
|
|
77
|
+
logo = Text(
|
|
78
|
+
"\n"
|
|
79
|
+
" ██████╗ ██╗ ██╗██████╗ ██████╗\n"
|
|
80
|
+
"██╔═══██╗╚██╗ ██╔╝██╔══██╗██╔═══██╗\n"
|
|
81
|
+
"██║ ██║ ╚████╔╝ ██████╔╝██║ ██║\n"
|
|
82
|
+
"██║▄▄ ██║ ╚██╔╝ ██╔══██╗██║ ██║\n"
|
|
83
|
+
"╚██████╔╝ ██║ ██║ ██║╚██████╔╝\n"
|
|
84
|
+
" ╚══▀▀═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝\n",
|
|
85
|
+
style="bold #ff8c00",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
title = Text(message, style="bold white")
|
|
89
|
+
subtitle = Text(
|
|
90
|
+
f"Creating your next application in {directory}/",
|
|
91
|
+
style="dim",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
content = Group(
|
|
95
|
+
Align.center(logo),
|
|
96
|
+
Align.center(title),
|
|
97
|
+
Align.center(subtitle),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self._console.print(
|
|
101
|
+
Panel(
|
|
102
|
+
content,
|
|
103
|
+
border_style="#ff8c00",
|
|
104
|
+
padding=(1, 4),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def ask_text(
|
|
109
|
+
self,
|
|
110
|
+
prompt: str,
|
|
111
|
+
default: str = "",
|
|
112
|
+
show_default: bool = True,
|
|
113
|
+
) -> str:
|
|
114
|
+
return Prompt.ask(
|
|
115
|
+
f"[bold]{prompt}[/bold]",
|
|
116
|
+
default=default,
|
|
117
|
+
show_default=show_default,
|
|
118
|
+
console=self._console
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def ask_choice(
|
|
122
|
+
self,
|
|
123
|
+
prompt: str,
|
|
124
|
+
choices: Sequence[str],
|
|
125
|
+
default: str = "",
|
|
126
|
+
) -> str:
|
|
127
|
+
options = list(choices)
|
|
128
|
+
|
|
129
|
+
if not options:
|
|
130
|
+
return default
|
|
131
|
+
|
|
132
|
+
answer = questionary.select(
|
|
133
|
+
prompt,
|
|
134
|
+
choices=options,
|
|
135
|
+
default=default or options[0],
|
|
136
|
+
style=QUESTIONARY_STYLE,
|
|
137
|
+
).ask()
|
|
138
|
+
|
|
139
|
+
if answer is None:
|
|
140
|
+
raise KeyboardInterrupt
|
|
141
|
+
|
|
142
|
+
return answer
|
|
143
|
+
|
|
144
|
+
def ask_multi_choice(
|
|
145
|
+
self,
|
|
146
|
+
prompt: str,
|
|
147
|
+
choices: Sequence[str],
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
answer = questionary.checkbox(
|
|
150
|
+
prompt,
|
|
151
|
+
choices=[
|
|
152
|
+
questionary.Choice(
|
|
153
|
+
choice,
|
|
154
|
+
value=choice,
|
|
155
|
+
checked=False,
|
|
156
|
+
)
|
|
157
|
+
for choice in choices
|
|
158
|
+
],
|
|
159
|
+
style=QUESTIONARY_STYLE,
|
|
160
|
+
).ask()
|
|
161
|
+
|
|
162
|
+
if answer is None:
|
|
163
|
+
raise KeyboardInterrupt
|
|
164
|
+
|
|
165
|
+
return answer
|
|
166
|
+
|
|
167
|
+
def confirm(
|
|
168
|
+
self,
|
|
169
|
+
prompt: str,
|
|
170
|
+
default: bool = True,
|
|
171
|
+
) -> bool:
|
|
172
|
+
return Confirm.ask(
|
|
173
|
+
f"[bold #fff]{prompt}[/bold #fff]",
|
|
174
|
+
default=default,
|
|
175
|
+
console=self._console,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def show_summary(
|
|
179
|
+
self,
|
|
180
|
+
title: str,
|
|
181
|
+
fields: dict[str, str],
|
|
182
|
+
) -> None:
|
|
183
|
+
table = Table(
|
|
184
|
+
title="Project Configuration",
|
|
185
|
+
show_header=False,
|
|
186
|
+
box=None,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
for label, value in fields.items():
|
|
190
|
+
table.add_row(
|
|
191
|
+
f"{label}:",
|
|
192
|
+
f"[#fdba74]{value}[/#fdba74]",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
self._console.print(
|
|
196
|
+
Panel(
|
|
197
|
+
table,
|
|
198
|
+
title=f"[bold]{title}[/bold]",
|
|
199
|
+
border_style="#ff8c00",
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def info(self, message: str) -> None:
|
|
204
|
+
self._print(message)
|
|
205
|
+
|
|
206
|
+
def success(self, message: str) -> None:
|
|
207
|
+
self._print(
|
|
208
|
+
f"[bold #fff]{message}[/bold #fff]"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def warning(self, message: str) -> None:
|
|
212
|
+
self._print(
|
|
213
|
+
f"[#fdba74]{message}[/#fdba74]"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def progress(self, message: str) -> None:
|
|
217
|
+
self._print(
|
|
218
|
+
f"[#fdba74]⏳ {message}[/#fdba74]"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def error(self, message: str) -> None:
|
|
222
|
+
self._print(
|
|
223
|
+
f"\n💔 [bold red]Error:[/bold red] {message}"
|
|
224
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.progress import (
|
|
3
|
+
BarColumn,
|
|
4
|
+
DownloadColumn,
|
|
5
|
+
Progress,
|
|
6
|
+
TextColumn,
|
|
7
|
+
TimeRemainingColumn,
|
|
8
|
+
TransferSpeedColumn,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RichProgress:
|
|
13
|
+
def __init__(self, console: Console | None = None):
|
|
14
|
+
self._progress = Progress(
|
|
15
|
+
TextColumn("[progress.description]{task.description}"),
|
|
16
|
+
BarColumn(),
|
|
17
|
+
DownloadColumn(),
|
|
18
|
+
TransferSpeedColumn(),
|
|
19
|
+
TimeRemainingColumn(),
|
|
20
|
+
console=console,
|
|
21
|
+
)
|
|
22
|
+
self._task_id = None
|
|
23
|
+
|
|
24
|
+
def start(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
total: int | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._task_id = self._progress.add_task(
|
|
30
|
+
message,
|
|
31
|
+
total=total,
|
|
32
|
+
)
|
|
33
|
+
self._progress.start()
|
|
34
|
+
|
|
35
|
+
def update(self, completed: int) -> None:
|
|
36
|
+
if self._task_id is not None:
|
|
37
|
+
self._progress.update(
|
|
38
|
+
self._task_id,
|
|
39
|
+
completed=completed,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def stop(self) -> None:
|
|
43
|
+
if self._task_id is not None:
|
|
44
|
+
self._progress.stop()
|
|
45
|
+
self._progress.remove_task(self._task_id)
|
|
46
|
+
self._task_id = None
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""qyro.adapters.freeze package."""
|
|
2
|
+
from qyro_cli.adapters.freeze.framework_hooks import FrameworkHookResolver
|
|
3
|
+
from qyro_cli.adapters.freeze.optimizer import BinaryOptimizer
|
|
4
|
+
from qyro_cli.adapters.freeze.pyinstaller_adapter import PyInstallerFreezer
|
|
5
|
+
|
|
6
|
+
__all__ = ["FrameworkHookResolver", "BinaryOptimizer", "PyInstallerFreezer"]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
qyro.adapters.freeze.framework_hooks
|
|
4
|
+
Framework-specific resolution hooks for Qyro's Freeze system.
|
|
5
|
+
Handles PyQt5, PyQt6, PySide2, PySide6, Kivy, Tkinter hidden imports and resource data,
|
|
6
|
+
integrating with Qyro's domain Binding and Project entities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, List
|
|
11
|
+
|
|
12
|
+
from qyro_cli.application.ports import FrameworkHookResolverPort
|
|
13
|
+
from qyro_cli.domain.build import FreezeManifest
|
|
14
|
+
from qyro_cli.domain.project import Binding, AddonModule
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FrameworkHookResolver(FrameworkHookResolverPort):
|
|
18
|
+
"""
|
|
19
|
+
Intelligently inspects project files and domain Binding to resolve:
|
|
20
|
+
- PyInstaller hidden imports
|
|
21
|
+
- Collect data / resource paths
|
|
22
|
+
- Binary dependencies (SDL2, GLEW, etc. for Kivy)
|
|
23
|
+
- Plugin directories for Qt family
|
|
24
|
+
- Common project settings and assets
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def resolve_args(
|
|
28
|
+
self,
|
|
29
|
+
project_root: Path,
|
|
30
|
+
manifest: FreezeManifest,
|
|
31
|
+
) -> List[str]:
|
|
32
|
+
args: List[str] = []
|
|
33
|
+
|
|
34
|
+
binding = Binding.parse(manifest.binding)
|
|
35
|
+
|
|
36
|
+
resolvers: dict[Binding, Callable[[Path, FreezeManifest], List[str]]] = {
|
|
37
|
+
Binding.PYSIDE6: self._resolve_pyside6,
|
|
38
|
+
Binding.PYQT6: self._resolve_pyqt6,
|
|
39
|
+
Binding.PYQT5: self._resolve_pyqt5,
|
|
40
|
+
Binding.PYSIDE2: self._resolve_pyside2,
|
|
41
|
+
Binding.KIVY: self._resolve_kivy,
|
|
42
|
+
Binding.TKINTER: self._resolve_tkinter,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resolver = resolvers.get(binding)
|
|
46
|
+
|
|
47
|
+
if resolver:
|
|
48
|
+
args.extend(resolver(project_root, manifest))
|
|
49
|
+
|
|
50
|
+
args.extend(self._resolve_competing_framework_exclusions(binding, manifest))
|
|
51
|
+
|
|
52
|
+
args.extend(self._resolve_addons(manifest))
|
|
53
|
+
args.extend(self._resolve_project_assets(project_root, manifest))
|
|
54
|
+
|
|
55
|
+
return args
|
|
56
|
+
|
|
57
|
+
def _resolve_qt(
|
|
58
|
+
self,
|
|
59
|
+
package: str,
|
|
60
|
+
*,
|
|
61
|
+
sip: bool = False,
|
|
62
|
+
) -> List[str]:
|
|
63
|
+
args = [
|
|
64
|
+
"--hidden-import", package,
|
|
65
|
+
"--hidden-import", f"{package}.QtCore",
|
|
66
|
+
"--hidden-import", f"{package}.QtGui",
|
|
67
|
+
"--hidden-import", f"{package}.QtWidgets",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
if package == "PySide6":
|
|
71
|
+
args.extend(["--exclude-module", "PySide6.scripts"])
|
|
72
|
+
|
|
73
|
+
if package == "PySide2":
|
|
74
|
+
args.extend(["--exclude-module", "PySide2.scripts"])
|
|
75
|
+
|
|
76
|
+
if package == "PyQt6":
|
|
77
|
+
args.extend(["--exclude-module", "PyQt6.scripts"])
|
|
78
|
+
|
|
79
|
+
if package == "PyQt5":
|
|
80
|
+
args.extend(["--exclude-module", "PyQt5.scripts"])
|
|
81
|
+
|
|
82
|
+
if sip:
|
|
83
|
+
args.extend(["--hidden-import", f"{package}.sip"])
|
|
84
|
+
|
|
85
|
+
return args
|
|
86
|
+
|
|
87
|
+
def _resolve_pyside6(
|
|
88
|
+
self,
|
|
89
|
+
project_root: Path,
|
|
90
|
+
manifest: FreezeManifest,
|
|
91
|
+
) -> List[str]:
|
|
92
|
+
args = self._resolve_qt("PySide6")
|
|
93
|
+
|
|
94
|
+
# Check for QML / Quick in project only if not explicitly excluded
|
|
95
|
+
user_excludes = {m.lower() for m in manifest.optimization.exclude_modules}
|
|
96
|
+
if self._has_qml(project_root):
|
|
97
|
+
if "pyside6.qtquick" not in user_excludes:
|
|
98
|
+
args.extend(["--hidden-import", "PySide6.QtQuick"])
|
|
99
|
+
if "pyside6.qtqml" not in user_excludes:
|
|
100
|
+
args.extend(["--hidden-import", "PySide6.QtQml"])
|
|
101
|
+
|
|
102
|
+
return args
|
|
103
|
+
|
|
104
|
+
def _resolve_pyqt6(
|
|
105
|
+
self,
|
|
106
|
+
project_root: Path,
|
|
107
|
+
manifest: FreezeManifest,
|
|
108
|
+
) -> List[str]:
|
|
109
|
+
return self._resolve_qt("PyQt6", sip=True)
|
|
110
|
+
|
|
111
|
+
def _resolve_pyqt5(
|
|
112
|
+
self,
|
|
113
|
+
project_root: Path,
|
|
114
|
+
manifest: FreezeManifest,
|
|
115
|
+
) -> List[str]:
|
|
116
|
+
return self._resolve_qt("PyQt5", sip=True)
|
|
117
|
+
|
|
118
|
+
def _resolve_pyside2(
|
|
119
|
+
self,
|
|
120
|
+
project_root: Path,
|
|
121
|
+
manifest: FreezeManifest,
|
|
122
|
+
) -> List[str]:
|
|
123
|
+
args = self._resolve_qt("PySide2")
|
|
124
|
+
args.extend(["--collect-all", "PySide2", "--collect-all", "shiboken2"])
|
|
125
|
+
return args
|
|
126
|
+
|
|
127
|
+
def _resolve_kivy(
|
|
128
|
+
self,
|
|
129
|
+
project_root: Path,
|
|
130
|
+
manifest: FreezeManifest,
|
|
131
|
+
) -> List[str]:
|
|
132
|
+
args = [
|
|
133
|
+
"--hidden-import", "kivy",
|
|
134
|
+
"--hidden-import", "kivy.app",
|
|
135
|
+
"--hidden-import", "kivy.uix",
|
|
136
|
+
"--hidden-import", "kivy.factory",
|
|
137
|
+
"--hidden-import", "kivy.lang",
|
|
138
|
+
"--hidden-import", "kivy.lang.builder",
|
|
139
|
+
"--hidden-import", "kivy.core.window",
|
|
140
|
+
"--hidden-import", "kivy.core.window.window_sdl2",
|
|
141
|
+
"--hidden-import", "kivy.core.text",
|
|
142
|
+
"--hidden-import", "kivy.core.text.text_sdl2",
|
|
143
|
+
"--hidden-import", "kivy.core.image",
|
|
144
|
+
"--hidden-import", "kivy.core.image.img_sdl2",
|
|
145
|
+
"--hidden-import", "kivy.core.clipboard.clipboard_sdl2",
|
|
146
|
+
"--hidden-import", "kivy.graphics",
|
|
147
|
+
"--hidden-import", "kivy.graphics.cgl",
|
|
148
|
+
"--hidden-import", "kivy.graphics.cgl_backend.cgl_glew",
|
|
149
|
+
"--collect-data", "kivy",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
# Scan and bundle .kv files (Kivy declarative layout files)
|
|
153
|
+
kv_patterns = [
|
|
154
|
+
"*.kv",
|
|
155
|
+
"views/**/*.kv",
|
|
156
|
+
"components/**/*.kv",
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
collected_kv: set[str] = set()
|
|
160
|
+
|
|
161
|
+
for pattern in kv_patterns:
|
|
162
|
+
for path in project_root.glob(pattern):
|
|
163
|
+
if not path.is_file():
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
relative = path.relative_to(project_root)
|
|
167
|
+
source = path.resolve().as_posix()
|
|
168
|
+
destination = relative.parent.as_posix()
|
|
169
|
+
|
|
170
|
+
if destination == ".":
|
|
171
|
+
destination = "."
|
|
172
|
+
|
|
173
|
+
collected_kv.add(
|
|
174
|
+
f"{source}{self._data_separator(manifest)}{destination}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
for item in sorted(collected_kv):
|
|
178
|
+
args.extend(["--add-data", item])
|
|
179
|
+
|
|
180
|
+
# Attach Qyro's bundled Kivy runtime hook to set custom window icon
|
|
181
|
+
kivy_rthook = Path(__file__).parent / "hooks" / "rthook_kivy_icon.py"
|
|
182
|
+
if kivy_rthook.exists():
|
|
183
|
+
args.extend(["--runtime-hook", kivy_rthook.as_posix()])
|
|
184
|
+
|
|
185
|
+
return args
|
|
186
|
+
|
|
187
|
+
def _resolve_tkinter(
|
|
188
|
+
self,
|
|
189
|
+
project_root: Path,
|
|
190
|
+
manifest: FreezeManifest,
|
|
191
|
+
) -> List[str]:
|
|
192
|
+
args = [
|
|
193
|
+
"--hidden-import", "_tkinter",
|
|
194
|
+
"--hidden-import", "tkinter",
|
|
195
|
+
"--hidden-import", "tkinter.ttk",
|
|
196
|
+
"--hidden-import", "tkinter.messagebox",
|
|
197
|
+
"--hidden-import", "tkinter.filedialog",
|
|
198
|
+
"--collect-data", "tkinter",
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
import sys
|
|
202
|
+
sep = self._data_separator(manifest)
|
|
203
|
+
py_prefix = Path(sys.prefix)
|
|
204
|
+
|
|
205
|
+
tcl_tk_candidates = [
|
|
206
|
+
(py_prefix / "lib" / "tcl8.6", "tcl8.6"),
|
|
207
|
+
(py_prefix / "lib" / "tk8.6", "tk8.6"),
|
|
208
|
+
(py_prefix / "lib" / "tcl8.5", "tcl8.5"),
|
|
209
|
+
(py_prefix / "lib" / "tk8.5", "tk8.5"),
|
|
210
|
+
(py_prefix / "tcl", "tcl"),
|
|
211
|
+
(py_prefix / "tk", "tk"),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
for src_path, target_name in tcl_tk_candidates:
|
|
215
|
+
if src_path.is_dir():
|
|
216
|
+
args.extend(["--add-data", f"{src_path.as_posix()}{sep}{target_name}"])
|
|
217
|
+
|
|
218
|
+
return args
|
|
219
|
+
|
|
220
|
+
def _resolve_addons(
|
|
221
|
+
self,
|
|
222
|
+
manifest: FreezeManifest,
|
|
223
|
+
) -> List[str]:
|
|
224
|
+
"""Resolves PyInstaller args for registered addons."""
|
|
225
|
+
args: List[str] = []
|
|
226
|
+
addons = {str(addon).lower() for addon in (manifest.addons or [])}
|
|
227
|
+
|
|
228
|
+
if AddonModule.REQUESTS.value.lower() in addons:
|
|
229
|
+
args.extend([
|
|
230
|
+
"--hidden-import", "requests",
|
|
231
|
+
"--hidden-import", "urllib3",
|
|
232
|
+
"--hidden-import", "certifi",
|
|
233
|
+
"--collect-data", "certifi",
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
if "kivymd" in addons:
|
|
237
|
+
args.extend([
|
|
238
|
+
"--hidden-import", "kivymd",
|
|
239
|
+
"--hidden-import", "kivymd.app",
|
|
240
|
+
"--hidden-import", "kivymd.uix",
|
|
241
|
+
"--collect-data", "kivymd",
|
|
242
|
+
])
|
|
243
|
+
|
|
244
|
+
if "plyer" in addons:
|
|
245
|
+
args.extend([
|
|
246
|
+
"--hidden-import", "plyer",
|
|
247
|
+
"--hidden-import", "plyer.platforms",
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
return args
|
|
251
|
+
|
|
252
|
+
def _resolve_project_assets(
|
|
253
|
+
self,
|
|
254
|
+
project_root: Path,
|
|
255
|
+
manifest: FreezeManifest,
|
|
256
|
+
) -> List[str]:
|
|
257
|
+
args: List[str] = []
|
|
258
|
+
sep = self._data_separator(manifest)
|
|
259
|
+
|
|
260
|
+
resources_dir_name = manifest.resources_dir or "resources"
|
|
261
|
+
resources_path = (project_root / resources_dir_name).resolve()
|
|
262
|
+
|
|
263
|
+
if resources_path.is_dir():
|
|
264
|
+
# If resources/base exists, include platform profiles
|
|
265
|
+
base_dir = resources_path / "base"
|
|
266
|
+
if base_dir.is_dir():
|
|
267
|
+
# Add base resources
|
|
268
|
+
args.extend(["--add-data", f"{base_dir.as_posix()}{sep}{resources_dir_name}"])
|
|
269
|
+
|
|
270
|
+
# Add platform-specific resources if present
|
|
271
|
+
plat = manifest.target_platform.lower()
|
|
272
|
+
plat_dir = None
|
|
273
|
+
if plat in ("mac", "macos", "darwin"):
|
|
274
|
+
plat_dir = resources_path / "mac"
|
|
275
|
+
elif plat == "windows":
|
|
276
|
+
plat_dir = resources_path / "windows"
|
|
277
|
+
elif plat == "linux":
|
|
278
|
+
plat_dir = resources_path / "linux"
|
|
279
|
+
|
|
280
|
+
if plat_dir and plat_dir.is_dir():
|
|
281
|
+
args.extend(["--add-data", f"{plat_dir.as_posix()}{sep}{resources_dir_name}"])
|
|
282
|
+
else:
|
|
283
|
+
# Flat resources directory
|
|
284
|
+
args.extend([
|
|
285
|
+
"--add-data",
|
|
286
|
+
f"{resources_path.as_posix()}{sep}{resources_dir_name}",
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
settings_path = (project_root / "settings").resolve()
|
|
290
|
+
if settings_path.is_dir():
|
|
291
|
+
args.extend([
|
|
292
|
+
"--add-data",
|
|
293
|
+
f"{settings_path.as_posix()}{sep}settings",
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
return args
|
|
297
|
+
|
|
298
|
+
def _data_separator(self, manifest: FreezeManifest) -> str:
|
|
299
|
+
return ";" if manifest.target_platform == "windows" else ":"
|
|
300
|
+
|
|
301
|
+
def _has_qml(self, project_root: Path) -> bool:
|
|
302
|
+
# Avoid detecting .qml in build directories or virtual environments
|
|
303
|
+
for path in project_root.rglob("*.qml"):
|
|
304
|
+
rel_parts = path.relative_to(project_root).parts
|
|
305
|
+
if any(part in ("build", "dist", ".git", ".venv", "venv", "temp") for part in rel_parts):
|
|
306
|
+
continue
|
|
307
|
+
return True
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def _resolve_competing_framework_exclusions(
|
|
311
|
+
self,
|
|
312
|
+
binding: Binding,
|
|
313
|
+
manifest: FreezeManifest,
|
|
314
|
+
) -> List[str]:
|
|
315
|
+
"""
|
|
316
|
+
Automatically excludes competing UI framework packages from PyInstaller scanning
|
|
317
|
+
so users never need to manually list 'kivy', 'kivy_install', etc. in exclude_modules.
|
|
318
|
+
"""
|
|
319
|
+
exclusions: List[str] = []
|
|
320
|
+
user_excludes = {m.lower() for m in manifest.optimization.exclude_modules}
|
|
321
|
+
|
|
322
|
+
# If active binding is Qt-based (PySide6, PyQt6, PyQt5, PySide2)
|
|
323
|
+
if binding.is_pyside or binding in (Binding.PYQT6, Binding.PYQT5):
|
|
324
|
+
competing = ["kivy", "kivy_install", "tkinter"]
|
|
325
|
+
if binding != Binding.PYSIDE6:
|
|
326
|
+
competing.append("PySide6")
|
|
327
|
+
if binding != Binding.PYQT6:
|
|
328
|
+
competing.append("PyQt6")
|
|
329
|
+
if binding != Binding.PYQT5:
|
|
330
|
+
competing.append("PyQt5")
|
|
331
|
+
if binding != Binding.PYSIDE2:
|
|
332
|
+
competing.append("PySide2")
|
|
333
|
+
|
|
334
|
+
for mod in competing:
|
|
335
|
+
if mod.lower() not in user_excludes:
|
|
336
|
+
exclusions.extend(["--exclude-module", mod])
|
|
337
|
+
|
|
338
|
+
# If active binding is Kivy
|
|
339
|
+
elif binding == Binding.KIVY:
|
|
340
|
+
competing = ["PySide6", "PyQt6", "PyQt5", "PySide2", "tkinter"]
|
|
341
|
+
for mod in competing:
|
|
342
|
+
if mod.lower() not in user_excludes:
|
|
343
|
+
exclusions.extend(["--exclude-module", mod])
|
|
344
|
+
|
|
345
|
+
# If active binding is Tkinter
|
|
346
|
+
elif binding == Binding.TKINTER:
|
|
347
|
+
competing = ["kivy", "kivy_install", "PySide6", "PyQt6", "PyQt5", "PySide2"]
|
|
348
|
+
for mod in competing:
|
|
349
|
+
if mod.lower() not in user_excludes:
|
|
350
|
+
exclusions.extend(["--exclude-module", mod])
|
|
351
|
+
|
|
352
|
+
return exclusions
|
|
File without changes
|