xppb 1.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.
xppb/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """A blazing-fast cross-platform Python bundling pipeline tool."""
2
+
3
+ __version__ = "1.0.0"
xppb/launcher_stub.c ADDED
@@ -0,0 +1,30 @@
1
+ #include <windows.h>
2
+ #include <stdio.h>
3
+ #include <string.h>
4
+
5
+ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
6
+ char exePath[MAX_PATH];
7
+ GetModuleFileNameA(NULL, exePath, MAX_PATH);
8
+
9
+ char* lastSlash = strrchr(exePath, '\\');
10
+ if (lastSlash != NULL) {
11
+ *lastSlash = '\0';
12
+ }
13
+
14
+ STARTUPINFO si;
15
+ PROCESS_INFORMATION pi;
16
+ ZeroMemory(&si, sizeof(si));
17
+ si.cb = sizeof(si);
18
+ ZeroMemory(&pi, sizeof(pi));
19
+
20
+ char cmd[32768];
21
+ // We use tokens here that Python will replace before compilation
22
+ snprintf(cmd, sizeof(cmd), "\"%s\\python\\__PYTHON_EXE__\" \"%s\\__ENTRY_POINT__\" %s", exePath, exePath, lpCmdLine);
23
+
24
+ if (CreateProcessA(NULL, cmd, NULL, NULL, FALSE, __CREATE_WINDOW_FLAG__, NULL, NULL, &si, &pi)) {
25
+ WaitForSingleObject(pi.hProcess, INFINITE);
26
+ CloseHandle(pi.hProcess);
27
+ CloseHandle(pi.hThread);
28
+ }
29
+ return 0;
30
+ }
xppb/xppb.py ADDED
@@ -0,0 +1,638 @@
1
+ import tomllib
2
+ import os
3
+ import sys
4
+ import shutil
5
+ import urllib.request
6
+ import tarfile
7
+ import zipfile
8
+ import subprocess
9
+ import platform
10
+ from pathlib import Path
11
+ import concurrent.futures
12
+ import tempfile
13
+ import contextlib
14
+
15
+ @contextlib.contextmanager
16
+ def mock_target_environment(platform_name):
17
+ """
18
+ Temporarily tricks the host Python environment and ModuleGraph into
19
+ evaluating logic as if it were running natively on the target OS.
20
+ """
21
+ orig_platform = sys.platform
22
+ orig_os = os.name
23
+ orig_builtins = sys.builtin_module_names
24
+ target_os = platform_name.lower()
25
+ if 'windows' in target_os:
26
+ sys.platform = 'win32'
27
+ os.name = 'nt'
28
+ sys.builtin_module_names = tuple(set(orig_builtins) | {'winreg', 'msvcrt', '_winapi', 'nt'})
29
+ elif 'macos' in target_os:
30
+ sys.platform = 'darwin'
31
+ os.name = 'posix'
32
+ sys.builtin_module_names = tuple(set(orig_builtins) | {'posix'})
33
+ elif 'linux' in target_os:
34
+ sys.platform = 'linux'
35
+ os.name = 'posix'
36
+ sys.builtin_module_names = tuple(set(orig_builtins) | {'posix'})
37
+ try:
38
+ yield
39
+ finally:
40
+ sys.platform = orig_platform
41
+ os.name = orig_os
42
+ sys.builtin_module_names = orig_builtins
43
+ try:
44
+ from modulegraph.modulegraph import ModuleGraph
45
+ except ImportError:
46
+ print("[-] Error: 'modulegraph' is required for advanced dependency analysis.")
47
+ print(' Install it via: pip install modulegraph')
48
+ sys.exit(1)
49
+
50
+ def generate_runtime_whitelist(entry_point_path, site_packages_path, stdlib_path, platform_name, hidden_imports=None, collect_all=None):
51
+ """
52
+ Uses ModuleGraph to trace required files, catching C-extensions and dynamic imports,
53
+ with an optimized, precise package file-level whitelisting system.
54
+ """
55
+ if hidden_imports is None:
56
+ hidden_imports = []
57
+ if collect_all is None:
58
+ collect_all = []
59
+ print(f' -> Tracing application dependencies via ModuleGraph (Targeting: {platform_name})...')
60
+ target_paths = [str(site_packages_path), str(stdlib_path)]
61
+ graph = ModuleGraph(path=target_paths)
62
+ scripts_to_trace = [str(entry_point_path)]
63
+ if hidden_imports:
64
+ print(f' [*] Resolving {len(hidden_imports)} hidden imports...')
65
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_script:
66
+ for mod in hidden_imports:
67
+ temp_script.write(f'import {mod}\n')
68
+ temp_script_path = temp_script.name
69
+ scripts_to_trace.append(temp_script_path)
70
+ try:
71
+ with mock_target_environment(platform_name):
72
+ graph = ModuleGraph(path=target_paths)
73
+ for script in scripts_to_trace:
74
+ graph.run_script(script)
75
+ finally:
76
+ if hidden_imports and os.path.exists(temp_script_path):
77
+ os.remove(temp_script_path)
78
+ whitelist = set()
79
+ missing_modules = set()
80
+ base_paths = [Path(site_packages_path).resolve(), Path(stdlib_path).resolve()]
81
+
82
+ def smart_whitelist_add(p):
83
+ """Surgical path addition: breaks the parent directory shield."""
84
+ p = Path(p).resolve()
85
+ if p.is_dir():
86
+ for sub_p in p.rglob('*'):
87
+ if sub_p.is_file():
88
+ smart_whitelist_add(sub_p)
89
+ return
90
+ whitelist.add(p)
91
+ parent = p.parent
92
+ while parent:
93
+ if not any((base in parent.parents or parent == base for base in base_paths)):
94
+ break
95
+ for init_name in ['__init__.py', '__init__.pyc']:
96
+ init_file = parent / init_name
97
+ if init_file.exists():
98
+ whitelist.add(init_file.resolve())
99
+ parent = parent.parent
100
+ for node in graph.nodes():
101
+ if hasattr(node, 'filename') and node.filename:
102
+ smart_whitelist_add(node.filename)
103
+ if hasattr(node, 'packagepath') and node.packagepath:
104
+ for pkg_dir in node.packagepath:
105
+ if pkg_dir:
106
+ smart_whitelist_add(pkg_dir)
107
+ if type(node).__name__ == 'MissingModule':
108
+ missing_modules.add(node.identifier)
109
+ essential_modules = ['site.py', 'os.py', 'stat.py', 'genericpath.py', 'encodings', 'codecs.py', 'io.py', 'abc.py', '_collections_abc.py', 'sitecustomize.py', 'importlib']
110
+ target_os = platform_name.lower()
111
+ if 'windows' in target_os:
112
+ essential_modules.extend(['ntpath.py', 'nt.py', '_winapi', 'msvcrt', 'winreg', 'socket.py', '_socket'])
113
+ else:
114
+ essential_modules.extend(['posixpath.py', 'posix.py', 'fcntl', 'termios', 'socket.py', '_socket'])
115
+ for essential in essential_modules:
116
+ for p in Path(stdlib_path).rglob(essential):
117
+ smart_whitelist_add(p)
118
+ print(' [*] Scanning compiled C-extensions for implicit dynamic imports...')
119
+ target_files = {}
120
+ for search_dir in base_paths:
121
+ for p in search_dir.rglob('*.py'):
122
+ target_files[p.stem] = p
123
+ rescued_binary_deps = 0
124
+ for file_path in list(whitelist):
125
+ if file_path.suffix.lower() in {'.so', '.pyd'}:
126
+ try:
127
+ with open(file_path, 'rb') as f:
128
+ data = f.read()
129
+ for s in data.split(b'\x00'):
130
+ if 2 < len(s) < 50 and s.replace(b'.', b'').replace(b'_', b'').isalnum():
131
+ try:
132
+ mod_string = s.decode('ascii')
133
+ base_name = mod_string.split('.')[-1]
134
+ if base_name in target_files and target_files[base_name] not in whitelist:
135
+ smart_whitelist_add(target_files[base_name])
136
+ rescued_binary_deps += 1
137
+ except UnicodeDecodeError:
138
+ pass
139
+ except OSError:
140
+ pass
141
+ if rescued_binary_deps > 0:
142
+ print(f' [+] Binary scanner uncovered {rescued_binary_deps} hidden C-extension dependencies.')
143
+ if missing_modules:
144
+ print(f' [*] Cross-compilation check: Attempting to rescue {len(missing_modules)} unresolved modules from target runtime...')
145
+ target_files = {}
146
+ for search_dir in base_paths:
147
+ for p in search_dir.rglob('*'):
148
+ if p.is_file():
149
+ target_files[p.stem] = p
150
+ rescued_count = 0
151
+ for bad_mod in missing_modules:
152
+ base_name = bad_mod.split('.')[-1]
153
+ if base_name in target_files:
154
+ smart_whitelist_add(target_files[base_name])
155
+ rescued_count += 1
156
+ if rescued_count > 0:
157
+ print(f' [+] Rescued {rescued_count} platform-specific modules from the cutting room floor.')
158
+ if collect_all:
159
+ print(f" [*] Applying 'collect_all' override for {len(collect_all)} dynamic packages...")
160
+ for pkg_name in collect_all:
161
+ pkg_path = Path(site_packages_path) / pkg_name
162
+ if pkg_path.exists() and pkg_path.is_dir():
163
+ smart_whitelist_add(pkg_path)
164
+ for metadata_dir in Path(site_packages_path).glob(f'{pkg_name}-*.*-info'):
165
+ smart_whitelist_add(metadata_dir)
166
+ else:
167
+ print(f" [!] Warning: collect_all package '{pkg_name}' not found. Did you forget to list it in DEPENDENCIES?")
168
+ return whitelist
169
+
170
+ def prune_runtime_bloat_strict(runtime_path, whitelist, site_packages_path, stdlib_path, extra_extensions=None):
171
+ """
172
+ Deletes EVERY file within the code-distribution scopes that is NOT in the whitelist,
173
+ while safely preserving critical non-Python data assets, compiled extensions,
174
+ and surgical metadata blocks for active packages.
175
+ """
176
+ print(' -> Executing radical whitelisting (Strict Surgical Prune with Asset & Metadata Rescue)...')
177
+ runtime_path = Path(runtime_path)
178
+ bytes_saved = 0
179
+ preserve_extensions = {'.so', '.pyd', '.dylib', '.dll', '.pem', '.crt', '.json', '.yaml', '.yml', '.txt', '.csv', '.tcl', '.tk'}
180
+ if extra_extensions:
181
+ preserve_extensions.update((ext.lower() if ext.startswith('.') else f'.{ext.lower()}' for ext in extra_extensions))
182
+ target_zones = [Path(site_packages_path).resolve(), Path(stdlib_path).resolve()]
183
+ active_package_dirs = {p.parent.resolve() for p in whitelist}
184
+ active_module_names = set()
185
+ for p in whitelist:
186
+ if p.parent in target_zones:
187
+ active_module_names.add(p.stem.lower().replace('_', ''))
188
+ else:
189
+ for parent in p.parents:
190
+ if parent.parent in target_zones:
191
+ active_module_names.add(parent.name.lower().replace('_', ''))
192
+ break
193
+ for zone in target_zones:
194
+ if not zone.exists():
195
+ continue
196
+ for file_path in zone.rglob('*'):
197
+ if not file_path.is_file() and (not file_path.is_symlink()):
198
+ continue
199
+ resolved_file = file_path.resolve()
200
+ if resolved_file in whitelist:
201
+ continue
202
+ if resolved_file.suffix.lower() in preserve_extensions:
203
+ is_active_asset = False
204
+ parent = resolved_file.parent
205
+ while parent and parent != zone and (parent != parent.parent):
206
+ if parent in active_package_dirs:
207
+ is_active_asset = True
208
+ break
209
+ parent = parent.parent
210
+ if is_active_asset:
211
+ whitelist.add(resolved_file)
212
+ continue
213
+ is_metadata_file = False
214
+ for parent in resolved_file.parents:
215
+ if parent == zone:
216
+ break
217
+ if parent.name.endswith('.dist-info') or parent.name.endswith('.egg-info'):
218
+ critical_meta_files = {'METADATA', 'entry_points.txt', 'top_level.txt', 'PKG-INFO'}
219
+ if resolved_file.name in critical_meta_files:
220
+ meta_base_name = parent.name.split('-')[0].lower().replace('_', '')
221
+ if meta_base_name in active_module_names:
222
+ is_metadata_file = True
223
+ whitelist.add(resolved_file)
224
+ break
225
+ if is_metadata_file:
226
+ continue
227
+ try:
228
+ bytes_saved += file_path.stat().st_size
229
+ file_path.unlink()
230
+ except OSError:
231
+ pass
232
+ for dir_path in sorted(runtime_path.rglob('*'), key=lambda x: len(x.parts), reverse=True):
233
+ if dir_path.is_dir() and (not any(dir_path.iterdir())):
234
+ try:
235
+ dir_path.rmdir()
236
+ except OSError:
237
+ pass
238
+ print(f' [+] Pruning complete. Reclaimed ~{bytes_saved / (1024 * 1024):.1f} MB.')
239
+
240
+ def load_configuration(config_filename='projconf.toml'):
241
+ """Reads and maps the external TOML configuration layer."""
242
+ config_path = Path(config_filename)
243
+ if not config_path.exists():
244
+ print(f"[-] Error: Configuration file '{config_filename}' not found in the current working directory.")
245
+ sys.exit(1)
246
+ print(f'Loading environment manifest: {config_filename}')
247
+ with open(config_path, 'rb') as f:
248
+ try:
249
+ toml_data = tomllib.load(f)
250
+ return {'PROJECT_NAME': toml_data['project']['name'], 'VERSION': toml_data['project']['version'], 'PYTHON_VERSION': toml_data['project'].get('python_version', '3.11'), 'SOURCE_FILES': toml_data['project'].get('source_files', []), 'ENTRY_POINT': toml_data['project']['entry_point'], 'DEPENDENCIES': toml_data['project'].get('dependencies', []), 'HIDDEN_IMPORTS': toml_data['project'].get('hidden_imports', []), 'COLLECT_ALL': toml_data['project'].get('collect_all', []), 'LAUNCH_COMMAND': toml_data['project']['launch_command'], 'PRESERVE_EXTENSIONS': toml_data['project'].get('preserve_extensions', []), 'RUNTIMES': toml_data['runtimes'], 'WINDOWS_CONFIG': toml_data.get('windows', {}), 'MACOS_CONFIG': toml_data.get('macos', {}), 'LINUX_CONFIG': toml_data.get('linux', {})}
251
+ except KeyError as e:
252
+ print(f'[-] Configuration Error: Missing required TOML key definition {e}')
253
+ sys.exit(1)
254
+ except Exception as e:
255
+ print(f'[-] Syntax Error parsing TOML file: {e}')
256
+ sys.exit(1)
257
+
258
+ def find_site_packages(base_path):
259
+ """Recursively crawls the runtime structure to locate the vendor folder."""
260
+ for root, dirs, _ in os.walk(base_path):
261
+ if os.path.basename(root) == 'site-packages':
262
+ return root
263
+ return None
264
+
265
+ def download_runtime(url, dest_path):
266
+ """
267
+ Downloads the python runtime, utilizing a global cache to save bandwidth
268
+ and significantly speed up subsequent builds.
269
+ """
270
+ CACHE_DIR = Path.home() / '.core_bundler_cache' / 'runtimes'
271
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
272
+ filename = url.split('/')[-1]
273
+ cached_file = CACHE_DIR / filename
274
+ if cached_file.exists():
275
+ print(f' [+] Cache hit! Using local runtime archive: {filename}')
276
+ shutil.copy2(cached_file, dest_path)
277
+ return
278
+ print(f' [~] Downloading runtime to cache from {url}...')
279
+ try:
280
+ urllib.request.urlretrieve(url, cached_file)
281
+ print(f' [+] Download complete. Cached permanently at {CACHE_DIR}')
282
+ shutil.copy2(cached_file, dest_path)
283
+ except Exception as e:
284
+ print(f' [!] Failed to download runtime: {e}')
285
+ if cached_file.exists():
286
+ cached_file.unlink()
287
+ sys.exit(1)
288
+
289
+ def extract_runtime(archive_path, extract_to):
290
+ """Extracts the .tar.gz bundle into the workspace securely, preventing path traversal attacks."""
291
+ print(f' -> Unpacking engine files securely...')
292
+ extract_to_path = Path(extract_to).resolve()
293
+ with tarfile.open(archive_path, 'r:gz') as tar:
294
+ if hasattr(tarfile, 'data_filter'):
295
+ try:
296
+ tar.extractall(path=extract_to_path, filter='data')
297
+ return
298
+ except (TypeError, ValueError):
299
+ pass
300
+ safe_members = []
301
+ for member in tar.getmembers():
302
+ target_path = Path(os.path.abspath(os.path.join(extract_to_path, member.name)))
303
+ if extract_to_path not in target_path.parents and target_path != extract_to_path:
304
+ print(f"[-] Malicious Archive Detected: Refusing to extract '{member.name}' (outside boundary).")
305
+ sys.exit(1)
306
+ safe_members.append(member)
307
+ tar.extractall(path=extract_to_path, members=safe_members)
308
+
309
+ def download_and_extract_deps(deps, target_site_packages, temp_dir, platform_key, python_version):
310
+ """Resolves and properly installs specified packages using uv (if available) or pip."""
311
+ if not deps:
312
+ return
313
+ platform_map = {'windows-x64': {'platform': 'win_amd64', 'abi': f"cp{python_version.replace('.', '')}"}, 'windows': {'platform': 'win_amd64', 'abi': f"cp{python_version.replace('.', '')}"}, 'linux-x64': {'platform': 'manylinux2014_x86_64', 'abi': f"cp{python_version.replace('.', '')}"}, 'linux': {'platform': 'manylinux2014_x86_64', 'abi': f"cp{python_version.replace('.', '')}"}, 'macos-x64': {'platform': 'macosx_10_9_x86_64', 'abi': f"cp{python_version.replace('.', '')}"}, 'macos-arm64': {'platform': 'macosx_11_0_arm64', 'abi': f"cp{python_version.replace('.', '')}"}}
314
+ lookup_key = platform_key.lower()
315
+ plat_info = platform_map.get(lookup_key)
316
+ print(f" -> Syncing dependencies for {platform_key} (Targeting Python {python_version}): {', '.join(deps)}...")
317
+ uv_bin = shutil.which('uv')
318
+ if uv_bin:
319
+ print(" [*] Acceleration Engaged: Routing dependency resolution through 'uv'...")
320
+ cmd = [uv_bin, 'pip', 'install', '--target', str(target_site_packages), '--only-binary=:all:']
321
+ else:
322
+ cmd = [sys.executable, '-m', 'pip', 'install', '--target', str(target_site_packages), '--only-binary=:all:', '--no-warn-script-location']
323
+ if plat_info:
324
+ cmd.extend(['--platform', plat_info['platform'], '--abi', plat_info['abi'], '--python-version', python_version, '--implementation', 'cp'])
325
+ cmd.extend(deps)
326
+ try:
327
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
328
+ except subprocess.CalledProcessError:
329
+ installer = 'uv' if uv_bin else 'pip'
330
+ print(f" [!] Error: {installer} failed to resolve or install target variants for: {', '.join(deps)}")
331
+ raise RuntimeError(f'Dependency resolution failed via {installer}.')
332
+
333
+ def generate_info_plist(dest_path, project_name, version):
334
+ """Generates and writes a compliant macOS Info.plist file."""
335
+ plist_content = f'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>CFBundlePackageType</key>\n <string>APPL</string>\n <key>CFBundleInfoDictionaryVersion</key>\n <string>6.0</string>\n <key>CFBundleName</key>\n <string>{project_name}</string>\n <key>CFBundleExecutable</key>\n <string>{project_name}</string>\n <key>CFBundleIdentifier</key>\n <string>com.standalone.{project_name.lower()}</string>\n <key>CFBundleShortVersionString</key>\n <string>{version}</string>\n <key>CFBundleVersion</key>\n <string>{version}</string>\n <key>LSMinimumSystemVersion</key>\n <string>10.13</string>\n</dict>\n</plist>\n'
336
+ dest_path.write_text(plist_content)
337
+
338
+ def find_windows_compiler():
339
+ """Detects native toolchains or cross-compilers on the host platform."""
340
+ for compiler in ['gcc', 'x86_64-w64-mingw32-gcc', 'cl']:
341
+ if shutil.which(compiler):
342
+ return compiler
343
+ return None
344
+
345
+ def find_signtool():
346
+ """Recursively searches traditional Windows SDK locations for signtool.exe."""
347
+ if shutil.which('signtool'):
348
+ return 'signtool'
349
+ possible_roots = [Path('C:/Program Files (x86)/Windows Kits/10/bin'), Path('C:/Program Files/Windows Kits/10/bin'), Path('C:/Program Files (x86)/Microsoft SDKs/ClickOnce/SignTool')]
350
+ for root in possible_roots:
351
+ if not root.exists():
352
+ continue
353
+ if root.name == 'SignTool':
354
+ exe = root / 'signtool.exe'
355
+ if exe.exists():
356
+ return str(exe)
357
+ else:
358
+ for sub in root.iterdir():
359
+ if sub.is_dir():
360
+ for arch in ['x64', 'x86']:
361
+ exe = sub / arch / 'signtool.exe'
362
+ if exe.exists():
363
+ return str(exe)
364
+ return None
365
+
366
+ def sign_windows_executable(target_file, win_config):
367
+ """Integrates code signing parameters if a certificate context is provided."""
368
+ cert_path = win_config.get('pfx_certificate')
369
+ cert_password = win_config.get('pfx_password')
370
+ if not cert_path or not cert_password:
371
+ return
372
+ signtool_bin = find_signtool()
373
+ if not signtool_bin:
374
+ print(' [!] Warning: signtool.exe missing from common paths. Skipping signature routine.')
375
+ return
376
+ print(f' -> Code signing target binary: {target_file.name}...')
377
+ cmd = [signtool_bin, 'sign', '/f', str(cert_path), '/p', cert_password, '/tr', 'http://timestamp.digicert.com', '/td', 'sha256', '/fd', 'sha256', str(target_file)]
378
+ try:
379
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
380
+ print('[+] Code signature successfully embedded.')
381
+ except subprocess.CalledProcessError:
382
+ print(' [!] Error: SignTool invocation failed. Verify certificate properties.')
383
+
384
+ def generate_windows_launcher(platform_dir, project_name, entry_point, win_config):
385
+ """Builds a zero-flash compiled binary using decoupled source files and guaranteed cleanup paths."""
386
+ hide_console = win_config.get('hide_console', False)
387
+ compiler = find_windows_compiler()
388
+ launcher_exe = platform_dir / f'{project_name}.exe'
389
+ entry_point_normalized = entry_point.replace('\\', '/')
390
+ entry_point_fixed = entry_point_normalized.replace('/', '\\\\')
391
+ python_exe = 'pythonw.exe' if hide_console else 'python.exe'
392
+ create_window_flag = 'CREATE_NO_WINDOW' if hide_console else '0'
393
+ if compiler:
394
+ print(f' -> Found build engine ({compiler}). Compiling native Win32 executable launcher...')
395
+ c_source_path = platform_dir / '_launcher.c'
396
+ stub_path = Path(__file__).parent / "launcher_stub.c"
397
+ if not stub_path.exists():
398
+ print(" [!] Error: 'launcher_stub.c' template missing. Reverting to fallback wrappers...")
399
+ compiler = None
400
+ else:
401
+ try:
402
+ raw_c_code = stub_path.read_text()
403
+ c_code = raw_c_code.replace('__PYTHON_EXE__', python_exe)
404
+ c_code = c_code.replace('__ENTRY_POINT__', entry_point_fixed)
405
+ c_code = c_code.replace('__CREATE_WINDOW_FLAG__', create_window_flag)
406
+ c_source_path.write_text(c_code)
407
+ if compiler == 'cl':
408
+ subprocess.run(['cl.exe', '/O2', f'/Fe:{launcher_exe}', str(c_source_path), '/link', '/SUBSYSTEM:WINDOWS'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
409
+ else:
410
+ build_cmd = [compiler, '-O2', str(c_source_path), '-o', str(launcher_exe)]
411
+ if hide_console:
412
+ build_cmd.append('-mwindows')
413
+ subprocess.run(build_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
414
+ return launcher_exe
415
+ except Exception:
416
+ print(' [!] Compilation pipeline exception. Reverting to structural fallback scripts...')
417
+ if launcher_exe.exists():
418
+ launcher_exe.unlink()
419
+ finally:
420
+ if c_source_path.exists():
421
+ c_source_path.unlink()
422
+ for junk in [platform_dir / '_launcher.obj', platform_dir / f'{project_name}.obj']:
423
+ if junk.exists():
424
+ junk.unlink()
425
+ print(' -> Constructing basic script wrapper (No native compiler available)...')
426
+ launcher_bat = platform_dir / f'{project_name}.bat'
427
+ bat_content = f'@echo off\nsetlocal\n"%~dp0python\\{python_exe}" "%~dp0{entry_point}" %*\nendlocal\n'
428
+ launcher_bat.write_text(bat_content)
429
+ if hide_console:
430
+ vbs_launcher = platform_dir / f'{project_name}.vbs'
431
+ vbs_content = f'Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run chr(34) & "{project_name}.bat" & chr(34), 0\nSet WshShell = Nothing\n'
432
+ vbs_launcher.write_text(vbs_content)
433
+ return vbs_launcher
434
+ return launcher_bat
435
+
436
+ def ensure_rcodesign():
437
+ """Locates or automatically downloads the cross-platform apple-codesign tool binary."""
438
+ system_path = shutil.which('rcodesign')
439
+ if system_path:
440
+ return system_path
441
+ tools_dir = Path('build/tools')
442
+ tools_dir.mkdir(parents=True, exist_ok=True)
443
+ host_sys = sys.platform
444
+ version = '0.29.0'
445
+ base_url = f'https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F{version}/'
446
+ if host_sys == 'win32':
447
+ asset_name = f'apple-codesign-{version}-x86_64-pc-windows-msvc.zip'
448
+ binary_name = 'rcodesign.exe'
449
+ elif host_sys == 'darwin':
450
+ asset_name = f'apple-codesign-{version}-macos-universal.tar.gz'
451
+ binary_name = 'rcodesign'
452
+ else:
453
+ asset_name = f'apple-codesign-{version}-x86_64-unknown-linux-musl.tar.gz'
454
+ binary_name = 'rcodesign'
455
+ local_binary = tools_dir / binary_name
456
+ if local_binary.exists():
457
+ return str(local_binary)
458
+ print(f' -> Apple tools missing. Auto-fetching cross-platform rcodesign v{version} for host pipeline...')
459
+ archive_dest = tools_dir / asset_name
460
+ try:
461
+ req = urllib.request.Request(base_url + asset_name, headers={'User-Agent': 'Mozilla/5.0'})
462
+ with urllib.request.urlopen(req) as response, open(archive_dest, 'wb') as out_file:
463
+ shutil.copyfileobj(response, out_file)
464
+ if asset_name.endswith('.zip'):
465
+ with zipfile.ZipFile(archive_dest, 'r') as zip_ref:
466
+ zip_ref.extractall(tools_dir)
467
+ else:
468
+ with tarfile.open(archive_dest, 'r:gz') as tar:
469
+ tar.extractall(path=tools_dir)
470
+ for root, _, files in os.walk(tools_dir):
471
+ if binary_name in files:
472
+ target = Path(root) / binary_name
473
+ if target != local_binary:
474
+ shutil.move(str(target), str(local_binary))
475
+ break
476
+ if local_binary.exists():
477
+ os.chmod(local_binary, 493)
478
+ if archive_dest.exists():
479
+ archive_dest.unlink()
480
+ return str(local_binary)
481
+ except Exception as e:
482
+ print(f' [!] Failed to download cross-platform signing dependency: {e}')
483
+ sys.exit(1)
484
+
485
+ def sign_macos_bundle_cross_platform(app_path, mac_config, rcodesign_bin):
486
+ """Signs a macOS .app bundle structure flawlessly from any supported OS platform context."""
487
+ p12_cert = mac_config.get('p12_certificate')
488
+ p12_pass = mac_config.get('p12_password')
489
+ if not p12_cert:
490
+ print(' [-] Skipping macOS signing engine: No .p12 certificate file defined.')
491
+ return
492
+ print(f' -> Cryptographically signing macOS application framework: {app_path.name}...')
493
+ cmd = [rcodesign_bin, 'sign', '--p12-file', str(p12_cert), '--p12-password', str(p12_pass), '--code-signature-flags', 'runtime', str(app_path)]
494
+ try:
495
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
496
+ print(' [+] Apple Developer signature successfully validated and compiled.')
497
+ except subprocess.CalledProcessError as e:
498
+ print(f' [!] Error: rcodesign execution error:\n{e.stderr}')
499
+ sys.exit(1)
500
+
501
+ def notarize_macos_bundle_cross_platform(app_path, mac_config, rcodesign_bin):
502
+ """Submits the payload to Apple Notary API and staples tickets directly inside non-macOS setups."""
503
+ api_key = mac_config.get('api_key_path')
504
+ api_issuer = mac_config.get('api_issuer_id')
505
+ api_key_id = mac_config.get('api_key_id')
506
+ if not all([api_key, api_issuer, api_key_id]):
507
+ print(' [-] Skipping Apple Notarization: ASC API Keys are incomplete.')
508
+ return
509
+ print(f' -> Submitting bundle to Apple Notary API servers (Cross-Platform Connection)...')
510
+ cmd = [rcodesign_bin, 'notary-submit', '--api-key-path', str(api_key), '--api-issuer', str(api_issuer), '--api-key', str(api_key_id), '--staple', str(app_path)]
511
+ try:
512
+ subprocess.run(cmd, check=True)
513
+ print(' [+] Notarization ticket acquired and stapled into app bundle.')
514
+ except subprocess.CalledProcessError:
515
+ print(' [!] Error: Apple Notary Service API transaction failed.')
516
+ sys.exit(1)
517
+
518
+ def bundle_platform(platform_name, url, build_dir, dist_dir, base_temp_dir, CONFIG):
519
+ """Encapsulates the build process for a single target platform to allow concurrent execution."""
520
+ print(f'\n[Target Platform: {platform_name}] Process started...')
521
+ platform_dir = build_dir / platform_name
522
+ platform_dir.mkdir(parents=True, exist_ok=True)
523
+ archive_path = build_dir / f'engine_{platform_name}.tar.gz'
524
+ temp_dir = base_temp_dir / platform_name
525
+ temp_dir.mkdir(parents=True, exist_ok=True)
526
+ is_macos = 'macos' in platform_name.lower()
527
+ if is_macos:
528
+ app_bundle = platform_dir / f"{CONFIG['PROJECT_NAME']}.app"
529
+ contents_dir = app_bundle / 'Contents'
530
+ macos_dir = contents_dir / 'MacOS'
531
+ resources_dir = contents_dir / 'Resources'
532
+ macos_dir.mkdir(parents=True, exist_ok=True)
533
+ resources_dir.mkdir(parents=True, exist_ok=True)
534
+ runtime_extract_target = resources_dir
535
+ source_dest_parent = resources_dir
536
+ else:
537
+ runtime_extract_target = platform_dir
538
+ source_dest_parent = platform_dir
539
+ download_runtime(url, archive_path)
540
+ extract_runtime(archive_path, runtime_extract_target)
541
+ site_packages = find_site_packages(runtime_extract_target)
542
+ if not site_packages:
543
+ print(f' [!] Error: Failed to pinpoint target environment layout for {platform_name}!')
544
+ return
545
+ download_and_extract_deps(CONFIG['DEPENDENCIES'], site_packages, temp_dir, platform_name, python_version=CONFIG['PYTHON_VERSION'])
546
+ stdlib_path = Path(site_packages).parent
547
+ active_hidden = list(CONFIG.get('HIDDEN_IMPORTS', []))
548
+ active_collect = list(CONFIG.get('COLLECT_ALL', []))
549
+ plat_lower = platform_name.lower()
550
+ if 'windows' in plat_lower:
551
+ active_hidden.extend(CONFIG['WINDOWS_CONFIG'].get('hidden_imports', []))
552
+ active_collect.extend(CONFIG['WINDOWS_CONFIG'].get('collect_all', []))
553
+ elif 'macos' in plat_lower:
554
+ active_hidden.extend(CONFIG['MACOS_CONFIG'].get('hidden_imports', []))
555
+ active_collect.extend(CONFIG['MACOS_CONFIG'].get('collect_all', []))
556
+ elif 'linux' in plat_lower:
557
+ active_hidden.extend(CONFIG['LINUX_CONFIG'].get('hidden_imports', []))
558
+ active_collect.extend(CONFIG['LINUX_CONFIG'].get('collect_all', []))
559
+ whitelist = generate_runtime_whitelist(entry_point_path=Path(CONFIG['ENTRY_POINT']), site_packages_path=Path(site_packages), stdlib_path=stdlib_path, platform_name=platform_name, hidden_imports=active_hidden, collect_all=active_collect)
560
+ prune_runtime_bloat_strict(runtime_path=runtime_extract_target, whitelist=whitelist, site_packages_path=site_packages, stdlib_path=stdlib_path, extra_extensions=CONFIG.get('PRESERVE_EXTENSIONS', []))
561
+ print(f' -> [{platform_name}] Packaging custom source layers...')
562
+ for src in CONFIG['SOURCE_FILES']:
563
+ src_path = Path(src)
564
+ if not src_path.exists():
565
+ print(f" [!] Missing source path ignored: '{src}'")
566
+ continue
567
+ dest_path = source_dest_parent / src_path.name
568
+ if src_path.is_dir():
569
+ shutil.copytree(src_path, dest_path)
570
+ else:
571
+ shutil.copy2(src_path, dest_path)
572
+ print(f' -> [{platform_name}] Constructing isolated binary wrappers...')
573
+ if 'windows' in platform_name:
574
+ launcher_path = generate_windows_launcher(platform_dir, CONFIG['PROJECT_NAME'], CONFIG['ENTRY_POINT'], CONFIG['WINDOWS_CONFIG'])
575
+ sign_windows_executable(launcher_path, CONFIG['WINDOWS_CONFIG'])
576
+ elif is_macos:
577
+ launcher_path = macos_dir / CONFIG['PROJECT_NAME']
578
+ launch_cmd = CONFIG['LAUNCH_COMMAND'].format(ENTRY_POINT=f"$SCRIPT_DIR/../Resources/{CONFIG['ENTRY_POINT']}")
579
+ sh_content = f'#!/usr/bin/env bash\nSCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"\n"$SCRIPT_DIR/../Resources/python/bin/python3" {launch_cmd} "$@"\n'
580
+ launcher_path.write_text(sh_content)
581
+ generate_info_plist(contents_dir / 'Info.plist', CONFIG['PROJECT_NAME'], CONFIG['VERSION'])
582
+ os.chmod(launcher_path, 493)
583
+ python_bin = resources_dir / 'python' / 'bin' / 'python3'
584
+ if python_bin.exists():
585
+ os.chmod(python_bin, 493)
586
+ rcodesign_bin = ensure_rcodesign()
587
+ sign_macos_bundle_cross_platform(app_bundle, CONFIG['MACOS_CONFIG'], rcodesign_bin)
588
+ notarize_macos_bundle_cross_platform(app_bundle, CONFIG['MACOS_CONFIG'], rcodesign_bin)
589
+ else:
590
+ launcher_path = platform_dir / CONFIG['PROJECT_NAME']
591
+ launch_cmd = CONFIG['LAUNCH_COMMAND'].format(ENTRY_POINT=f"$SCRIPT_DIR/{CONFIG['ENTRY_POINT']}")
592
+ sh_content = f'#!/usr/bin/env bash\nSCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"\n"$SCRIPT_DIR/python/bin/python3" {launch_cmd} "$@"\n'
593
+ launcher_path.write_text(sh_content)
594
+ os.chmod(launcher_path, 493)
595
+ python_bin = platform_dir / 'python' / 'bin' / 'python3'
596
+ if python_bin.exists():
597
+ os.chmod(python_bin, 493)
598
+ print(f' -> [{platform_name}] Sealing compressed release asset...')
599
+ dist_base_name = dist_dir / f"{CONFIG['PROJECT_NAME']}-{CONFIG['VERSION']}-{platform_name}"
600
+ if 'windows' in platform_name:
601
+ shutil.make_archive(str(dist_base_name), 'zip', root_dir=platform_dir)
602
+ print(f' [+] Archive ready: {dist_base_name}.zip')
603
+ else:
604
+ shutil.make_archive(str(dist_base_name), 'gztar', root_dir=platform_dir)
605
+ print(f' [+] Archive ready: {dist_base_name}.tar.gz')
606
+ if archive_path.exists():
607
+ archive_path.unlink()
608
+
609
+ def main():
610
+ CONFIG = load_configuration()
611
+ build_dir = Path('build')
612
+ dist_dir = Path('dist')
613
+ base_temp_dir = Path('build/temp_deps')
614
+ if build_dir.exists():
615
+ shutil.rmtree(build_dir)
616
+ if dist_dir.exists():
617
+ shutil.rmtree(dist_dir)
618
+ build_dir.mkdir(parents=True, exist_ok=True)
619
+ dist_dir.mkdir(parents=True, exist_ok=True)
620
+ base_temp_dir.mkdir(parents=True, exist_ok=True)
621
+ print(f'\n=== xppb v1.0.0')
622
+ print(f"\n=== Bundling Sequence Initiated: {CONFIG['PROJECT_NAME']} v{CONFIG['VERSION']} ===")
623
+ with concurrent.futures.ProcessPoolExecutor() as executor:
624
+ futures = []
625
+ for platform_name, url in CONFIG['RUNTIMES'].items():
626
+ futures.append(executor.submit(bundle_platform, platform_name, url, build_dir, dist_dir, base_temp_dir, CONFIG))
627
+ for future in concurrent.futures.as_completed(futures):
628
+ try:
629
+ future.result()
630
+ except Exception as e:
631
+ print(f'\n[!] A thread encountered a fatal error during compilation: {e}')
632
+ if base_temp_dir.exists():
633
+ shutil.rmtree(base_temp_dir)
634
+ print('\n========================================================================')
635
+ print(f'Success! Standalone bundles have been compiled inside: {dist_dir.resolve()}')
636
+ print('========================================================================')
637
+ if __name__ == '__main__':
638
+ main()
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: xppb
3
+ Version: 1.0.0
4
+ Summary: A blazing-fast cross-platform Python bundling pipeline tool.
5
+ Author-email: nulsie <donotemailme@mail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: modulegraph>=0.17.4
9
+
10
+ # xppb (cross-platform python bundler)
11
+
12
+ xppb is a blazing-fast truly cross-platform binary bundler designed to compile, prune, and package Python applications into standalone, production-ready executables for Windows, macOS, and Linux from a single host machine.
13
+
14
+ Unlike traditional bundlers that are severely limited by their host operating system, xppb natively supports **true cross-compilation**. By temporarily spoofing the Python interpreter's global state and relying on standard archives, a developer on Linux can seamlessly build a signed Windows `.exe` and a fully notarized macOS `.app` bundle simultaneously in a single execution in a matter of seconds(takes a lot configuration and 20-30 min in Nuitka for reference).
15
+
16
+ ---
17
+
18
+ While tools like PyInstaller, Nuitka, and cx_Freeze are industry standards, xppb bypasses their historical limitations through a modern, aggressive architecture:
19
+
20
+ * **True Single-Host Cross-Compilation**: PyInstaller and Nuitka require you to build Windows binaries on a Windows host. xppb utilizes a `mock_target_environment` context manager to trick the host Python environment and `modulegraph` into evaluating code paths as if they were running natively on the target operating system.
21
+
22
+
23
+ * **Radical "Default-Deny" Bloat Pruning**: Traditional bundlers pull in massive directories based on exclusion lists. xppb uses a strict surgical whitelist, deleting every file in `site-packages` and the standard library that is not explicitly resolved by the dependency graph. It safely preserves only the structural `__init__.py` files, compiled extensions, and active metadata blocks (like `.dist-info`).
24
+
25
+
26
+ * **Host-Agnostic Apple Code-Signing**: You no longer need an Xcode-equipped Mac to notarize macOS software. xppb automatically fetches Indygreg’s cross-platform `rcodesign` tool to cryptographically sign bundles and submit them directly to the Apple Notary API from Linux or Windows environments.
27
+
28
+
29
+ * **Lightning Fast Dependency Resolution**: xppb searches the host for Astral's `uv` binary. If found, it routes dependency installation through `uv`, utilizing its `--platform` and `--abi` flags to download platform-specific wheels in a fraction of the time standard `pip` would take.
30
+
31
+
32
+ * **Zero-Flash Windows Launchers**: Instead of relying solely on slow script wrappers, xppb scans the host for native C compilers (`gcc`, `cl`, etc.) and dynamically templates and compiles a C-based executable (`launcher_stub.c`) to launch the application silently without console flashes.
33
+
34
+
35
+
36
+ ---
37
+
38
+ ## Core Features Under the Hood
39
+
40
+ ### 1. The Environment Spoofer (`mock_target_environment`)
41
+
42
+ To trace platform-specific dependencies (like `winreg` on Windows or `termios` on Linux) without crashing the host machine, xppb temporarily intercepts and rewrites Python's core system identifiers.
43
+
44
+ ```python
45
+ # Modifies global state thread-safely before ModuleGraph execution
46
+ if "windows" in target_os:
47
+ sys.platform = "win32"
48
+ os.name = "nt"
49
+ sys.builtin_module_names = tuple(set(orig_builtins) | {"winreg", "msvcrt", "_winapi", "nt"})
50
+
51
+ ```
52
+
53
+ ### 2. C-Extension Binary Rescuer
54
+
55
+ Compiled modules (`.so` / `.pyd`) often hide dynamic imports in their C code that standard AST tracers miss. xppb sweeps compiled extensions, reading them as binary data and matching valid ASCII strings against available system modules to rescue hidden dependencies.
56
+
57
+ ```python
58
+ with open(file_path, "rb") as f:
59
+ data = f.read()
60
+ for s in data.split(b'\0'):
61
+ # Look for strings that match the active environment file map
62
+ if 2 < len(s) < 50 and s.replace(b'.', b'').replace(b'_', b'').isalnum():
63
+ # ...rescues missing modules...
64
+
65
+ ```
66
+
67
+ ### 3. Persistent Runtime Caching
68
+
69
+ To save bandwidth across frequent builds, xppb downloads massive standard standard-library `.tar.gz` runtimes into a persistent user-level directory (`~/.core_bundler_cache/runtimes`). Subsequent cross-compilations pull locally from the cache instantly.
70
+
71
+ ---
72
+
73
+ ## On Using It
74
+
75
+ ## Through uv:
76
+ ```
77
+ uv pip install xppb
78
+ ```
79
+
80
+ ## Through pip:
81
+ just remove the uv from the above command.
82
+
83
+ ## Git Clone:
84
+ ```
85
+ git clone https://codeberg.org/nulsie/xppb.git
86
+ cd xppb
87
+ ```
88
+
89
+ ### Prerequisites
90
+
91
+ * Python 3.11+
92
+ * `pip install modulegraph`
93
+
94
+ * *(Optional but Recommended)*: Install `uv` for drastically accelerated wheel downloads.
95
+
96
+
97
+
98
+ ### Configuration (`projconf.toml`)
99
+
100
+ xppb is entirely configuration-driven. Create a file named `projconf.toml` in your project root. Here is a complete reference of the required layout:
101
+
102
+ ```toml
103
+ [project]
104
+ name = "MyAwesomeApp"
105
+ version = "1.0.0"
106
+ python_version = "3.11"
107
+ entry_point = "main.py"
108
+ source_files = ["main.py", "assets/"]
109
+ dependencies = ["requests", "rich", "numpy"]
110
+ hidden_imports = ["pkg_resources.extern"]
111
+ collect_all = []
112
+ preserve_extensions = [".png", ".ico"]
113
+ launch_command = "{ENTRY_POINT}"
114
+
115
+ [runtimes]
116
+ # URLs pointing to standalone python standard libraries (.tar.gz)
117
+ windows-x64 = "https://example.com/python-3.11-win64.tar.gz"
118
+ macos-arm64 = "https://example.com/python-3.11-macos-arm64.tar.gz"
119
+ linux-x64 = "https://example.com/python-3.11-linux64.tar.gz"
120
+
121
+ [windows]
122
+ hide_console = true
123
+ pfx_certificate = "certs/win_cert.pfx"
124
+ pfx_password = "SuperSecretPassword123"
125
+ hidden_imports = ["winreg"]
126
+
127
+ [macos]
128
+ p12_certificate = "certs/mac_cert.p12"
129
+ p12_password = "SuperSecretPassword123"
130
+ api_key_path = "certs/AuthKey_ABCD123.p8"
131
+ api_issuer_id = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
132
+ api_key_id = "ABCD123"
133
+
134
+ ```
135
+
136
+ ### Usage
137
+
138
+ Once your `projconf.toml` is configured, simply run `xppb` in your working directory and execute it:
139
+
140
+ ```bash
141
+ xppb
142
+
143
+ ```
144
+
145
+ xppb utilizes a `ProcessPoolExecutor` to spin up a concurrent build thread for every OS target defined in your `[runtimes]` configuration block.
146
+
147
+ * Build artifacts are processed temporarily in a `build/` directory.
148
+
149
+ * Final, compressed distribution assets (ready to be uploaded to GitHub Releases or S3) are deposited into `dist/`.
150
+
151
+ -----
152
+
153
+ **Author:** nulsie **License:** MIT License
@@ -0,0 +1,7 @@
1
+ xppb/__init__.py,sha256=LEcj1vU8MTsXjgJ0x2Er8l6GgRzRpXsqitZTqhkPAkI,91
2
+ xppb/launcher_stub.c,sha256=04C-3qgQekUToChHP9xNdLqdThw7puaVIkEnLX_1sVQ,946
3
+ xppb/xppb.py,sha256=8L6FoGnJzh7b2BI65AERBSr8mh2jGLS5J2sxo5nVzLk,33859
4
+ xppb-1.0.0.dist-info/entry_points.txt,sha256=b-OczJAZjjuVe7-5Vfy2vHhXRhCPicBYRblpzIN7rsE,39
5
+ xppb-1.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
6
+ xppb-1.0.0.dist-info/METADATA,sha256=MrT4RgwpaltL7HMrZ2Avzzc89Ku1ReectHHqF_dgP8A,6207
7
+ xppb-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ xppb=xppb.xppb:main
3
+