gelang 0.1.0
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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- package/scripts/check-toolchains.py +85 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Module/import system for GE.
|
|
2
|
+
|
|
3
|
+
Resolves `from module import function` statements by finding the
|
|
4
|
+
corresponding `.ge.py` file and merging its functions into the
|
|
5
|
+
current compilation unit.
|
|
6
|
+
|
|
7
|
+
Supported import syntax:
|
|
8
|
+
from mymodule import my_function
|
|
9
|
+
from mymodule import func1, func2
|
|
10
|
+
from .mymodule import func (relative import)
|
|
11
|
+
|
|
12
|
+
The module file is searched in:
|
|
13
|
+
1. The same directory as the source file
|
|
14
|
+
2. The GE standard library directory (future)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from .analyzer import FuncUnit, parse_source_full, ClassUnit, collect_constants, collect_preamble
|
|
21
|
+
from .frontends import frontend_for, python_flavour_of
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
#: Modules provided by the compiler itself. Importing them is how a source
|
|
25
|
+
#: file selects a backend decorator; they must never be resolved as user code.
|
|
26
|
+
_COMPILER_MODULES = ("pyeffic", "__future__")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_compiler_module(module_name: str) -> bool:
|
|
30
|
+
"""True if the import names a compiler-provided module."""
|
|
31
|
+
head = module_name.lstrip(".").split(".")[0]
|
|
32
|
+
return head in _COMPILER_MODULES
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _lower_source(source: str, path: Path | None = None) -> str:
|
|
36
|
+
flavour = frontend_for(path) if path is not None else "python"
|
|
37
|
+
if flavour == "typescript":
|
|
38
|
+
from .frontends.typescript import ts_to_python
|
|
39
|
+
return ts_to_python(source)
|
|
40
|
+
if flavour == "hybrid":
|
|
41
|
+
from .frontends.hybrid import hybrid_to_python
|
|
42
|
+
return hybrid_to_python(source)
|
|
43
|
+
return source
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_source(source: str, path: Path | None = None) -> tuple[list[FuncUnit], list[ClassUnit]]:
|
|
47
|
+
"""Parse source using the frontend that matches the file flavour."""
|
|
48
|
+
if path is not None and frontend_for(path) != "python":
|
|
49
|
+
from .analyzer import parse_source_full as _parse_py
|
|
50
|
+
return _parse_py(_lower_source(source, path))
|
|
51
|
+
return parse_source_full(source)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _collect_constants(source: str, path: Path | None = None) -> dict[str, object]:
|
|
55
|
+
"""Collect module-level constants using the matching frontend."""
|
|
56
|
+
if path is not None and frontend_for(path) != "python":
|
|
57
|
+
return collect_constants(_lower_source(source, path))
|
|
58
|
+
return collect_constants(source)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _collect_preamble(source: str, path: Path | None = None) -> dict[str, str]:
|
|
62
|
+
"""Collect ge_preamble/gePreamble blocks using the matching frontend."""
|
|
63
|
+
if path is not None and frontend_for(path) != "python":
|
|
64
|
+
return collect_preamble(_lower_source(source, path))
|
|
65
|
+
return collect_preamble(source)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_imports(source: str, source_path: Path) -> tuple[list[FuncUnit], list[ClassUnit], list[str]]:
|
|
69
|
+
"""Parse source, resolve all imports (transitively), and return merged units.
|
|
70
|
+
|
|
71
|
+
Imports are followed recursively so a module that only re-exports from
|
|
72
|
+
other modules (an aggregator) still contributes the full function set.
|
|
73
|
+
Each module file is visited once; cycles are broken by the visited set.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
(units, classes, warnings)
|
|
77
|
+
"""
|
|
78
|
+
warnings: list[str] = []
|
|
79
|
+
source_dir = source_path.parent if source_path else Path(".")
|
|
80
|
+
|
|
81
|
+
units: list[FuncUnit] = []
|
|
82
|
+
classes: list[ClassUnit] = []
|
|
83
|
+
constants: dict[str, object] = {}
|
|
84
|
+
preamble: dict[str, str] = {}
|
|
85
|
+
seen_modules: set[str] = set()
|
|
86
|
+
seen_units: set[str] = set()
|
|
87
|
+
seen_classes: set[str] = set()
|
|
88
|
+
|
|
89
|
+
def _merge_preamble(src_preamble: dict[str, str]) -> None:
|
|
90
|
+
for bk, code in src_preamble.items():
|
|
91
|
+
if bk in preamble:
|
|
92
|
+
preamble[bk] += "\n" + code
|
|
93
|
+
else:
|
|
94
|
+
preamble[bk] = code
|
|
95
|
+
|
|
96
|
+
def _visit(module_source: str, base_dir: Path, module_key: str, depth: int,
|
|
97
|
+
module_path: Path | None = None) -> None:
|
|
98
|
+
"""Parse one module and recurse into its imports (depth-first)."""
|
|
99
|
+
if module_key in seen_modules or depth > 64:
|
|
100
|
+
return
|
|
101
|
+
seen_modules.add(module_key)
|
|
102
|
+
|
|
103
|
+
# Non-Python flavours are lowered before the import scan; the same
|
|
104
|
+
# frontend also builds the IR below.
|
|
105
|
+
scan_source = _lower_source(module_source, module_path)
|
|
106
|
+
tree = ast.parse(scan_source)
|
|
107
|
+
|
|
108
|
+
# 1) recurse into this module's imports first (dependencies before dependents)
|
|
109
|
+
for node in tree.body:
|
|
110
|
+
if not isinstance(node, ast.ImportFrom):
|
|
111
|
+
continue
|
|
112
|
+
module_name = node.module or ""
|
|
113
|
+
if not module_name:
|
|
114
|
+
continue
|
|
115
|
+
if _is_compiler_module(module_name):
|
|
116
|
+
# GE runtime modules (`from pyeffic.backends import rust`) are
|
|
117
|
+
# compiler intrinsics, not user code — resolving them would
|
|
118
|
+
# pull the compiler's own sources into the program.
|
|
119
|
+
continue
|
|
120
|
+
dep_path = _find_module(module_name, base_dir)
|
|
121
|
+
if dep_path is None or not dep_path.exists():
|
|
122
|
+
if depth == 0:
|
|
123
|
+
warnings.append(f"could not find module '{module_name}' at line {node.lineno}")
|
|
124
|
+
continue
|
|
125
|
+
dep_source = dep_path.read_text(encoding="utf-8")
|
|
126
|
+
dep_key = str(dep_path.resolve())
|
|
127
|
+
_visit(dep_source, dep_path.parent, dep_key, depth + 1, dep_path)
|
|
128
|
+
|
|
129
|
+
# 2) then add this module's own definitions
|
|
130
|
+
mod_units, mod_classes = _parse_source(module_source, module_path)
|
|
131
|
+
for u in mod_units:
|
|
132
|
+
if u.name not in seen_units:
|
|
133
|
+
seen_units.add(u.name)
|
|
134
|
+
units.append(u)
|
|
135
|
+
for c in mod_classes:
|
|
136
|
+
if c.name not in seen_classes:
|
|
137
|
+
seen_classes.add(c.name)
|
|
138
|
+
classes.append(c)
|
|
139
|
+
constants.update(_collect_constants(module_source, module_path))
|
|
140
|
+
_merge_preamble(_collect_preamble(module_source, module_path))
|
|
141
|
+
|
|
142
|
+
_visit(source, source_dir, str(source_path.resolve()) if source_path else "<source>", 0,
|
|
143
|
+
source_path)
|
|
144
|
+
|
|
145
|
+
globals()["_LAST_CONSTANTS"] = constants
|
|
146
|
+
globals()["_LAST_PREAMBLE"] = preamble
|
|
147
|
+
|
|
148
|
+
return units, classes, warnings
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_last_constants() -> dict[str, object]:
|
|
152
|
+
"""Return the constants collected during the last resolve_imports call."""
|
|
153
|
+
return globals().get("_LAST_CONSTANTS", {})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_last_preamble() -> dict[str, str]:
|
|
157
|
+
"""Return the preamble collected during the last resolve_imports call."""
|
|
158
|
+
return globals().get("_LAST_PREAMBLE", {})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _find_module(module_name: str, source_dir: Path) -> Path | None:
|
|
162
|
+
"""Find a GE source file for the given module name.
|
|
163
|
+
|
|
164
|
+
Both flavours are searched, Python-like first:
|
|
165
|
+
app.main -> app/main.ge.py, app/main.ts.ge.py, app/main.ge.ts
|
|
166
|
+
"""
|
|
167
|
+
# handle relative imports (from .module import ...)
|
|
168
|
+
if module_name.startswith("."):
|
|
169
|
+
module_name = module_name.lstrip(".")
|
|
170
|
+
|
|
171
|
+
# handle dotted module names: app.main -> app/main.ge.py
|
|
172
|
+
parts = module_name.split(".")
|
|
173
|
+
base = parts[-1]
|
|
174
|
+
# candidate file names for the module leaf, in preference order.
|
|
175
|
+
# `.ge` (hybrid) is canonical; the single-flavour variants are honoured
|
|
176
|
+
# next, and a plain .py is the last resort.
|
|
177
|
+
leaf_names = (f"{base}.ge", f"{base}.ge.py", f"{base}.ge.ts",
|
|
178
|
+
f"{base}.ts.ge.py", f"{base}.py")
|
|
179
|
+
|
|
180
|
+
# search in source_dir and parent directories (up to 3 levels)
|
|
181
|
+
search_dirs = [source_dir]
|
|
182
|
+
parent = source_dir.parent
|
|
183
|
+
for _ in range(3):
|
|
184
|
+
search_dirs.append(parent)
|
|
185
|
+
parent = parent.parent
|
|
186
|
+
|
|
187
|
+
for search_dir in search_dirs:
|
|
188
|
+
if len(parts) > 1:
|
|
189
|
+
# dotted name: only resolve relative to the package path.
|
|
190
|
+
# app.main -> search_dir/app/main.ge.py
|
|
191
|
+
# Never fall back to matching just the leaf, or `app.main` would
|
|
192
|
+
# wrongly resolve to a sibling `main.ge.py`.
|
|
193
|
+
pkg_dir = search_dir.joinpath(*parts[:-1])
|
|
194
|
+
for leaf in leaf_names:
|
|
195
|
+
candidate = pkg_dir / leaf
|
|
196
|
+
if candidate.exists():
|
|
197
|
+
return candidate
|
|
198
|
+
# package directory: app/main/__init__.ge
|
|
199
|
+
for leaf in ("__init__.ge", "__init__.ge.py", "__init__.ge.ts",
|
|
200
|
+
"__init__.py"):
|
|
201
|
+
candidate = pkg_dir / leaf
|
|
202
|
+
if candidate.exists():
|
|
203
|
+
return candidate
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# simple name: search_dir/<name>.<flavour>
|
|
207
|
+
for leaf in leaf_names:
|
|
208
|
+
candidate = search_dir / leaf
|
|
209
|
+
if candidate.exists():
|
|
210
|
+
return candidate
|
|
211
|
+
|
|
212
|
+
# search_dir / name / __init__.ge
|
|
213
|
+
pkg_dir = search_dir / module_name
|
|
214
|
+
for leaf in ("__init__.ge", "__init__.ge.py", "__init__.ge.ts",
|
|
215
|
+
"__init__.py"):
|
|
216
|
+
candidate = pkg_dir / leaf
|
|
217
|
+
if candidate.exists():
|
|
218
|
+
return candidate
|
|
219
|
+
|
|
220
|
+
return None
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""GE packer: bundle artifacts into a single .ge distribution package.
|
|
2
|
+
|
|
3
|
+
Like npm: `ge pack` builds everything and bundles it into one `.ge` file.
|
|
4
|
+
`ge install package.ge` unpacks and compiles for any target (Windows, Android).
|
|
5
|
+
|
|
6
|
+
The .ge file is a zip archive containing:
|
|
7
|
+
package.json — metadata (name, version, ffi_exports, targets)
|
|
8
|
+
source.ge.py — the original GE source (for recompilation on any platform)
|
|
9
|
+
native/
|
|
10
|
+
ge_logic.rs — generated Rust source (for cross-compilation)
|
|
11
|
+
ge_logic.dll — pre-compiled Windows native lib (if available)
|
|
12
|
+
dart/
|
|
13
|
+
bindings.dart — generated Dart FFI bindings
|
|
14
|
+
main.dart — generated Flutter UI
|
|
15
|
+
pubspec.yaml — Flutter project config
|
|
16
|
+
|
|
17
|
+
Compression shrinks DISTRIBUTION SIZE, not runtime speed.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import zlib
|
|
24
|
+
import zipfile
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FileSize:
|
|
31
|
+
path: Path
|
|
32
|
+
raw: int
|
|
33
|
+
packed: int
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def ratio(self) -> float:
|
|
37
|
+
return self.packed / self.raw if self.raw else 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PackReport:
|
|
42
|
+
package: Path | None = None
|
|
43
|
+
files: list[FileSize] = field(default_factory=list)
|
|
44
|
+
total_raw: int = 0
|
|
45
|
+
total_packed: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def ratio(self) -> float:
|
|
49
|
+
return self.total_packed / self.total_raw if self.total_raw else 0.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class PackageMeta:
|
|
54
|
+
"""Metadata stored inside the .ge package (package.json)."""
|
|
55
|
+
name: str = ""
|
|
56
|
+
version: str = "0.1.0"
|
|
57
|
+
ge_version: str = "0.1.0"
|
|
58
|
+
app_name: str = ""
|
|
59
|
+
lib_name: str = "ge_logic"
|
|
60
|
+
backend: str = "rust"
|
|
61
|
+
ffi_exports: list[str] = field(default_factory=list)
|
|
62
|
+
targets: list[str] = field(default_factory=lambda: ["windows", "android"])
|
|
63
|
+
source_file: str = "source.ge.py"
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
return {
|
|
67
|
+
"name": self.name, "version": self.version,
|
|
68
|
+
"ge_version": self.ge_version, "app_name": self.app_name,
|
|
69
|
+
"lib_name": self.lib_name, "backend": self.backend,
|
|
70
|
+
"ffi_exports": self.ffi_exports, "targets": self.targets,
|
|
71
|
+
"source_file": self.source_file,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_dict(cls, d: dict) -> "PackageMeta":
|
|
76
|
+
return cls(
|
|
77
|
+
name=d.get("name", ""), version=d.get("version", "0.1.0"),
|
|
78
|
+
ge_version=d.get("ge_version", "0.1.0"),
|
|
79
|
+
app_name=d.get("app_name", ""), lib_name=d.get("lib_name", "ge_logic"),
|
|
80
|
+
backend=d.get("backend", "rust"),
|
|
81
|
+
ffi_exports=d.get("ffi_exports", []),
|
|
82
|
+
targets=d.get("targets", ["windows", "android"]),
|
|
83
|
+
source_file=d.get("source_file", "source.ge.py"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def minify_source(text: str) -> str:
|
|
88
|
+
"""Strip comments and collapse runs of whitespace. Preserves strings."""
|
|
89
|
+
out_lines = []
|
|
90
|
+
for line in text.splitlines():
|
|
91
|
+
stripped = re.sub(r'(?:"[^"]*")|(\s*//[^\n]*)', lambda m: m.group(1) or "", line)
|
|
92
|
+
stripped = re.sub(r"\s+", " ", stripped).strip()
|
|
93
|
+
if stripped:
|
|
94
|
+
out_lines.append(stripped)
|
|
95
|
+
return "\n".join(out_lines)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def strip_binary_size(path: Path) -> int:
|
|
99
|
+
return path.stat().st_size if path.exists() else 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---- .ge package format (zip-based, npm-like) ----
|
|
103
|
+
|
|
104
|
+
def pack_ge(source: Path, meta: PackageMeta, artifacts: dict[str, Path],
|
|
105
|
+
out_path: Path, minify: bool = True) -> PackReport:
|
|
106
|
+
"""Bundle source + generated artifacts + metadata into a single .ge zip.
|
|
107
|
+
|
|
108
|
+
artifacts: dict mapping archive path -> local file path, e.g.:
|
|
109
|
+
{"native/ge_logic.rs": Path("ge_build/myrent/lib/ge_logic.rs"),
|
|
110
|
+
"dart/bindings.dart": Path("ge_build/myrent/lib/bindings.dart"), ...}
|
|
111
|
+
"""
|
|
112
|
+
report = PackReport(package=out_path)
|
|
113
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
|
116
|
+
# 1) metadata
|
|
117
|
+
meta_json = json.dumps(meta.to_dict(), indent=2)
|
|
118
|
+
zf.writestr("package.json", meta_json)
|
|
119
|
+
report.files.append(FileSize(Path("package.json"), len(meta_json), len(meta_json.encode())))
|
|
120
|
+
report.total_raw += len(meta_json)
|
|
121
|
+
report.total_packed += len(meta_json.encode())
|
|
122
|
+
|
|
123
|
+
# 2) GE source
|
|
124
|
+
raw = source.read_bytes()
|
|
125
|
+
if minify:
|
|
126
|
+
text = raw.decode("utf-8", errors="ignore")
|
|
127
|
+
mini = minify_source(text)
|
|
128
|
+
data = mini.encode("utf-8")
|
|
129
|
+
else:
|
|
130
|
+
data = raw
|
|
131
|
+
zf.writestr(meta.source_file, data)
|
|
132
|
+
report.files.append(FileSize(source, len(raw), len(data)))
|
|
133
|
+
report.total_raw += len(raw)
|
|
134
|
+
report.total_packed += len(data)
|
|
135
|
+
|
|
136
|
+
# 3) artifacts (native lib, Rust source, Dart files, pubspec)
|
|
137
|
+
for arc_name, local_path in artifacts.items():
|
|
138
|
+
if not local_path or not local_path.exists():
|
|
139
|
+
continue
|
|
140
|
+
raw = local_path.read_bytes()
|
|
141
|
+
# only minify the GE source (.py); native source (.rs/.cpp) and Dart
|
|
142
|
+
# must be preserved exactly for cross-compilation and Flutter
|
|
143
|
+
if minify and local_path.suffix == ".py":
|
|
144
|
+
text = raw.decode("utf-8", errors="ignore")
|
|
145
|
+
mini = minify_source(text)
|
|
146
|
+
data = mini.encode("utf-8")
|
|
147
|
+
else:
|
|
148
|
+
data = raw
|
|
149
|
+
zf.writestr(arc_name, data)
|
|
150
|
+
report.files.append(FileSize(local_path, len(raw), len(data)))
|
|
151
|
+
report.total_raw += len(raw)
|
|
152
|
+
report.total_packed += len(data)
|
|
153
|
+
|
|
154
|
+
report.total_packed = out_path.stat().st_size
|
|
155
|
+
return report
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def pack_simple(source: Path, out_path: Path, app_name: str = "",
|
|
159
|
+
backend: str = "rust", minify: bool = True) -> PackReport:
|
|
160
|
+
"""Convenience: pack a single source file into a .ge package.
|
|
161
|
+
|
|
162
|
+
This is the simplest API for the common case of bundling one .ge.py file.
|
|
163
|
+
"""
|
|
164
|
+
meta = PackageMeta(
|
|
165
|
+
name=app_name or source.stem.replace(".ge", ""),
|
|
166
|
+
app_name=app_name or source.stem.replace(".ge", ""),
|
|
167
|
+
backend=backend,
|
|
168
|
+
)
|
|
169
|
+
return pack_ge(source, meta, {}, out_path, minify=minify)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def unpack_ge(pkg_path: Path, dest_dir: Path) -> PackageMeta:
|
|
173
|
+
"""Extract a .ge package to dest_dir. Returns the parsed metadata."""
|
|
174
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
with zipfile.ZipFile(pkg_path, "r") as zf:
|
|
176
|
+
zf.extractall(dest_dir)
|
|
177
|
+
meta_path = dest_dir / "package.json"
|
|
178
|
+
if meta_path.exists():
|
|
179
|
+
return PackageMeta.from_dict(json.loads(meta_path.read_text(encoding="utf-8")))
|
|
180
|
+
return PackageMeta()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---- legacy .gepkg format (backward compat) ----
|
|
184
|
+
|
|
185
|
+
def pack(files: list[Path] | Path, out_path: Path, minify: bool = True) -> PackReport:
|
|
186
|
+
"""Bundle files into a zlib-compressed .gepkg. Returns a size report.
|
|
187
|
+
|
|
188
|
+
Accepts either a single Path or a list of Paths for convenience.
|
|
189
|
+
"""
|
|
190
|
+
if isinstance(files, Path):
|
|
191
|
+
files = [files]
|
|
192
|
+
report = PackReport(package=out_path)
|
|
193
|
+
blob = bytearray()
|
|
194
|
+
for f in files:
|
|
195
|
+
if not f.exists():
|
|
196
|
+
continue
|
|
197
|
+
raw = f.read_bytes()
|
|
198
|
+
if minify and f.suffix in (".rs", ".cpp", ".dart", ".py"):
|
|
199
|
+
text = raw.decode("utf-8", errors="ignore")
|
|
200
|
+
mini = minify_source(text)
|
|
201
|
+
data = mini.encode("utf-8")
|
|
202
|
+
else:
|
|
203
|
+
data = raw
|
|
204
|
+
name = f.name.encode("utf-8")
|
|
205
|
+
blob += len(name).to_bytes(4, "little") + name
|
|
206
|
+
blob += len(data).to_bytes(4, "little") + data
|
|
207
|
+
report.files.append(FileSize(f, len(raw), len(data)))
|
|
208
|
+
report.total_raw += len(raw)
|
|
209
|
+
report.total_packed += len(data)
|
|
210
|
+
|
|
211
|
+
compressed = zlib.compress(bytes(blob), level=9)
|
|
212
|
+
out_path.write_bytes(compressed)
|
|
213
|
+
report.total_packed = len(compressed)
|
|
214
|
+
return report
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def fmt_size(n: int) -> str:
|
|
218
|
+
if n < 1024:
|
|
219
|
+
return f"{n} B"
|
|
220
|
+
if n < 1024 * 1024:
|
|
221
|
+
return f"{n/1024:.1f} KB"
|
|
222
|
+
return f"{n/(1024*1024):.2f} MB"
|