pybend 0.2.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.
pybend/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pybend.importer import get_asset, BendMemoryFinder, BendMemoryLoader, BendMemoryExtensionLoader
2
+
3
+ __all__ = ["get_asset", "BendMemoryFinder", "BendMemoryLoader", "BendMemoryExtensionLoader"]
pybend/cli.py ADDED
@@ -0,0 +1,172 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+ import click
5
+ from rich.console import Console
6
+ from rich.theme import Theme
7
+
8
+ from pybend.config import resolve_config
9
+ from pybend.compiler import compile_app
10
+
11
+ # Set up custom theme for Rich logging
12
+ custom_theme = Theme(
13
+ {
14
+ "info": "cyan",
15
+ "warning": "yellow",
16
+ "error": "bold red",
17
+ "success": "bold green",
18
+ }
19
+ )
20
+ console = Console(theme=custom_theme)
21
+
22
+
23
+ def find_default_template() -> Path:
24
+ """Finds the default bootloader template location in the project workspace."""
25
+ pybend_root = Path(__file__).resolve().parent.parent
26
+
27
+ # Check release target first
28
+ release_path = (
29
+ pybend_root / "bootloader" / "target" / "release" / "bootloader"
30
+ )
31
+ if release_path.exists():
32
+ return release_path
33
+
34
+ # Fall back to debug target
35
+ debug_path = pybend_root / "bootloader" / "target" / "debug" / "bootloader"
36
+ if debug_path.exists():
37
+ return debug_path
38
+
39
+ # Fall back to packaged template folder (e.g. templates/linux/x86_64/bootloader)
40
+ import platform
41
+ templates_dir = Path(__file__).resolve().parent / "templates"
42
+ system = platform.system().lower()
43
+ machine = platform.machine().lower()
44
+ # Map Python names to Rust target triples
45
+ arch_map = {"x86_64": "x86_64", "aarch64": "aarch64", "amd64": "x86_64"}
46
+ os_map = {"linux": "linux", "darwin": "macos", "windows": "windows"}
47
+ guessed_arch = arch_map.get(machine, machine)
48
+ guessed_os = os_map.get(system, system)
49
+ package_path = templates_dir / guessed_os / guessed_arch / "bootloader"
50
+ if package_path.exists():
51
+ return package_path
52
+ # Ultimate fallback: direct path
53
+ return templates_dir / "bootloader"
54
+
55
+
56
+ @click.group()
57
+ def main() -> None:
58
+ """PyBend: A zero-configuration, high-performance compiler
59
+
60
+ for Python applications.
61
+ """
62
+ pass
63
+
64
+
65
+ @main.command()
66
+ @click.option("--entry", "-e", type=str, help="Python entry script path.")
67
+ @click.option("--output", "-o", type=str, help="Output standalone executable path.")
68
+ @click.option(
69
+ "--optimize",
70
+ "-O",
71
+ type=click.Choice(["0", "1", "2"]),
72
+ help="Bytecode optimization level.",
73
+ )
74
+ @click.option(
75
+ "--include", "-i", multiple=True, help="Force include module(s) in the VFS."
76
+ )
77
+ @click.option(
78
+ "--exclude",
79
+ "-x",
80
+ multiple=True,
81
+ help="Explicitly exclude module(s) from the VFS.",
82
+ )
83
+ @click.option(
84
+ "--template",
85
+ "-t",
86
+ type=str,
87
+ help="Custom native runner bootloader template path.",
88
+ )
89
+ @click.option("--config", "-c", type=str, help="Custom pybend.toml config path.")
90
+ def build(
91
+ entry: Optional[str],
92
+ output: Optional[str],
93
+ optimize: Optional[str],
94
+ include: List[str],
95
+ exclude: List[str],
96
+ template: Optional[str],
97
+ config: Optional[str],
98
+ ) -> None:
99
+ """Compile a Python application into a standalone native binary."""
100
+ console.print("[info]Starting PyBend compilation toolchain...[/info]")
101
+
102
+ # 1. Resolve configuration options
103
+ cli_args = {}
104
+ if entry is not None:
105
+ cli_args["entry_point"] = entry
106
+ if output is not None:
107
+ cli_args["output_exe"] = output
108
+ if optimize is not None:
109
+ cli_args["optimization_level"] = int(optimize)
110
+ if include:
111
+ cli_args["include_modules"] = list(include)
112
+ if exclude:
113
+ cli_args["exclude_modules"] = list(exclude)
114
+
115
+ config_path = Path(config) if config else None
116
+
117
+ try:
118
+ resolved_config = resolve_config(cli_args, config_path=config_path)
119
+ except ValueError as e:
120
+ console.print(f"[error]Configuration Error:[/error] {e}")
121
+ sys.exit(1)
122
+ except Exception as e:
123
+ console.print(f"[error]Unexpected Error:[/error] {e}")
124
+ sys.exit(1)
125
+
126
+ # 2. Resolve template path
127
+ if template:
128
+ runner_template = Path(template).resolve()
129
+ else:
130
+ runner_template = find_default_template()
131
+
132
+ if not runner_template.exists():
133
+ console.print(
134
+ "[error]Template Error:[/error] Bootloader template not found "
135
+ f"at '{runner_template}'.\n"
136
+ "Please build the bootloader or specify it via --template."
137
+ )
138
+ sys.exit(3)
139
+
140
+ entry_point = Path(resolved_config["entry_point"])
141
+ output_exe = Path(resolved_config["output_exe"])
142
+
143
+ if not entry_point.exists():
144
+ console.print(
145
+ f"[error]Resolution Error:[/error] Entry point script '{entry_point}' does not exist."
146
+ )
147
+ sys.exit(2)
148
+
149
+ # Make parent output directory if missing
150
+ output_exe.parent.mkdir(parents=True, exist_ok=True)
151
+
152
+ console.print(f"[info]Entry Point: [/info] {entry_point}")
153
+ console.print(f"[info]Output Target: [/info] {output_exe}")
154
+ console.print(
155
+ f"[info]Optimization: [/info] Level {resolved_config['optimization_level']}"
156
+ )
157
+
158
+ try:
159
+ compile_app(resolved_config, runner_template)
160
+ except ImportError as e:
161
+ console.print(f"[error]Dependency Error:[/error] {e}")
162
+ sys.exit(2)
163
+ except FileNotFoundError as e:
164
+ console.print(f"[error]Compilation Error:[/error] {e}")
165
+ sys.exit(3)
166
+ except Exception as e:
167
+ console.print(f"[error]Build Failed:[/error] {e}")
168
+ sys.exit(3)
169
+
170
+ console.print(
171
+ f"[success]Successfully compiled '{entry_point.name}' to '{output_exe}'[/success]"
172
+ )
pybend/compiler.py ADDED
@@ -0,0 +1,79 @@
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+ from pybend.dependency_resolver import DependencyResolver
6
+ from pybend.vfs_builder import VfsBuilder
7
+ from pybend.splicer import splice
8
+
9
+
10
+ def compile_app(config: Dict[str, Any], runner_template: Path) -> None:
11
+ """Compiles a Python application into a standalone binary.
12
+
13
+ Accepts the fully resolved configuration dictionary and the bootloader template path.
14
+ """
15
+ entry_point = Path(config["entry_point"]).resolve()
16
+ output_exe = Path(config["output_exe"]).resolve()
17
+ runner_template = Path(runner_template).resolve()
18
+
19
+ optimization_level = config.get("optimization_level", 2)
20
+ include_modules = config.get("include_modules", [])
21
+ exclude_modules = config.get("exclude_modules", [])
22
+ include_data = [Path(p) for p in config.get("include_data", [])]
23
+
24
+ # 1. Resolve static dependencies
25
+ resolver = DependencyResolver(entry_point, include_modules, exclude_modules)
26
+ modules_map = resolver.resolve_static_imports()
27
+
28
+ # 2. Add the user's entry point under 'pybend_app_entry' name in VFS
29
+ modules_map["pybend_app_entry"] = str(entry_point)
30
+
31
+ # 3. Create the dynamic bend_app_main module
32
+ app_main_code = """import sys
33
+ import types
34
+
35
+ def exec_bootstrap():
36
+ finder = sys.meta_path[0]
37
+ code_obj = finder.get_code_object("pybend_app_entry")
38
+
39
+ # Configure __main__ module environment
40
+ main_mod = sys.modules.setdefault("__main__", types.ModuleType("__main__"))
41
+ main_mod.__file__ = "pybend_app_entry.py"
42
+
43
+ # Execute the entry bytecode in __main__ context
44
+ try:
45
+ exec(code_obj, main_mod.__dict__)
46
+ finally:
47
+ try:
48
+ sys.stdout.flush()
49
+ except Exception:
50
+ pass
51
+ try:
52
+ sys.stderr.flush()
53
+ except Exception:
54
+ pass
55
+ """
56
+
57
+ with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as tmp:
58
+ tmp.write(app_main_code)
59
+ tmp_path = Path(tmp.name)
60
+
61
+ try:
62
+ modules_map["bend_app_main"] = str(tmp_path)
63
+
64
+ # 4. Build VFS payload
65
+ builder = VfsBuilder(
66
+ modules_map,
67
+ optimization_level=optimization_level,
68
+ include_data=include_data,
69
+ )
70
+ vfs_blob = builder.build_vfs_blob()
71
+ finally:
72
+ if tmp_path.exists():
73
+ tmp_path.unlink()
74
+
75
+ # 5. Append VFS payload onto target native runner template
76
+ splice(runner_template, vfs_blob, output_exe)
77
+
78
+ # 6. Mark output binary as executable
79
+ os.chmod(output_exe, 0o755)
pybend/config.py ADDED
@@ -0,0 +1,133 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Optional
4
+
5
+ DEFAULT_CONFIG: Dict[str, Any] = {
6
+ "entry_point": None,
7
+ "output_exe": None,
8
+ "optimization_level": 2,
9
+ "include_modules": [],
10
+ "exclude_modules": [],
11
+ "include_data": [],
12
+ }
13
+
14
+
15
+ def load_config(config_path: Path) -> Dict[str, Any]:
16
+ """Loads configuration from a TOML file and validates it.
17
+
18
+ If the file doesn't exist, returns the default configuration.
19
+ """
20
+ config_path = Path(config_path).resolve()
21
+ if not config_path.exists():
22
+ return DEFAULT_CONFIG.copy()
23
+
24
+ try:
25
+ with open(config_path, "rb") as f:
26
+ data = tomllib.load(f)
27
+ except Exception as e:
28
+ raise ValueError(f"Failed to parse TOML config file: {e}")
29
+
30
+ build_data = data.get("build", {})
31
+ config = DEFAULT_CONFIG.copy()
32
+
33
+ # Merge values from [build] section
34
+ for key in config.keys():
35
+ if key in build_data:
36
+ config[key] = build_data[key]
37
+
38
+ # Validate type and values
39
+ if config["entry_point"] is not None and not isinstance(config["entry_point"], str):
40
+ raise ValueError("entry_point must be a string")
41
+ if config["output_exe"] is not None and not isinstance(config["output_exe"], str):
42
+ raise ValueError("output_exe must be a string")
43
+ if not isinstance(config["optimization_level"], int):
44
+ raise ValueError("optimization_level must be an integer")
45
+ if not isinstance(config["include_modules"], list) or not all(
46
+ isinstance(x, str) for x in config["include_modules"]
47
+ ):
48
+ raise ValueError("include_modules must be a list of strings")
49
+ if not isinstance(config["exclude_modules"], list) or not all(
50
+ isinstance(x, str) for x in config["exclude_modules"]
51
+ ):
52
+ raise ValueError("exclude_modules must be a list of strings")
53
+ if not isinstance(config["include_data"], list) or not all(
54
+ isinstance(x, str) for x in config["include_data"]
55
+ ):
56
+ raise ValueError("include_data must be a list of strings")
57
+
58
+ opt_level = config["optimization_level"]
59
+ if opt_level not in (0, 1, 2):
60
+ raise ValueError(
61
+ f"Invalid optimization_level: {opt_level}. Must be 0, 1, or 2."
62
+ )
63
+
64
+ # Resolve paths relative to pybend.toml location
65
+ config_dir = config_path.parent
66
+ if config["entry_point"] is not None:
67
+ config["entry_point"] = str((config_dir / config["entry_point"]).resolve())
68
+ if config["output_exe"] is not None:
69
+ config["output_exe"] = str((config_dir / config["output_exe"]).resolve())
70
+
71
+ resolved_data = []
72
+ for item in config["include_data"]:
73
+ resolved_data.append(str((config_dir / item).resolve()))
74
+ config["include_data"] = resolved_data
75
+
76
+ return config
77
+
78
+
79
+ def resolve_config(
80
+ cli_args: Dict[str, Any],
81
+ config_path: Optional[Path] = None,
82
+ project_dir: Optional[Path] = None,
83
+ ) -> Dict[str, Any]:
84
+ """Resolves configuration options by merging command-line overrides,
85
+
86
+ TOML configuration values, and defaults. Performs auto-discovery
87
+ for missing entry points.
88
+ """
89
+ if project_dir is None:
90
+ project_dir = Path.cwd()
91
+ project_dir = Path(project_dir).resolve()
92
+
93
+ # Determine TOML configuration file path
94
+ if config_path is None:
95
+ config_path = project_dir / "pybend.toml"
96
+ else:
97
+ config_path = Path(config_path).resolve()
98
+
99
+ # 1. Load TOML configuration
100
+ config = load_config(config_path)
101
+
102
+ # 2. Merge CLI overrides
103
+ for key, val in cli_args.items():
104
+ if val is not None:
105
+ # If the value is a file path parameter, resolve it relative to CWD
106
+ if key in ("entry_point", "output_exe"):
107
+ config[key] = str(Path(val).resolve())
108
+ elif key == "include_data":
109
+ config[key] = [str(Path(x).resolve()) for x in val]
110
+ else:
111
+ config[key] = val
112
+
113
+ # 3. Auto-Discovery for entry_point
114
+ if config["entry_point"] is None:
115
+ main_py = project_dir / "main.py"
116
+ app_py = project_dir / "app.py"
117
+ if main_py.exists():
118
+ config["entry_point"] = str(main_py.resolve())
119
+ elif app_py.exists():
120
+ config["entry_point"] = str(app_py.resolve())
121
+ else:
122
+ raise ValueError(
123
+ "Could not auto-discover entry point. "
124
+ "Please specify entry_point in pybend.toml or via --entry flag."
125
+ )
126
+
127
+ # 4. Generate output_exe default if missing
128
+ if config["output_exe"] is None:
129
+ entry_path = Path(config["entry_point"])
130
+ dist_dir = entry_path.parent / "dist"
131
+ config["output_exe"] = str(dist_dir / f"{entry_path.stem}.bin")
132
+
133
+ return config
@@ -0,0 +1,80 @@
1
+ import sys
2
+ import modulefinder
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+
7
+ class DependencyResolver:
8
+ """traces static dependencies of a Python entry script using standard ModuleFinder."""
9
+
10
+ def __init__(
11
+ self,
12
+ entry_point: Path,
13
+ include_modules: Optional[List[str]] = None,
14
+ exclude_modules: Optional[List[str]] = None,
15
+ ) -> None:
16
+ self.entry_point = Path(entry_point).resolve()
17
+ self.include_modules = include_modules or []
18
+ self.exclude_modules = exclude_modules or []
19
+ # Add entry point's parent directory to search path for local imports
20
+ search_path = [str(self.entry_point.parent)] + sys.path
21
+ self.finder = modulefinder.ModuleFinder(path=search_path)
22
+
23
+ @staticmethod
24
+ def _get_site_packages_dirs() -> List[Path]:
25
+ """Return all site-packages directories from sys.path."""
26
+ return [
27
+ Path(p).resolve()
28
+ for p in sys.path
29
+ if "site-packages" in p or "dist-packages" in p
30
+ ]
31
+
32
+ def _should_include(self, path: Path) -> bool:
33
+ """Check if a module path should be included in the VFS."""
34
+ path = path.resolve()
35
+ if path.suffix not in (".py", ".so"):
36
+ return False
37
+
38
+ entry_parent = self.entry_point.parent.resolve()
39
+ # Always include modules relative to the project directory
40
+ if path.is_relative_to(entry_parent):
41
+ return True
42
+
43
+ # Include modules from site-packages (third-party packages)
44
+ for site_dir in self._get_site_packages_dirs():
45
+ if path.is_relative_to(site_dir):
46
+ return True
47
+
48
+ return False
49
+
50
+ def resolve_static_imports(self) -> Dict[str, str]:
51
+ """Runs the module finder script tracing and returns module name to absolute path map."""
52
+ self.finder.run_script(str(self.entry_point))
53
+ resolved: Dict[str, str] = {}
54
+ entry_parent = self.entry_point.parent.resolve()
55
+
56
+ for name, mod in self.finder.modules.items():
57
+ if mod.__file__ is not None:
58
+ path = Path(mod.__file__).resolve()
59
+ if self._should_include(path):
60
+ resolved[name] = str(path)
61
+
62
+ # Force-include whitelisted modules
63
+ for mod_name in self.include_modules:
64
+ if mod_name not in resolved:
65
+ # Try to find the module on sys.path
66
+ try:
67
+ import importlib
68
+ spec = importlib.util.find_spec(mod_name)
69
+ if spec is not None and spec.origin is not None:
70
+ origin = Path(spec.origin).resolve()
71
+ if origin.suffix in (".py", ".so"):
72
+ resolved[mod_name] = str(origin)
73
+ except (ImportError, ModuleNotFoundError, AttributeError):
74
+ pass
75
+
76
+ # Remove blacklisted modules
77
+ for mod_name in self.exclude_modules:
78
+ resolved.pop(mod_name, None)
79
+
80
+ return resolved
pybend/importer.py ADDED
@@ -0,0 +1,128 @@
1
+ from types import ModuleType, CodeType
2
+ import importlib.machinery
3
+ from importlib.machinery import ModuleSpec
4
+ import marshal
5
+ import os
6
+ from typing import Dict, Tuple, Optional, List
7
+ import zlib
8
+
9
+
10
+ VfsIndexValue = Tuple[int, int, int, int] # (offset, compressed_size, uncompressed_size, is_c_ext)
11
+
12
+
13
+ class BendMemoryLoader:
14
+ """PEP 451 module loader that executes compiled bytecodes from memory."""
15
+
16
+ def __init__(self, code_obj: CodeType) -> None:
17
+ self.code_obj = code_obj
18
+
19
+ def create_module(self, spec: ModuleSpec) -> Optional[ModuleType]:
20
+ return None
21
+
22
+ def exec_module(self, module: ModuleType) -> None:
23
+ exec(self.code_obj, module.__dict__)
24
+
25
+
26
+ class BendMemoryExtensionLoader:
27
+ """PEP 451 module loader that loads compiled C-extensions
28
+ from memory using os.memfd_create.
29
+ """
30
+
31
+ def __init__(self, fullname: str, so_bytes: bytes) -> None:
32
+ self.fullname = fullname
33
+ self.so_bytes = so_bytes
34
+ self.fd: Optional[int] = None
35
+ self.delegate: Optional[importlib.machinery.ExtensionFileLoader] = None
36
+
37
+ def create_module(self, spec: ModuleSpec) -> Optional[ModuleType]:
38
+ self.fd = os.memfd_create(self.fullname, flags=os.MFD_CLOEXEC)
39
+ os.write(self.fd, self.so_bytes)
40
+ path = f"/proc/self/fd/{self.fd}"
41
+ spec.origin = path
42
+ self.delegate = importlib.machinery.ExtensionFileLoader(self.fullname, path)
43
+ return self.delegate.create_module(spec)
44
+
45
+ def exec_module(self, module: ModuleType) -> None:
46
+ if self.delegate is None:
47
+ raise ImportError(
48
+ f"Loader delegate for {self.fullname} was not initialized."
49
+ )
50
+ try:
51
+ self.delegate.exec_module(module)
52
+ finally:
53
+ if self.fd is not None:
54
+ try:
55
+ os.close(self.fd)
56
+ except OSError:
57
+ pass
58
+ self.fd = None
59
+
60
+
61
+ class BendMemoryFinder:
62
+ """PEP 451 MetaPathFinder that resolves modules from the memory-mapped BendVFS index."""
63
+
64
+ def __init__(
65
+ self,
66
+ vfs_index: Dict[str, VfsIndexValue],
67
+ vfs_data: bytes,
68
+ asset_index: Optional[Dict[str, Tuple[int, int, int]]] = None,
69
+ ) -> None:
70
+ self.index = vfs_index
71
+ self.data = vfs_data
72
+ self.asset_index = asset_index or {}
73
+
74
+ def find_spec(
75
+ self,
76
+ fullname: str,
77
+ path: Optional[List[str]],
78
+ target: Optional[ModuleType] = None,
79
+ ) -> Optional[ModuleSpec]:
80
+ if fullname in self.index:
81
+ is_package = any(k.startswith(fullname + ".") for k in self.index.keys())
82
+ offset, comp_size, uncomp_size, is_c_ext = self.index[fullname]
83
+
84
+ if is_c_ext == 1:
85
+ compressed_bytes = self.data[offset : offset + comp_size]
86
+ so_bytes = zlib.decompress(compressed_bytes)
87
+ loader = BendMemoryExtensionLoader(fullname, so_bytes)
88
+ else:
89
+ code_obj = self.get_code_object(fullname)
90
+ loader = BendMemoryLoader(code_obj)
91
+
92
+ spec = importlib.machinery.ModuleSpec(
93
+ fullname, loader, is_package=is_package
94
+ )
95
+ if is_package:
96
+ spec.submodule_search_locations = []
97
+ return spec
98
+ return None
99
+
100
+ def get_code_object(self, fullname: str) -> CodeType:
101
+ offset, comp_size, uncomp_size, is_c_ext = self.index[fullname]
102
+ compressed_bytes = self.data[offset : offset + comp_size]
103
+ bytecode_bytes = zlib.decompress(compressed_bytes)
104
+ return marshal.loads(bytecode_bytes)
105
+
106
+
107
+ def get_asset(relative_path: str) -> bytes:
108
+ """Retrieve a static asset from the VFS by its relative project path."""
109
+ finder = _get_bend_finder()
110
+ if finder is None:
111
+ raise FileNotFoundError(
112
+ f"PyBend VFS not initialized; cannot load asset '{relative_path}'"
113
+ )
114
+ if relative_path not in finder.asset_index:
115
+ raise FileNotFoundError(
116
+ f"Asset '{relative_path}' not found in PyBend VFS"
117
+ )
118
+ offset, comp_size, uncomp_size = finder.asset_index[relative_path]
119
+ compressed_bytes = finder.data[offset : offset + comp_size]
120
+ return zlib.decompress(compressed_bytes)
121
+
122
+
123
+ def _get_bend_finder() -> Optional["BendMemoryFinder"]:
124
+ import sys
125
+ for finder in sys.meta_path:
126
+ if isinstance(finder, BendMemoryFinder):
127
+ return finder
128
+ return None
pybend/splicer.py ADDED
@@ -0,0 +1,20 @@
1
+ import struct
2
+ from pathlib import Path
3
+
4
+
5
+ def splice(runner_path: Path, vfs_blob: bytes, output_path: Path) -> None:
6
+ """Appends the VFS data array directly to the tail of the target native runner template."""
7
+ runner_data = Path(runner_path).read_bytes()
8
+
9
+ # 8-byte big-endian representation of the VFS payload length
10
+ vfs_length = struct.pack(">Q", len(vfs_blob))
11
+
12
+ # 4-byte magic signature footer
13
+ magic_footer = b"BEND"
14
+
15
+ # Write final output executable
16
+ with open(output_path, "wb") as f:
17
+ f.write(runner_data)
18
+ f.write(vfs_blob)
19
+ f.write(vfs_length)
20
+ f.write(magic_footer)
File without changes
Binary file
pybend/vfs_builder.py ADDED
@@ -0,0 +1,104 @@
1
+ import py_compile
2
+ import struct
3
+ import tempfile
4
+ import zlib
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+
9
+ class VfsBuilder:
10
+ """compiles Python source modules to optimized bytecode and serializes them into a single BendVFS blob."""
11
+
12
+ def __init__(
13
+ self,
14
+ modules_map: Dict[str, str],
15
+ optimization_level: int = 2,
16
+ include_data: Optional[List[Path]] = None,
17
+ ):
18
+ self.modules_map = modules_map
19
+ self.optimization_level = optimization_level
20
+ self.include_data = include_data or []
21
+
22
+ def build_vfs_blob(self) -> bytes:
23
+ """Compiles modules, compresses them with zlib, and constructs the VFS payload."""
24
+ entries = []
25
+ asset_entries = []
26
+ payload_data = b""
27
+
28
+ # Process modules in sorted order for determinism
29
+ for name in sorted(self.modules_map.keys()):
30
+ py_file = Path(self.modules_map[name]).resolve()
31
+
32
+ if py_file.suffix == ".so":
33
+ bytecode_bytes = py_file.read_bytes()
34
+ is_c_ext = 0x01
35
+ else:
36
+ # Create a temporary file for .pyc compilation
37
+ with tempfile.NamedTemporaryFile(suffix=".pyc", delete=False) as tmp:
38
+ tmp_path = Path(tmp.name)
39
+
40
+ try:
41
+ py_compile.compile(
42
+ str(py_file),
43
+ cfile=str(tmp_path),
44
+ optimize=self.optimization_level,
45
+ doraise=True,
46
+ )
47
+ pyc_bytes = tmp_path.read_bytes()
48
+
49
+ # Strip the 16-byte .pyc header to get raw marshalled code object
50
+ bytecode_bytes = pyc_bytes[16:]
51
+ finally:
52
+ if tmp_path.exists():
53
+ tmp_path.unlink()
54
+ is_c_ext = 0x00
55
+
56
+ compressed_bytes = zlib.compress(bytecode_bytes)
57
+
58
+ compressed_size = len(compressed_bytes)
59
+ uncompressed_size = len(bytecode_bytes)
60
+ payload_offset = len(payload_data)
61
+
62
+ payload_data += compressed_bytes
63
+
64
+ # Serialize entry metadata: (payload_offset, compressed_size, uncompressed_size, is_c_ext)
65
+ name_bytes = name.encode("utf-8")
66
+ entry_header = struct.pack(">H", len(name_bytes)) + name_bytes
67
+ entry_meta = (
68
+ struct.pack(">QQQ", payload_offset, compressed_size, uncompressed_size)
69
+ + struct.pack("B", is_c_ext)
70
+ )
71
+
72
+ entries.append(entry_header + entry_meta)
73
+
74
+ # Process static asset files
75
+ for asset_path in self.include_data:
76
+ asset_path = Path(asset_path).resolve()
77
+ if not asset_path.exists():
78
+ continue
79
+ raw_bytes = asset_path.read_bytes()
80
+ compressed_bytes = zlib.compress(raw_bytes)
81
+
82
+ compressed_size = len(compressed_bytes)
83
+ uncompressed_size = len(raw_bytes)
84
+ payload_offset = len(payload_data)
85
+
86
+ payload_data += compressed_bytes
87
+
88
+ # Use relative path as the asset key
89
+ rel_path = str(asset_path.relative_to(Path.cwd()))
90
+ name_bytes = rel_path.encode("utf-8")
91
+ entry_header = struct.pack(">H", len(name_bytes)) + name_bytes
92
+ entry_meta = struct.pack(">QQQ", payload_offset, compressed_size, uncompressed_size)
93
+
94
+ asset_entries.append(entry_header + entry_meta)
95
+
96
+ # Assemble the final blob
97
+ magic = b"BEND"
98
+ entry_count = struct.pack(">I", len(entries))
99
+ metadata_table = b"".join(entries)
100
+
101
+ asset_count = struct.pack(">I", len(asset_entries))
102
+ asset_table = b"".join(asset_entries)
103
+
104
+ return magic + entry_count + metadata_table + asset_count + asset_table + payload_data
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybend
3
+ Version: 0.2.0
4
+ Summary: Compile Python applications into standalone native binaries
5
+ Author: Jude Nii Klemesu Commey
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Jude Nii Klemesu Commey
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: binary,compiler,deployment,freezer,standalone
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Programming Language :: Rust
37
+ Classifier: Topic :: Software Development :: Build Tools
38
+ Classifier: Topic :: Software Development :: Compilers
39
+ Requires-Python: >=3.11
40
+ Requires-Dist: click>=8.0
41
+ Requires-Dist: rich>=13.0
42
+ Description-Content-Type: text/markdown
43
+
44
+ # PyBend ๐Ÿชถ
45
+
46
+ > Compile Python applications into standalone native binaries that run entirely in volatile memory.
47
+
48
+ PyBend is a zero-configuration, high-performance Python-to-Native-Binary compiler toolchain. It compiles your Python applications, third-party packages, and native C-extensions into a single, standalone executable โ€” with **zero physical disk I/O at runtime**.
49
+
50
+ Unlike traditional freezers (PyInstaller, cx_Freeze, Nuitka) that extract dependencies to `/tmp` or `%TEMP%`, PyBend keeps everything in RAM via a custom PEP 451 meta-path importer backed by an embedded Virtual File System.
51
+
52
+ ---
53
+
54
+ ## ๐Ÿš€ Performance Snapshot
55
+
56
+ | Metric | PyBend | PyInstaller |
57
+ |--------|--------|-------------|
58
+ | **Cold-Start Latency** | ~23 ms | ~300โ€“500 ms |
59
+ | **Disk I/O at Runtime** | 0 physical writes | Writes to `/tmp` |
60
+ | **Binary Size (base)** | ~340 KB (engine) + VFS | ~30 MB+ |
61
+ | **Code on Disk** | Never | Extracted to temp |
62
+
63
+ ## ๐Ÿ› ๏ธ How It Works
64
+
65
+ ### 1. AST Tracing & Tree-Shaking
66
+ Scans your import graph using Python's `modulefinder`. Only the modules your app actually imports are included โ€” no bloat.
67
+
68
+ ### 2. Optimization Pass (-OO)
69
+ All source is compiled to bytecode with assertions and docstrings stripped, yielding 8โ€“15% smaller payloads.
70
+
71
+ ### 3. Dual-Table VFS v2 Layout
72
+ Bytecode, native `.so` extensions, and static assets are packed into a structured binary blob:
73
+
74
+ ```
75
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
76
+ โ”‚ BENDVFS BLOB โ”‚
77
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
78
+ โ”‚ [4B: "BEND" Magic] [4B: Code Entry Count] โ”‚
79
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
80
+ โ”‚ โ”‚ Code Module Table โ”‚ โ”‚
81
+ โ”‚ โ”‚ name โ†’ (offset, compressed_size, โ”‚ โ”‚
82
+ โ”‚ โ”‚ uncompressed_size, is_c_ext) โ”‚ โ”‚
83
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
84
+ โ”‚ [4B: Asset Entry Count] โ”‚
85
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
86
+ โ”‚ โ”‚ Asset Table โ”‚ โ”‚
87
+ โ”‚ โ”‚ path โ†’ (offset, compressed_size, โ”‚ โ”‚
88
+ โ”‚ โ”‚ uncompressed_size) โ”‚ โ”‚
89
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
90
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
91
+ โ”‚ โ”‚ Compressed Payload Pool (zlib DEFLATE) โ”‚ โ”‚
92
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
93
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
94
+ ```
95
+
96
+ ### 4. RAM-Mapped Execution
97
+ - The Rust bootloader memory-maps itself via `memmap2`, locates the VFS by its `BEND` footer signature, and passes it to the embedded Python runtime.
98
+ - A `BendMemoryFinder` is injected into `sys.meta_path[0]` per PEP 451 โ€” all imports resolve from RAM.
99
+ - C-extensions are written to anonymous `memfd_create` file descriptors and loaded with `dlopen` โ€” the kernel pages them directly from RAM.
100
+
101
+ ---
102
+
103
+ ## ๐Ÿ“ฆ Quick Start
104
+
105
+ ### 1. Install
106
+
107
+ ```bash
108
+ pip install pybend
109
+ ```
110
+
111
+ ### 2. Zero-Config Build
112
+
113
+ If your project directory contains `main.py` or `app.py`:
114
+
115
+ ```bash
116
+ pybend build
117
+ ```
118
+
119
+ That's it. The output lands at `./dist/<name>.bin`.
120
+
121
+ ### 3. Explicit Build
122
+
123
+ ```bash
124
+ pybend build --entry src/server.py --output deploy/server.bin -O 2
125
+ ```
126
+
127
+ ### 4. Config-Driven Build (`pybend.toml`)
128
+
129
+ ```toml
130
+ [build]
131
+ entry_point = "src/main.py"
132
+ output_exe = "dist/app.bin"
133
+ optimization_level = 2
134
+ include_modules = ["hidden_dep"]
135
+ exclude_modules = ["test", "unittest"]
136
+ include_data = ["config.json", "assets/"]
137
+ ```
138
+
139
+ ```bash
140
+ pybend build
141
+ ```
142
+
143
+ ### 5. Fetching Embedded Assets at Runtime
144
+
145
+ Any file listed in `include_data` can be streamed from memory:
146
+
147
+ ```python
148
+ import pybend
149
+
150
+ config = pybend.get_asset("config.json")
151
+ template = pybend.get_asset("templates/email.html")
152
+ ```
153
+
154
+ No disk access. No extraction. Directly from the VFS.
155
+
156
+ ---
157
+
158
+ ## ๐Ÿ—๏ธ Project Structure
159
+
160
+ ```
161
+ pybend/
162
+ โ”œโ”€โ”€ pyproject.toml # Hatchling build config
163
+ โ”œโ”€โ”€ bootloader/ # Rust runtime engine
164
+ โ”‚ โ”œโ”€โ”€ Cargo.toml
165
+ โ”‚ โ””โ”€โ”€ src/
166
+ โ”‚ โ”œโ”€โ”€ main.rs
167
+ โ”‚ โ”œโ”€โ”€ extractor.rs
168
+ โ”‚ โ””โ”€โ”€ bootstrapper.rs
169
+ โ”œโ”€โ”€ pybend/ # Python build orchestration
170
+ โ”‚ โ”œโ”€โ”€ __init__.py
171
+ โ”‚ โ”œโ”€โ”€ cli.py # Click CLI
172
+ โ”‚ โ”œโ”€โ”€ compiler.py # Pipeline orchestrator
173
+ โ”‚ โ”œโ”€โ”€ config.py # TOML + CLI config resolution
174
+ โ”‚ โ”œโ”€โ”€ dependency_resolver.py # Import tracing + filtering
175
+ โ”‚ โ”œโ”€โ”€ importer.py # Runtime VFS finder & loaders
176
+ โ”‚ โ”œโ”€โ”€ splicer.py # Binary tail-splicing
177
+ โ”‚ โ”œโ”€โ”€ vfs_builder.py # VFS blob construction
178
+ โ”‚ โ””โ”€โ”€ templates/ # Pre-built bootloader binaries
179
+ โ”œโ”€โ”€ docs/ # MkDocs documentation
180
+ โ”œโ”€โ”€ tests/ # Test suite
181
+ โ””โ”€โ”€ .github/workflows/ # CI/CD pipelines
182
+ ```
183
+
184
+ ---
185
+
186
+ ## โš™๏ธ Configuration Reference
187
+
188
+ ### CLI Flags
189
+
190
+ | Flag | Shorthand | Description |
191
+ |------|-----------|-------------|
192
+ | `--entry` | `-e` | Entry point script path |
193
+ | `--output` | `-o` | Output executable path |
194
+ | `--optimize` | `-O` | Optimization level (0, 1, 2) |
195
+ | `--include` | `-i` | Force-include a module |
196
+ | `--exclude` | `-x` | Exclude a module |
197
+ | `--template` | `-t` | Custom bootloader binary |
198
+ | `--config` | `-c` | Custom pybend.toml path |
199
+
200
+ ### pybend.toml Fields
201
+
202
+ | Field | Type | Default | Description |
203
+ |-------|------|---------|-------------|
204
+ | `entry_point` | string | auto-discover | Entry script path |
205
+ | `output_exe` | string | `dist/<name>.bin` | Output binary path |
206
+ | `optimization_level` | int | `2` | Bytecode optimization (0/1/2) |
207
+ | `include_modules` | string[] | `[]` | Force-include modules |
208
+ | `exclude_modules` | string[] | `[]` | Exclude modules |
209
+ | `include_data` | string[] | `[]` | Static file paths to embed |
210
+
211
+ ---
212
+
213
+ ## ๐Ÿงช Testing
214
+
215
+ ```bash
216
+ # Python test suite (22 tests)
217
+ python -m unittest discover -s tests -v
218
+
219
+ # Rust tests
220
+ cd bootloader && RUSTFLAGS="-L lib" cargo test
221
+ ```
222
+
223
+ Current test coverage: **22 Python tests + 2 Rust tests โ€” all passing**.
224
+
225
+ Includes end-to-end compilation + execution tests, C-extension loading via `memfd_create`, VFS structural validation, latency benchmarks, and disk-isolation verification.
226
+
227
+ ---
228
+
229
+ ## ๐ŸŽฏ Target Platform
230
+
231
+ - **Linux x86_64** (primary, production-tested)
232
+
233
+ Future targets: `aarch64-unknown-linux-musl`, `x86_64-pc-windows-gnu`, `aarch64-apple-darwin`.
234
+
235
+ ---
236
+
237
+ ## ๐Ÿ“œ License
238
+
239
+ MIT โ€” see [LICENSE](LICENSE).
240
+
241
+ Copyright (c) 2026 Jude Nii Klemesu Commey
@@ -0,0 +1,15 @@
1
+ pybend/__init__.py,sha256=7lFhG2AL6EU35XFXp5DF7SbE_JcjCIZXUO7xZWY3JTM,195
2
+ pybend/cli.py,sha256=tEJd46PQg8Sxx7b1CjcL00OcHTPyqLMlPSbEnhnR-Eg,5340
3
+ pybend/compiler.py,sha256=O37w_6Y19AAvcvGVNl9xN0Y65CCgJqlo8Dz5qmv2y-E,2539
4
+ pybend/config.py,sha256=qnrjn6ACW1T4Guhhex2JS9PTnTIE2IujWql-yMjlj0Q,4704
5
+ pybend/dependency_resolver.py,sha256=AqFNcMZ-Loy_IkLxurZ-_Dp_AwhmHASebHbkuqNMwM8,3035
6
+ pybend/importer.py,sha256=_uhc_ggr-eHMqaefyBbuEdQ7t3jVQ0nYQd0zqVWS27U,4449
7
+ pybend/splicer.py,sha256=vPhCexiIrl12DxZb0U4yMU5rDQ-6y2d85-frdkgzzyo,630
8
+ pybend/vfs_builder.py,sha256=vNmno3j96MzajCN5Ly4Cm7Z-akjc_kGBENjAPppl11M,3842
9
+ pybend/templates/linux/x86_64/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ pybend/templates/linux/x86_64/bootloader,sha256=j1RKz2XyPC0hHb9B9oJFk2XPK3hT4wMNW1yO-8jv3q4,336856
11
+ pybend-0.2.0.dist-info/METADATA,sha256=yhp1eB1EDcxK6RvuqDeIQvwOM6HN-e7u3c5kbCa49_w,9551
12
+ pybend-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
13
+ pybend-0.2.0.dist-info/entry_points.txt,sha256=5MjBC2Sy6cuViomgHxZP6AL8NR5YRagE4PPWtZuw1RE,43
14
+ pybend-0.2.0.dist-info/licenses/LICENSE,sha256=ytu4Q06aRxed_gQxDf4BOQCkZ_IWNSTNr04qkHLjTkc,1080
15
+ pybend-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pybend = pybend.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jude Nii Klemesu Commey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.