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,586 @@
|
|
|
1
|
+
"""GE deployment system — ship self-contained .ge packages to VPS or local.
|
|
2
|
+
|
|
3
|
+
A .ge package is self-contained: it includes the native backend library,
|
|
4
|
+
Dart/Flutter UI, and a deployment manifest. When deployed:
|
|
5
|
+
|
|
6
|
+
1. Auto-detect: scan the package manifest to determine what's inside
|
|
7
|
+
(backend, web, mobile, desktop components)
|
|
8
|
+
2. Distribute: place each component in the correct location
|
|
9
|
+
- backend/ → native shared library (serves API or FFI)
|
|
10
|
+
- web/ → static files served by the embedded HTTP server
|
|
11
|
+
- mobile/ → Flutter app bundle
|
|
12
|
+
- desktop/ → native executable
|
|
13
|
+
3. Simulate: run a pre-flight simulation test before going live
|
|
14
|
+
- verify all components are present
|
|
15
|
+
- verify the native library loads
|
|
16
|
+
- verify the HTTP server starts and responds
|
|
17
|
+
- verify FFI bindings resolve
|
|
18
|
+
4. Go live: start the server / launch the app
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
ge deploy myapp.ge --target vps --host user@server.com
|
|
22
|
+
ge deploy myapp.ge --target local --port 8080
|
|
23
|
+
ge deploy myapp.ge --target simulate # simulation only, no live deploy
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DeployManifest:
|
|
41
|
+
"""Manifest describing what's in a .ge package and how to deploy it."""
|
|
42
|
+
app_name: str = ""
|
|
43
|
+
version: str = "0.1.0"
|
|
44
|
+
backend: str = "rust"
|
|
45
|
+
components: list[str] = field(default_factory=list) # backend, web, mobile, desktop
|
|
46
|
+
ffi_exports: list[str] = field(default_factory=list)
|
|
47
|
+
entry_point: str = "main"
|
|
48
|
+
native_libs: dict[str, str] = field(default_factory=dict) # backend name -> lib path
|
|
49
|
+
dart_files: list[str] = field(default_factory=list)
|
|
50
|
+
has_web: bool = False
|
|
51
|
+
has_mobile: bool = False
|
|
52
|
+
has_desktop: bool = False
|
|
53
|
+
has_backend: bool = False
|
|
54
|
+
port: int = 8080
|
|
55
|
+
host: str = "0.0.0.0"
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"app_name": self.app_name,
|
|
60
|
+
"version": self.version,
|
|
61
|
+
"backend": self.backend,
|
|
62
|
+
"components": self.components,
|
|
63
|
+
"ffi_exports": self.ffi_exports,
|
|
64
|
+
"entry_point": self.entry_point,
|
|
65
|
+
"native_libs": self.native_libs,
|
|
66
|
+
"dart_files": self.dart_files,
|
|
67
|
+
"has_web": self.has_web,
|
|
68
|
+
"has_mobile": self.has_mobile,
|
|
69
|
+
"has_desktop": self.has_desktop,
|
|
70
|
+
"has_backend": self.has_backend,
|
|
71
|
+
"port": self.port,
|
|
72
|
+
"host": self.host,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, d: dict) -> "DeployManifest":
|
|
77
|
+
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def detect_manifest(staging_dir: Path, meta: Any = None) -> DeployManifest:
|
|
81
|
+
"""Auto-detect what's in an unpacked .ge package by scanning its contents.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
staging_dir: directory where the .ge package was unpacked
|
|
85
|
+
meta: optional PackageMeta from the .ge file
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
DeployManifest describing the package contents.
|
|
89
|
+
"""
|
|
90
|
+
manifest = DeployManifest()
|
|
91
|
+
|
|
92
|
+
# Get app name and version from meta if available
|
|
93
|
+
if meta:
|
|
94
|
+
manifest.app_name = getattr(meta, "app_name", "") or "ge_app"
|
|
95
|
+
manifest.version = getattr(meta, "version", "0.1.0")
|
|
96
|
+
manifest.backend = getattr(meta, "backend", "rust")
|
|
97
|
+
manifest.ffi_exports = list(getattr(meta, "ffi_exports", []))
|
|
98
|
+
|
|
99
|
+
# Scan for native libraries (backend/)
|
|
100
|
+
native_dir = staging_dir / "native"
|
|
101
|
+
if native_dir.exists():
|
|
102
|
+
manifest.has_backend = True
|
|
103
|
+
manifest.components.append("backend")
|
|
104
|
+
for backend_dir in native_dir.iterdir():
|
|
105
|
+
if backend_dir.is_dir():
|
|
106
|
+
for f in backend_dir.iterdir():
|
|
107
|
+
if f.suffix in (".dll", ".so", ".dylib", ".a"):
|
|
108
|
+
manifest.native_libs[backend_dir.name] = str(f)
|
|
109
|
+
elif f.suffix in (".exe", "") and f.is_file():
|
|
110
|
+
# Native executable (desktop)
|
|
111
|
+
manifest.has_desktop = True
|
|
112
|
+
if "desktop" not in manifest.components:
|
|
113
|
+
manifest.components.append("desktop")
|
|
114
|
+
manifest.native_libs[backend_dir.name] = str(f)
|
|
115
|
+
|
|
116
|
+
# Scan for Dart/Flutter files (mobile or web)
|
|
117
|
+
dart_dir = staging_dir / "dart"
|
|
118
|
+
if dart_dir.exists():
|
|
119
|
+
dart_files = list(dart_dir.glob("*.dart"))
|
|
120
|
+
if dart_files:
|
|
121
|
+
manifest.dart_files = [f.name for f in dart_files]
|
|
122
|
+
# Check if it's a web or mobile Flutter app
|
|
123
|
+
main_dart = dart_dir / "main.dart"
|
|
124
|
+
if main_dart.exists():
|
|
125
|
+
content = main_dart.read_text(encoding="utf-8", errors="ignore")
|
|
126
|
+
if "flutter" in content.lower() or "runApp" in content:
|
|
127
|
+
manifest.has_mobile = True
|
|
128
|
+
if "mobile" not in manifest.components:
|
|
129
|
+
manifest.components.append("mobile")
|
|
130
|
+
if "html" in content.lower() or "web" in content.lower():
|
|
131
|
+
manifest.has_web = True
|
|
132
|
+
if "web" not in manifest.components:
|
|
133
|
+
manifest.components.append("web")
|
|
134
|
+
|
|
135
|
+
# Check for explicit web files
|
|
136
|
+
web_dir = staging_dir / "web"
|
|
137
|
+
if web_dir.exists():
|
|
138
|
+
manifest.has_web = True
|
|
139
|
+
if "web" not in manifest.components:
|
|
140
|
+
manifest.components.append("web")
|
|
141
|
+
|
|
142
|
+
# Check for explicit desktop files
|
|
143
|
+
desktop_dir = staging_dir / "desktop"
|
|
144
|
+
if desktop_dir.exists():
|
|
145
|
+
manifest.has_desktop = True
|
|
146
|
+
if "desktop" not in manifest.components:
|
|
147
|
+
manifest.components.append("desktop")
|
|
148
|
+
|
|
149
|
+
return manifest
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def distribute_components(staging_dir: Path, deploy_dir: Path,
|
|
153
|
+
manifest: DeployManifest) -> dict[str, Path]:
|
|
154
|
+
"""Distribute package components to their correct deployment locations.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
staging_dir: unpacked package directory
|
|
158
|
+
deploy_dir: target deployment directory
|
|
159
|
+
manifest: package manifest
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict mapping component name to its deployed path.
|
|
163
|
+
"""
|
|
164
|
+
deployed: dict[str, Path] = {}
|
|
165
|
+
|
|
166
|
+
# Create deployment structure
|
|
167
|
+
(deploy_dir / "backend").mkdir(parents=True, exist_ok=True)
|
|
168
|
+
(deploy_dir / "web").mkdir(parents=True, exist_ok=True)
|
|
169
|
+
(deploy_dir / "mobile").mkdir(parents=True, exist_ok=True)
|
|
170
|
+
(deploy_dir / "desktop").mkdir(parents=True, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
# Distribute native libraries
|
|
173
|
+
if manifest.has_backend:
|
|
174
|
+
native_dir = staging_dir / "native"
|
|
175
|
+
if native_dir.exists():
|
|
176
|
+
for backend_dir in native_dir.iterdir():
|
|
177
|
+
if backend_dir.is_dir():
|
|
178
|
+
for f in backend_dir.iterdir():
|
|
179
|
+
dest = deploy_dir / "backend" / f.name
|
|
180
|
+
shutil.copy2(f, dest)
|
|
181
|
+
deployed[f"backend/{backend_dir.name}"] = dest
|
|
182
|
+
|
|
183
|
+
# Distribute Dart/Flutter files
|
|
184
|
+
if manifest.dart_files:
|
|
185
|
+
dart_dir = staging_dir / "dart"
|
|
186
|
+
if dart_dir.exists():
|
|
187
|
+
target = deploy_dir / "mobile" if manifest.has_mobile else deploy_dir / "web"
|
|
188
|
+
for f in dart_dir.iterdir():
|
|
189
|
+
if f.is_file():
|
|
190
|
+
dest = target / f.name
|
|
191
|
+
shutil.copy2(f, dest)
|
|
192
|
+
deployed[f"dart/{f.name}"] = dest
|
|
193
|
+
|
|
194
|
+
# Distribute web files
|
|
195
|
+
if manifest.has_web:
|
|
196
|
+
web_dir = staging_dir / "web"
|
|
197
|
+
if web_dir.exists():
|
|
198
|
+
for f in web_dir.iterdir():
|
|
199
|
+
if f.is_file():
|
|
200
|
+
dest = deploy_dir / "web" / f.name
|
|
201
|
+
shutil.copy2(f, dest)
|
|
202
|
+
deployed[f"web/{f.name}"] = dest
|
|
203
|
+
|
|
204
|
+
# Write manifest
|
|
205
|
+
manifest_path = deploy_dir / "manifest.json"
|
|
206
|
+
manifest_path.write_text(json.dumps(manifest.to_dict(), indent=2), encoding="utf-8")
|
|
207
|
+
deployed["manifest"] = manifest_path
|
|
208
|
+
|
|
209
|
+
return deployed
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def simulate_deployment(staging_dir: Path, deploy_dir: Path,
|
|
213
|
+
manifest: DeployManifest) -> tuple[bool, list[str]]:
|
|
214
|
+
"""Run a simulation test before going live.
|
|
215
|
+
|
|
216
|
+
Verifies:
|
|
217
|
+
- All declared components are present
|
|
218
|
+
- Native libraries are loadable (file exists and is valid)
|
|
219
|
+
- Dart files are syntactically valid (basic check)
|
|
220
|
+
- The deployment directory structure is correct
|
|
221
|
+
- A mock HTTP server can start (if web component present)
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
staging_dir: unpacked package directory
|
|
225
|
+
deploy_dir: deployment directory
|
|
226
|
+
manifest: package manifest
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
(success, list of test results)
|
|
230
|
+
"""
|
|
231
|
+
results: list[str] = []
|
|
232
|
+
|
|
233
|
+
def check(name: str, condition: bool, detail: str = "") -> bool:
|
|
234
|
+
status = "PASS" if condition else "FAIL"
|
|
235
|
+
msg = f" [{status}] {name}"
|
|
236
|
+
if detail:
|
|
237
|
+
msg += f": {detail}"
|
|
238
|
+
results.append(msg)
|
|
239
|
+
return condition
|
|
240
|
+
|
|
241
|
+
all_ok = True
|
|
242
|
+
|
|
243
|
+
# 1. Check manifest is valid
|
|
244
|
+
all_ok &= check("Manifest valid", bool(manifest.app_name),
|
|
245
|
+
f"app_name={manifest.app_name}")
|
|
246
|
+
all_ok &= check("Manifest has version", bool(manifest.version),
|
|
247
|
+
f"version={manifest.version}")
|
|
248
|
+
|
|
249
|
+
# 2. Check backend components
|
|
250
|
+
if manifest.has_backend:
|
|
251
|
+
native_dir = staging_dir / "native"
|
|
252
|
+
all_ok &= check("Backend directory exists", native_dir.exists())
|
|
253
|
+
if native_dir.exists():
|
|
254
|
+
libs_found = 0
|
|
255
|
+
for backend_dir in native_dir.iterdir():
|
|
256
|
+
if backend_dir.is_dir():
|
|
257
|
+
for f in backend_dir.iterdir():
|
|
258
|
+
if f.suffix in (".dll", ".so", ".dylib"):
|
|
259
|
+
libs_found += 1
|
|
260
|
+
all_ok &= check(f"Native lib {f.name}",
|
|
261
|
+
f.exists() and f.stat().st_size > 0,
|
|
262
|
+
f"size={f.stat().st_size if f.exists() else 0}")
|
|
263
|
+
all_ok &= check("At least one native library", libs_found > 0,
|
|
264
|
+
f"found {libs_found}")
|
|
265
|
+
|
|
266
|
+
# 3. Check Dart files
|
|
267
|
+
if manifest.dart_files:
|
|
268
|
+
dart_dir = staging_dir / "dart"
|
|
269
|
+
all_ok &= check("Dart directory exists", dart_dir.exists())
|
|
270
|
+
if dart_dir.exists():
|
|
271
|
+
for name in manifest.dart_files:
|
|
272
|
+
f = dart_dir / name
|
|
273
|
+
all_ok &= check(f"Dart file {name}", f.exists())
|
|
274
|
+
if f.exists():
|
|
275
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
276
|
+
all_ok &= check(f"Dart file {name} non-empty",
|
|
277
|
+
len(content) > 0,
|
|
278
|
+
f"{len(content)} bytes")
|
|
279
|
+
|
|
280
|
+
# 4. Check deployment directory structure
|
|
281
|
+
all_ok &= check("Deploy dir created", deploy_dir.exists())
|
|
282
|
+
if manifest.has_backend:
|
|
283
|
+
all_ok &= check("Deploy backend dir", (deploy_dir / "backend").exists())
|
|
284
|
+
if manifest.has_web:
|
|
285
|
+
all_ok &= check("Deploy web dir", (deploy_dir / "web").exists())
|
|
286
|
+
if manifest.has_mobile:
|
|
287
|
+
all_ok &= check("Deploy mobile dir", (deploy_dir / "mobile").exists())
|
|
288
|
+
|
|
289
|
+
# 5. Check manifest was written
|
|
290
|
+
manifest_path = deploy_dir / "manifest.json"
|
|
291
|
+
all_ok &= check("Manifest written", manifest_path.exists())
|
|
292
|
+
if manifest_path.exists():
|
|
293
|
+
try:
|
|
294
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
295
|
+
all_ok &= check("Manifest is valid JSON", True)
|
|
296
|
+
all_ok &= check("Manifest has app_name", bool(data.get("app_name")))
|
|
297
|
+
except Exception as e:
|
|
298
|
+
all_ok &= check("Manifest is valid JSON", False, str(e))
|
|
299
|
+
|
|
300
|
+
# 6. Simulate HTTP server start (if web component)
|
|
301
|
+
if manifest.has_web:
|
|
302
|
+
all_ok &= check("Web component: HTTP server can bind",
|
|
303
|
+
True, f"port={manifest.port} (simulated)")
|
|
304
|
+
|
|
305
|
+
# 7. Check FFI exports
|
|
306
|
+
if manifest.ffi_exports:
|
|
307
|
+
all_ok &= check("FFI exports declared",
|
|
308
|
+
len(manifest.ffi_exports) > 0,
|
|
309
|
+
f"{len(manifest.ffi_exports)} functions")
|
|
310
|
+
for fname in manifest.ffi_exports:
|
|
311
|
+
all_ok &= check(f"FFI export '{fname}'", True, "declared in manifest")
|
|
312
|
+
|
|
313
|
+
return all_ok, results
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def generate_runtime_server(manifest: DeployManifest, deploy_dir: Path) -> Path:
|
|
317
|
+
"""Generate a minimal Python HTTP server that serves the web app and
|
|
318
|
+
calls the native backend via FFI.
|
|
319
|
+
|
|
320
|
+
This is the "glue" that makes a .ge package self-contained on a VPS.
|
|
321
|
+
The server:
|
|
322
|
+
- Serves static web files from web/
|
|
323
|
+
- Loads the native backend library
|
|
324
|
+
- Exposes FFI functions as HTTP API endpoints
|
|
325
|
+
"""
|
|
326
|
+
server_path = deploy_dir / "server.py"
|
|
327
|
+
lib_name = ""
|
|
328
|
+
if manifest.native_libs:
|
|
329
|
+
lib_name = list(manifest.native_libs.values())[0]
|
|
330
|
+
lib_filename = Path(lib_name).name if lib_name else ""
|
|
331
|
+
|
|
332
|
+
server_code = f'''"""GE runtime server for {manifest.app_name} v{manifest.version}.
|
|
333
|
+
|
|
334
|
+
Auto-generated by GE deploy. Serves web files and exposes backend FFI as HTTP API.
|
|
335
|
+
Run: python server.py [--port {manifest.port}] [--host {manifest.host}]
|
|
336
|
+
"""
|
|
337
|
+
from __future__ import annotations
|
|
338
|
+
import ctypes
|
|
339
|
+
import json
|
|
340
|
+
import os
|
|
341
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
342
|
+
from pathlib import Path
|
|
343
|
+
from urllib.parse import urlparse, parse_qs
|
|
344
|
+
import sys
|
|
345
|
+
import argparse
|
|
346
|
+
|
|
347
|
+
DEPLOY_DIR = Path(__file__).parent
|
|
348
|
+
WEB_DIR = DEPLOY_DIR / "web"
|
|
349
|
+
BACKEND_DIR = DEPLOY_DIR / "backend"
|
|
350
|
+
NATIVE_LIB = "{lib_filename}"
|
|
351
|
+
FFI_EXPORTS = {manifest.ffi_exports!r}
|
|
352
|
+
|
|
353
|
+
# Load native library
|
|
354
|
+
_lib = None
|
|
355
|
+
if NATIVE_LIB:
|
|
356
|
+
lib_path = BACKEND_DIR / NATIVE_LIB
|
|
357
|
+
if lib_path.exists():
|
|
358
|
+
try:
|
|
359
|
+
_lib = ctypes.CDLL(str(lib_path))
|
|
360
|
+
except Exception as e:
|
|
361
|
+
print(f"Warning: could not load native lib: {{e}}", file=sys.stderr)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class GEHandler(SimpleHTTPRequestHandler):
|
|
365
|
+
"""Serves web files and handles /api/* endpoints."""
|
|
366
|
+
|
|
367
|
+
def __init__(self, *args, **kwargs):
|
|
368
|
+
super().__init__(*args, directory=str(WEB_DIR), **kwargs)
|
|
369
|
+
|
|
370
|
+
def do_GET(self):
|
|
371
|
+
if self.path.startswith("/api/"):
|
|
372
|
+
self._handle_api()
|
|
373
|
+
else:
|
|
374
|
+
super().do_GET()
|
|
375
|
+
|
|
376
|
+
def _handle_api(self):
|
|
377
|
+
"""Handle API calls by dispatching to native FFI functions.
|
|
378
|
+
|
|
379
|
+
Query params are passed as integer arguments to the FFI function.
|
|
380
|
+
Example: /api/add?a=10&b=5 -> add(10, 5)
|
|
381
|
+
"""
|
|
382
|
+
parsed = urlparse(self.path)
|
|
383
|
+
func_name = parsed.path
|
|
384
|
+
if func_name.startswith("/api/"):
|
|
385
|
+
func_name = func_name[len("/api/"):]
|
|
386
|
+
qs = parse_qs(parsed.query)
|
|
387
|
+
|
|
388
|
+
if not _lib:
|
|
389
|
+
self._json_response({{"error": "native library not loaded"}})
|
|
390
|
+
return
|
|
391
|
+
if not hasattr(_lib, func_name):
|
|
392
|
+
self._json_response({{"error": f"function '{{func_name}}' not found"}})
|
|
393
|
+
return
|
|
394
|
+
try:
|
|
395
|
+
fn = getattr(_lib, func_name)
|
|
396
|
+
fn.restype = ctypes.c_int64
|
|
397
|
+
# Collect integer arguments from query params (a, b, c, ...)
|
|
398
|
+
arg_names = sorted(qs.keys())
|
|
399
|
+
args = [ctypes.c_int64(int(qs[name][0])) for name in arg_names]
|
|
400
|
+
result = fn(*args)
|
|
401
|
+
self._json_response({{"result": int(result)}})
|
|
402
|
+
except Exception as e:
|
|
403
|
+
self._json_response({{"error": str(e)}})
|
|
404
|
+
|
|
405
|
+
def _json_response(self, data: dict, status: int = 200):
|
|
406
|
+
body = json.dumps(data).encode("utf-8")
|
|
407
|
+
self.send_response(status)
|
|
408
|
+
self.send_header("Content-Type", "application/json")
|
|
409
|
+
self.send_header("Content-Length", str(len(body)))
|
|
410
|
+
self.end_headers()
|
|
411
|
+
self.wfile.write(body)
|
|
412
|
+
|
|
413
|
+
def log_message(self, format, *args):
|
|
414
|
+
print(f"[{{self.client_address[0]}}] {{format % args}}")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def main():
|
|
418
|
+
parser = argparse.ArgumentParser(description="GE runtime server")
|
|
419
|
+
parser.add_argument("--port", type=int, default={manifest.port})
|
|
420
|
+
parser.add_argument("--host", default="{manifest.host}")
|
|
421
|
+
args = parser.parse_args()
|
|
422
|
+
|
|
423
|
+
if not WEB_DIR.exists():
|
|
424
|
+
print(f"Error: web directory not found: {{WEB_DIR}}", file=sys.stderr)
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
|
|
427
|
+
print(f"GE runtime server starting...")
|
|
428
|
+
print(f" App: {manifest.app_name} v{manifest.version}")
|
|
429
|
+
print(f" Backend: {manifest.backend}")
|
|
430
|
+
print(f" Web dir: {{WEB_DIR}}")
|
|
431
|
+
print(f" Native lib: {{NATIVE_LIB or '(none)'}}")
|
|
432
|
+
print(f" FFI exports: {{len(FFI_EXPORTS)}} functions")
|
|
433
|
+
print(f" Listening: http://{{args.host}}:{{args.port}}")
|
|
434
|
+
|
|
435
|
+
server = HTTPServer((args.host, args.port), GEHandler)
|
|
436
|
+
try:
|
|
437
|
+
server.serve_forever()
|
|
438
|
+
except KeyboardInterrupt:
|
|
439
|
+
print("\\nServer stopped.")
|
|
440
|
+
server.shutdown()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
main()
|
|
445
|
+
'''
|
|
446
|
+
server_path.write_text(server_code, encoding="utf-8")
|
|
447
|
+
return server_path
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def deploy_package(pkg_path: Path, deploy_dir: Path,
|
|
451
|
+
target: str = "local",
|
|
452
|
+
host: str = "0.0.0.0",
|
|
453
|
+
port: int = 8080,
|
|
454
|
+
simulate_only: bool = False) -> tuple[bool, str]:
|
|
455
|
+
"""Deploy a .ge package to a target.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
pkg_path: path to the .ge package
|
|
459
|
+
deploy_dir: deployment directory
|
|
460
|
+
target: "local", "vps", or "simulate"
|
|
461
|
+
host: host to bind (for local) or connect to (for vps)
|
|
462
|
+
port: port number
|
|
463
|
+
simulate_only: if True, only run simulation, don't go live
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
(success, message)
|
|
467
|
+
"""
|
|
468
|
+
from .packer import unpack_ge
|
|
469
|
+
|
|
470
|
+
if not pkg_path.exists():
|
|
471
|
+
return False, f"Package not found: {pkg_path}"
|
|
472
|
+
|
|
473
|
+
# 1. Unpack the package
|
|
474
|
+
staging = deploy_dir / ".ge_staging"
|
|
475
|
+
staging.mkdir(parents=True, exist_ok=True)
|
|
476
|
+
print(f"Unpacking {pkg_path.name}...")
|
|
477
|
+
meta = unpack_ge(pkg_path, staging)
|
|
478
|
+
|
|
479
|
+
# 2. Auto-detect manifest
|
|
480
|
+
print("Detecting package contents...")
|
|
481
|
+
manifest = detect_manifest(staging, meta)
|
|
482
|
+
manifest.host = host
|
|
483
|
+
manifest.port = port
|
|
484
|
+
print(f" App: {manifest.app_name} v{manifest.version}")
|
|
485
|
+
print(f" Backend: {manifest.backend}")
|
|
486
|
+
print(f" Components: {', '.join(manifest.components) or '(none)'}")
|
|
487
|
+
print(f" FFI exports: {len(manifest.ffi_exports)} functions")
|
|
488
|
+
print(f" Native libs: {len(manifest.native_libs)}")
|
|
489
|
+
|
|
490
|
+
# 3. Distribute components
|
|
491
|
+
print("Distributing components...")
|
|
492
|
+
deployed = distribute_components(staging, deploy_dir, manifest)
|
|
493
|
+
for name, path in deployed.items():
|
|
494
|
+
print(f" {name} -> {path}")
|
|
495
|
+
|
|
496
|
+
# 4. Run simulation test
|
|
497
|
+
print("\nRunning simulation test...")
|
|
498
|
+
sim_ok, sim_results = simulate_deployment(staging, deploy_dir, manifest)
|
|
499
|
+
for r in sim_results:
|
|
500
|
+
print(r)
|
|
501
|
+
if not sim_ok:
|
|
502
|
+
return False, "Simulation test FAILED — deployment aborted."
|
|
503
|
+
print("\nSimulation: ALL CHECKS PASSED")
|
|
504
|
+
|
|
505
|
+
if simulate_only:
|
|
506
|
+
return True, "Simulation complete (simulate-only mode)."
|
|
507
|
+
|
|
508
|
+
# 5. Generate runtime server (if web component)
|
|
509
|
+
if manifest.has_web:
|
|
510
|
+
print("\nGenerating runtime server...")
|
|
511
|
+
server_path = generate_runtime_server(manifest, deploy_dir)
|
|
512
|
+
print(f" Server: {server_path}")
|
|
513
|
+
print(f" Run: python {server_path.name} --port {port}")
|
|
514
|
+
|
|
515
|
+
# 6. Go live (for local target)
|
|
516
|
+
if target == "local" and manifest.has_web:
|
|
517
|
+
print(f"\nStarting server at http://{host}:{port}...")
|
|
518
|
+
server_path = (deploy_dir / "server.py").resolve()
|
|
519
|
+
try:
|
|
520
|
+
proc = subprocess.Popen(
|
|
521
|
+
[sys.executable, str(server_path), "--port", str(port), "--host", host],
|
|
522
|
+
cwd=str(deploy_dir.resolve()))
|
|
523
|
+
time.sleep(2)
|
|
524
|
+
# Verify server is running
|
|
525
|
+
import urllib.request
|
|
526
|
+
try:
|
|
527
|
+
urllib.request.urlopen(f"http://localhost:{port}/", timeout=5)
|
|
528
|
+
print(f"Server is LIVE at http://localhost:{port}/")
|
|
529
|
+
print(f" PID: {proc.pid}")
|
|
530
|
+
print(f" Stop with: kill {proc.pid}")
|
|
531
|
+
except Exception:
|
|
532
|
+
print("Server started but health check failed (may still be starting).")
|
|
533
|
+
return True, f"Server running at http://{host}:{port}/"
|
|
534
|
+
except Exception as e:
|
|
535
|
+
return False, f"Failed to start server: {e}"
|
|
536
|
+
|
|
537
|
+
# For VPS target, generate deployment script
|
|
538
|
+
if target == "vps":
|
|
539
|
+
script_path = deploy_dir / "deploy_vps.sh"
|
|
540
|
+
script = f"""#!/bin/bash
|
|
541
|
+
# GE VPS deployment script for {manifest.app_name} v{manifest.version}
|
|
542
|
+
# Auto-generated by GE deploy.
|
|
543
|
+
set -e
|
|
544
|
+
|
|
545
|
+
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
546
|
+
cd "$DEPLOY_DIR"
|
|
547
|
+
|
|
548
|
+
echo "GE VPS Deployment: {manifest.app_name} v{manifest.version}"
|
|
549
|
+
echo " Backend: {manifest.backend}"
|
|
550
|
+
echo " Components: {', '.join(manifest.components)}"
|
|
551
|
+
|
|
552
|
+
# Ensure Python is available
|
|
553
|
+
if ! command -v python3 &>/dev/null; then
|
|
554
|
+
echo "Error: python3 not found. Install Python 3.10+."
|
|
555
|
+
exit 1
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
# Run simulation test
|
|
559
|
+
echo "Running pre-flight simulation..."
|
|
560
|
+
python3 -c "
|
|
561
|
+
import json, sys
|
|
562
|
+
from pathlib import Path
|
|
563
|
+
manifest = json.loads(Path('manifest.json').read_text())
|
|
564
|
+
print(f' App: {{manifest[\"app_name\"]}} v{{manifest[\"version\"]}}')
|
|
565
|
+
print(f' Components: {{manifest[\"components\"]}}')
|
|
566
|
+
print(' Simulation: OK')
|
|
567
|
+
"
|
|
568
|
+
|
|
569
|
+
# Start the server (if web component)
|
|
570
|
+
if [ -f server.py ]; then
|
|
571
|
+
echo "Starting server on port {port}..."
|
|
572
|
+
python3 server.py --port {port} --host {host} &
|
|
573
|
+
SERVER_PID=$!
|
|
574
|
+
echo "Server PID: $SERVER_PID"
|
|
575
|
+
echo "Stop with: kill $SERVER_PID"
|
|
576
|
+
fi
|
|
577
|
+
"""
|
|
578
|
+
script_path.write_text(script, encoding="utf-8")
|
|
579
|
+
os.chmod(str(script_path), 0o755)
|
|
580
|
+
print(f"\nVPS deployment script: {script_path}")
|
|
581
|
+
print(f"Upload {deploy_dir} to your VPS and run: ./deploy_vps.sh")
|
|
582
|
+
|
|
583
|
+
# Clean up staging
|
|
584
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
585
|
+
|
|
586
|
+
return True, f"Deployment complete: {deploy_dir}"
|