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 +3 -0
- pybend/cli.py +172 -0
- pybend/compiler.py +79 -0
- pybend/config.py +133 -0
- pybend/dependency_resolver.py +80 -0
- pybend/importer.py +128 -0
- pybend/splicer.py +20 -0
- pybend/templates/linux/x86_64/.gitkeep +0 -0
- pybend/templates/linux/x86_64/bootloader +0 -0
- pybend/vfs_builder.py +104 -0
- pybend-0.2.0.dist-info/METADATA +241 -0
- pybend-0.2.0.dist-info/RECORD +15 -0
- pybend-0.2.0.dist-info/WHEEL +4 -0
- pybend-0.2.0.dist-info/entry_points.txt +2 -0
- pybend-0.2.0.dist-info/licenses/LICENSE +21 -0
pybend/__init__.py
ADDED
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,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.
|