inscript-lang 2.10.0__tar.gz → 2.11.0__tar.gz

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.
Files changed (35) hide show
  1. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/PKG-INFO +1 -1
  2. inscript_lang-2.11.0/export_pipeline.py +647 -0
  3. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript.py +26 -1
  4. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/SOURCES.txt +1 -0
  6. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/top_level.txt +1 -0
  7. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/pyproject.toml +1 -1
  8. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/setup.py +1 -1
  9. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/README.md +0 -0
  10. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/analyzer.py +0 -0
  11. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/ast_nodes.py +0 -0
  12. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/compiler.py +0 -0
  13. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/environment.py +0 -0
  14. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/errors.py +0 -0
  15. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/hot_reload.py +0 -0
  16. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_dap.py +0 -0
  17. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_fmt.py +0 -0
  18. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  19. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  20. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_lang.egg-info/requires.txt +0 -0
  21. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/inscript_test.py +0 -0
  22. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/interpreter.py +0 -0
  23. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/lexer.py +0 -0
  24. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/parser.py +0 -0
  25. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/pygame_backend.py +0 -0
  26. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/repl.py +0 -0
  27. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/scene_tree.py +0 -0
  28. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/setup.cfg +0 -0
  29. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib.py +0 -0
  30. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib_assets.py +0 -0
  31. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib_extended.py +0 -0
  32. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib_extended_2.py +0 -0
  33. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib_game.py +0 -0
  34. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/stdlib_values.py +0 -0
  35. {inscript_lang-2.10.0 → inscript_lang-2.11.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.10.0
3
+ Version: 2.11.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -0,0 +1,647 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ export_pipeline.py — InScript v2.11.0 Export Pipeline
4
+ ================================================================
5
+ `inscript build --target <target> [project_dir]`
6
+
7
+ Supported targets
8
+ ─────────────────
9
+ desktop → self-contained directory + launch script
10
+ (embeds Python + inscript runtime + all assets)
11
+ web → index.html + Pyodide WASM bundle + assets
12
+ android → APK via BeeWare Briefcase (requires: pip install briefcase)
13
+ ibc → ahead-of-time bytecode compilation (pre-existing, improved)
14
+
15
+ `inscript new <name>` → scaffold a standard project layout:
16
+ <name>/
17
+ inscript.toml project manifest
18
+ src/
19
+ main.ins entry-point scene
20
+ assets/
21
+ sprites/ sfx/ fonts/ maps/
22
+ scenes/
23
+ main.inscene root scene file
24
+ build/ (gitignored output dir)
25
+
26
+ Build manifest
27
+ ──────────────
28
+ `build/build.toml` is written after every successful build, recording:
29
+ target, version, entry_point, asset_hashes, timestamp.
30
+ """
31
+
32
+ from __future__ import annotations
33
+ import os, sys, shutil, json, hashlib, datetime, textwrap, zipfile
34
+ from typing import Any, Dict, List, Optional
35
+
36
+ # ─────────────────────────────────────────────────────────────────────────────
37
+ # Constants
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ BUILD_DIR = "build"
41
+ MANIFEST_FILE = "inscript.toml"
42
+ ENTRY_DEFAULT = os.path.join("src", "main.ins")
43
+ BUILD_MANIFEST = "build.toml"
44
+
45
+ VALID_TARGETS = ("desktop", "web", "android", "ibc")
46
+
47
+ # Pyodide CDN base used in web builds
48
+ PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.27.0/full/"
49
+
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ # BuildResult
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ class BuildResult:
55
+ def __init__(self, target: str, success: bool, output_dir: str,
56
+ artifacts: List[str], errors: List[str], elapsed_ms: float):
57
+ self.target = target
58
+ self.success = success
59
+ self.output_dir = output_dir
60
+ self.artifacts = artifacts # paths of produced files
61
+ self.errors = errors
62
+ self.elapsed_ms = elapsed_ms
63
+
64
+ def __repr__(self):
65
+ status = "OK" if self.success else "ERR"
66
+ return (f"<BuildResult {self.target} {status} "
67
+ f"artifacts={len(self.artifacts)} {self.elapsed_ms:.0f}ms>")
68
+
69
+
70
+ # ─────────────────────────────────────────────────────────────────────────────
71
+ # Project layout helpers
72
+ # ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ def _read_manifest(project_dir: str) -> dict:
75
+ path = os.path.join(project_dir, MANIFEST_FILE)
76
+ if not os.path.isfile(path):
77
+ return {}
78
+ try:
79
+ from inscript import _parse_toml_simple
80
+ with open(path, encoding="utf-8") as f:
81
+ return _parse_toml_simple(f.read())
82
+ except Exception:
83
+ return {}
84
+
85
+
86
+ def _entry_point(project_dir: str, manifest: dict) -> str:
87
+ """Resolve the .ins entry-point from manifest or default."""
88
+ ep = manifest.get("package", {}).get("entry", ENTRY_DEFAULT)
89
+ return os.path.join(project_dir, ep)
90
+
91
+
92
+ def _collect_assets(project_dir: str) -> List[str]:
93
+ """Walk assets/ and scenes/ and return all file paths (relative to project_dir)."""
94
+ assets = []
95
+ for sub in ("assets", "scenes"):
96
+ base = os.path.join(project_dir, sub)
97
+ if not os.path.isdir(base):
98
+ continue
99
+ for root, _, files in os.walk(base):
100
+ for fn in files:
101
+ full = os.path.join(root, fn)
102
+ assets.append(os.path.relpath(full, project_dir))
103
+ return sorted(assets)
104
+
105
+
106
+ def _collect_sources(project_dir: str) -> List[str]:
107
+ """Return all .ins files (relative to project_dir)."""
108
+ sources = []
109
+ for root, _, files in os.walk(project_dir):
110
+ for fn in files:
111
+ if fn.endswith(".ins"):
112
+ full = os.path.join(root, fn)
113
+ rel = os.path.relpath(full, project_dir)
114
+ if not rel.startswith("build" + os.sep):
115
+ sources.append(rel)
116
+ return sorted(sources)
117
+
118
+
119
+ def _sha256_file(path: str) -> str:
120
+ if not os.path.isfile(path):
121
+ return ""
122
+ h = hashlib.sha256()
123
+ with open(path, "rb") as f:
124
+ for chunk in iter(lambda: f.read(65536), b""):
125
+ h.update(chunk)
126
+ return h.hexdigest()
127
+
128
+
129
+ def _write_build_manifest(output_dir: str, meta: dict) -> str:
130
+ """Write build/build.toml and return its path."""
131
+ os.makedirs(output_dir, exist_ok=True)
132
+ path = os.path.join(output_dir, BUILD_MANIFEST)
133
+ lines = [
134
+ "# InScript Build Manifest — auto-generated\n",
135
+ f'generated = "{datetime.datetime.utcnow().isoformat()}Z"\n',
136
+ f'target = "{meta.get("target", "unknown")}"\n',
137
+ f'version = "{meta.get("version", "0.0.0")}"\n',
138
+ f'entry = "{meta.get("entry", "")}"\n',
139
+ f'inscript = "{meta.get("inscript_version", "")}"\n\n',
140
+ "[artifacts]\n",
141
+ ]
142
+ for k, v in meta.get("artifacts", {}).items():
143
+ lines.append(f'{k} = "{v}"\n')
144
+ lines.append("\n[asset_hashes]\n")
145
+ for k, v in meta.get("asset_hashes", {}).items():
146
+ safe_k = k.replace("\\", "/").replace('"', "")
147
+ lines.append(f'"{safe_k}" = "{v}"\n')
148
+ with open(path, "w", encoding="utf-8") as f:
149
+ f.writelines(lines)
150
+ return path
151
+
152
+
153
+ # ─────────────────────────────────────────────────────────────────────────────
154
+ # Target: desktop
155
+ # ─────────────────────────────────────────────────────────────────────────────
156
+
157
+ def build_desktop(project_dir: str = ".") -> BuildResult:
158
+ """
159
+ Build a self-contained desktop distribution.
160
+
161
+ Output layout:
162
+ build/desktop/
163
+ run.py — launcher that invokes inscript on main.ins
164
+ run.sh — Unix launcher
165
+ run.bat — Windows launcher
166
+ inscript_runtime/ — copy of the InScript runtime (.py files)
167
+ assets/ — copy of project assets/
168
+ scenes/ — copy of project scenes/
169
+ src/ — copy of .ins source files
170
+ build.toml — build manifest
171
+ """
172
+ import time
173
+ t0 = time.perf_counter()
174
+ errors: List[str] = []
175
+ artifacts: List[str] = []
176
+
177
+ manifest = _read_manifest(project_dir)
178
+ pkg = manifest.get("package", {})
179
+ version = pkg.get("version", "0.0.0")
180
+ name = pkg.get("name", os.path.basename(os.path.abspath(project_dir)))
181
+ entry_rel = pkg.get("entry", ENTRY_DEFAULT)
182
+ entry_abs = os.path.join(project_dir, entry_rel)
183
+
184
+ out_dir = os.path.join(project_dir, BUILD_DIR, "desktop")
185
+ os.makedirs(out_dir, exist_ok=True)
186
+
187
+ # ── Copy runtime .py files ───────────────────────────────────────────────
188
+ runtime_src = os.path.dirname(os.path.abspath(__file__))
189
+ runtime_dst = os.path.join(out_dir, "inscript_runtime")
190
+ os.makedirs(runtime_dst, exist_ok=True)
191
+ runtime_modules = [
192
+ "inscript.py", "lexer.py", "parser.py", "interpreter.py",
193
+ "compiler.py", "vm.py", "analyzer.py", "errors.py",
194
+ "environment.py", "ast_nodes.py",
195
+ "stdlib.py", "stdlib_extended.py", "stdlib_extended_2.py",
196
+ "stdlib_game.py", "stdlib_values.py", "stdlib_assets.py",
197
+ "scene_tree.py", "hot_reload.py", "pygame_backend.py",
198
+ ]
199
+ for mod in runtime_modules:
200
+ src = os.path.join(runtime_src, mod)
201
+ if os.path.isfile(src):
202
+ shutil.copy2(src, os.path.join(runtime_dst, mod))
203
+
204
+ # ── Copy source + assets ─────────────────────────────────────────────────
205
+ for sub in ("src", "assets", "scenes"):
206
+ src = os.path.join(project_dir, sub)
207
+ dst = os.path.join(out_dir, sub)
208
+ if os.path.isdir(src):
209
+ if os.path.exists(dst):
210
+ shutil.rmtree(dst)
211
+ shutil.copytree(src, dst)
212
+
213
+ # ── Write launchers ──────────────────────────────────────────────────────
214
+ run_py = os.path.join(out_dir, "run.py")
215
+ with open(run_py, "w") as f:
216
+ f.write(textwrap.dedent(f"""\
217
+ #!/usr/bin/env python3
218
+ # InScript Desktop Launcher — {name} v{version}
219
+ import sys, os
220
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "inscript_runtime"))
221
+ import inscript
222
+ sys.exit(inscript.main(["{entry_rel}"]))
223
+ """))
224
+ artifacts.append(run_py)
225
+
226
+ run_sh = os.path.join(out_dir, "run.sh")
227
+ with open(run_sh, "w") as f:
228
+ f.write(f"#!/bin/sh\ncd \"$(dirname \"$0\")\"\npython3 run.py \"$@\"\n")
229
+ try:
230
+ os.chmod(run_sh, 0o755)
231
+ except Exception:
232
+ pass
233
+ artifacts.append(run_sh)
234
+
235
+ run_bat = os.path.join(out_dir, "run.bat")
236
+ with open(run_bat, "w") as f:
237
+ f.write(f"@echo off\ncd /d \"%~dp0\"\npython run.py %*\n")
238
+ artifacts.append(run_bat)
239
+
240
+ # ── Compute asset hashes ─────────────────────────────────────────────────
241
+ asset_hashes = {}
242
+ for rel in _collect_assets(project_dir):
243
+ full = os.path.join(project_dir, rel)
244
+ asset_hashes[rel] = _sha256_file(full)
245
+
246
+ # ── Write build manifest ─────────────────────────────────────────────────
247
+ from inscript import VERSION as _IV
248
+ bm = _write_build_manifest(out_dir, {
249
+ "target": "desktop", "version": version, "entry": entry_rel,
250
+ "inscript_version": _IV,
251
+ "artifacts": {"run_py": "run.py", "run_sh": "run.sh", "run_bat": "run.bat"},
252
+ "asset_hashes": asset_hashes,
253
+ })
254
+ artifacts.append(bm)
255
+
256
+ elapsed = (time.perf_counter() - t0) * 1000
257
+ print(f"[build:desktop] ✅ {name} v{version} → {out_dir} ({elapsed:.0f}ms)")
258
+ return BuildResult("desktop", True, out_dir, artifacts, errors, elapsed)
259
+
260
+
261
+ # ─────────────────────────────────────────────────────────────────────────────
262
+ # Target: web
263
+ # ─────────────────────────────────────────────────────────────────────────────
264
+
265
+ _WEB_TEMPLATE = """\
266
+ <!DOCTYPE html>
267
+ <html lang="en">
268
+ <head>
269
+ <meta charset="UTF-8">
270
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
271
+ <title>{name}</title>
272
+ <style>
273
+ body {{ margin:0; background:#111; display:flex; flex-direction:column;
274
+ align-items:center; justify-content:center; height:100vh;
275
+ font-family:monospace; color:#ccc; }}
276
+ #canvas {{ border:2px solid #333; }}
277
+ #status {{ margin-top:8px; font-size:12px; }}
278
+ </style>
279
+ </head>
280
+ <body>
281
+ <canvas id="canvas" width="{width}" height="{height}"></canvas>
282
+ <div id="status">Loading InScript runtime…</div>
283
+ <script src="{pyodide_cdn}pyodide.js"></script>
284
+ <script type="module">
285
+ const status = document.getElementById("status");
286
+
287
+ async function main() {{
288
+ status.textContent = "Loading Pyodide…";
289
+ const pyodide = await loadPyodide({{ indexURL: "{pyodide_cdn}" }});
290
+ status.textContent = "Loading InScript runtime…";
291
+
292
+ // Fetch and exec inscript runtime
293
+ const r = await fetch("inscript_runtime/inscript.py");
294
+ const src = await r.text();
295
+ pyodide.runPython(src);
296
+
297
+ // Fetch and run entry point
298
+ const e = await fetch("{entry}");
299
+ const game = await e.text();
300
+ status.textContent = "Running {name}…";
301
+ pyodide.runPython(game);
302
+ }}
303
+
304
+ main().catch(err => {{
305
+ status.textContent = "Error: " + err.message;
306
+ console.error(err);
307
+ }});
308
+ </script>
309
+ </body>
310
+ </html>
311
+ """
312
+
313
+ def build_web(project_dir: str = ".") -> BuildResult:
314
+ """
315
+ Build a Pyodide-based web export.
316
+
317
+ Output layout:
318
+ build/web/
319
+ index.html — shell page with Pyodide loader
320
+ inscript_runtime/ — minimal runtime .py files
321
+ assets/ — copied assets
322
+ src/ — .ins source files
323
+ build.toml
324
+ """
325
+ import time
326
+ t0 = time.perf_counter()
327
+ errors: List[str] = []
328
+ artifacts: List[str] = []
329
+
330
+ manifest = _read_manifest(project_dir)
331
+ pkg = manifest.get("package", {})
332
+ version = pkg.get("version", "0.0.0")
333
+ name = pkg.get("name", os.path.basename(os.path.abspath(project_dir)))
334
+ entry = pkg.get("entry", ENTRY_DEFAULT)
335
+ width = int(pkg.get("window_width", 800))
336
+ height = int(pkg.get("window_height", 600))
337
+
338
+ out_dir = os.path.join(project_dir, BUILD_DIR, "web")
339
+ os.makedirs(out_dir, exist_ok=True)
340
+
341
+ # Write index.html
342
+ html_path = os.path.join(out_dir, "index.html")
343
+ with open(html_path, "w", encoding="utf-8") as f:
344
+ f.write(_WEB_TEMPLATE.format(
345
+ name=name, width=width, height=height,
346
+ pyodide_cdn=PYODIDE_CDN, entry=entry,
347
+ ))
348
+ artifacts.append(html_path)
349
+
350
+ # Copy minimal runtime
351
+ runtime_src = os.path.dirname(os.path.abspath(__file__))
352
+ runtime_dst = os.path.join(out_dir, "inscript_runtime")
353
+ os.makedirs(runtime_dst, exist_ok=True)
354
+ web_runtime_modules = [
355
+ "inscript.py", "lexer.py", "parser.py", "interpreter.py",
356
+ "compiler.py", "vm.py", "analyzer.py", "errors.py",
357
+ "environment.py", "ast_nodes.py",
358
+ "stdlib.py", "stdlib_extended.py", "stdlib_extended_2.py",
359
+ "stdlib_game.py", "stdlib_values.py", "stdlib_assets.py",
360
+ "scene_tree.py", "hot_reload.py",
361
+ ]
362
+ for mod in web_runtime_modules:
363
+ src = os.path.join(runtime_src, mod)
364
+ if os.path.isfile(src):
365
+ shutil.copy2(src, os.path.join(runtime_dst, mod))
366
+
367
+ # Copy assets and source
368
+ for sub in ("assets", "src", "scenes"):
369
+ s = os.path.join(project_dir, sub)
370
+ d = os.path.join(out_dir, sub)
371
+ if os.path.isdir(s):
372
+ if os.path.exists(d):
373
+ shutil.rmtree(d)
374
+ shutil.copytree(s, d)
375
+
376
+ # Compute asset hashes
377
+ asset_hashes = {r: _sha256_file(os.path.join(project_dir, r))
378
+ for r in _collect_assets(project_dir)}
379
+
380
+ from inscript import VERSION as _IV
381
+ bm = _write_build_manifest(out_dir, {
382
+ "target": "web", "version": version, "entry": entry,
383
+ "inscript_version": _IV, "pyodide_cdn": PYODIDE_CDN,
384
+ "artifacts": {"index": "index.html"},
385
+ "asset_hashes": asset_hashes,
386
+ })
387
+ artifacts.append(bm)
388
+
389
+ elapsed = (time.perf_counter() - t0) * 1000
390
+ print(f"[build:web] ✅ {name} v{version} → {out_dir} ({elapsed:.0f}ms)")
391
+ print(f"[build:web] Serve with: python -m http.server 8080 --directory {out_dir}")
392
+ return BuildResult("web", True, out_dir, artifacts, errors, elapsed)
393
+
394
+
395
+ # ─────────────────────────────────────────────────────────────────────────────
396
+ # Target: android
397
+ # ─────────────────────────────────────────────────────────────────────────────
398
+
399
+ def build_android(project_dir: str = ".") -> BuildResult:
400
+ """
401
+ Build an Android APK via BeeWare Briefcase.
402
+ Requires: pip install briefcase
403
+
404
+ Steps:
405
+ 1. Generate a Briefcase project layout in build/android/
406
+ 2. Run `briefcase create android` + `briefcase build android`
407
+ 3. APK lands in build/android/android/<name>-<ver>/
408
+ """
409
+ import time, subprocess
410
+ t0 = time.perf_counter()
411
+ errors: List[str] = []
412
+ artifacts: List[str] = []
413
+
414
+ # Check briefcase is available
415
+ briefcase_ok = shutil.which("briefcase") is not None
416
+ if not briefcase_ok:
417
+ try:
418
+ subprocess.run([sys.executable, "-m", "briefcase", "--version"],
419
+ check=True, capture_output=True)
420
+ briefcase_ok = True
421
+ except Exception:
422
+ pass
423
+
424
+ manifest = _read_manifest(project_dir)
425
+ pkg = manifest.get("package", {})
426
+ version = pkg.get("version", "0.0.0")
427
+ name = pkg.get("name", "mygame")
428
+ entry = pkg.get("entry", ENTRY_DEFAULT)
429
+ bundle = pkg.get("bundle", f"dev.inscript.{name}")
430
+
431
+ out_dir = os.path.join(project_dir, BUILD_DIR, "android")
432
+ os.makedirs(out_dir, exist_ok=True)
433
+
434
+ # Write pyproject.toml for Briefcase
435
+ bp_path = os.path.join(out_dir, "pyproject.toml")
436
+ with open(bp_path, "w", encoding="utf-8") as f:
437
+ f.write(textwrap.dedent(f"""\
438
+ [tool.briefcase]
439
+ project_name = "{name}"
440
+ bundle = "{bundle}"
441
+ version = "{version}"
442
+ description = "Built with InScript"
443
+ license = "MIT"
444
+ url = "https://github.com/authorss81/inscript"
445
+
446
+ [tool.briefcase.app.{name}]
447
+ formal_name = "{name}"
448
+ description = "Built with InScript"
449
+ sources = ["."]
450
+ requires = ["inscript-lang"]
451
+
452
+ [tool.briefcase.app.{name}.android]
453
+ requires = ["toga-android"]
454
+ """))
455
+ artifacts.append(bp_path)
456
+
457
+ # Copy source into build/android/
458
+ for sub in ("src", "assets", "scenes"):
459
+ s = os.path.join(project_dir, sub)
460
+ d = os.path.join(out_dir, sub)
461
+ if os.path.isdir(s):
462
+ if os.path.exists(d): shutil.rmtree(d)
463
+ shutil.copytree(s, d)
464
+
465
+ # Main app entry
466
+ app_py = os.path.join(out_dir, "app.py")
467
+ with open(app_py, "w") as f:
468
+ f.write(textwrap.dedent(f"""\
469
+ # InScript Android entry — generated by inscript build --target android
470
+ import sys, os
471
+ try:
472
+ import inscript
473
+ inscript.main(["{entry}"])
474
+ except Exception as e:
475
+ print(f"InScript launch error: {{e}}", file=sys.stderr)
476
+ """))
477
+ artifacts.append(app_py)
478
+
479
+ from inscript import VERSION as _IV
480
+ bm = _write_build_manifest(out_dir, {
481
+ "target": "android", "version": version, "entry": entry,
482
+ "inscript_version": _IV, "bundle": bundle,
483
+ "artifacts": {"pyproject": "pyproject.toml", "app": "app.py"},
484
+ "asset_hashes": {},
485
+ })
486
+ artifacts.append(bm)
487
+
488
+ if not briefcase_ok:
489
+ msg = ("BeeWare Briefcase not installed. "
490
+ "Install with: pip install briefcase "
491
+ "Then run from build/android/: briefcase create android && briefcase build android")
492
+ print(f"[build:android] ⚠ {msg}")
493
+ errors.append(msg)
494
+ elapsed = (time.perf_counter() - t0) * 1000
495
+ return BuildResult("android", False, out_dir, artifacts, errors, elapsed)
496
+
497
+ # Run briefcase
498
+ try:
499
+ subprocess.run(
500
+ [sys.executable, "-m", "briefcase", "create", "android"],
501
+ cwd=out_dir, check=True
502
+ )
503
+ subprocess.run(
504
+ [sys.executable, "-m", "briefcase", "build", "android"],
505
+ cwd=out_dir, check=True
506
+ )
507
+ print(f"[build:android] ✅ APK built → {out_dir}")
508
+ except subprocess.CalledProcessError as e:
509
+ errors.append(f"briefcase failed: {e}")
510
+ print(f"[build:android] ❌ briefcase error: {e}", file=sys.stderr)
511
+
512
+ elapsed = (time.perf_counter() - t0) * 1000
513
+ return BuildResult("android", len(errors) == 0, out_dir, artifacts, errors, elapsed)
514
+
515
+
516
+ # ─────────────────────────────────────────────────────────────────────────────
517
+ # Project scaffold: `inscript new <name>`
518
+ # ─────────────────────────────────────────────────────────────────────────────
519
+
520
+ _MAIN_INS_TEMPLATE = """\
521
+ # {name} — main scene
522
+ # Created by InScript v{inscript_version}
523
+
524
+ import "asset" as asset
525
+
526
+ node MainScene {{
527
+ let title: str = "{name}"
528
+
529
+ _ready() {{
530
+ print("Welcome to " + title)
531
+ }}
532
+
533
+ _update(dt) {{
534
+ # game logic here
535
+ }}
536
+
537
+ _draw() {{
538
+ # rendering here
539
+ }}
540
+ }}
541
+ """
542
+
543
+ _INSCRIPT_TOML_TEMPLATE = """\
544
+ [package]
545
+ name = "{name}"
546
+ version = "0.1.0"
547
+ description = "A game built with InScript"
548
+ entry = "src/main.ins"
549
+ stdlib = ">={inscript_version}"
550
+
551
+ [dependencies]
552
+ # add packages here: math-utils = "^1.0"
553
+
554
+ [build]
555
+ window_width = 800
556
+ window_height = 600
557
+ """
558
+
559
+ def scaffold_project(name: str, dest_dir: str = ".") -> str:
560
+ """
561
+ Create a standard InScript project layout.
562
+ Returns the project root path.
563
+ """
564
+ from inscript import VERSION as _IV
565
+
566
+ root = os.path.join(dest_dir, name)
567
+ if os.path.exists(root):
568
+ raise FileExistsError(f"Directory already exists: {root}")
569
+
570
+ # Create directory structure
571
+ for sub in ("src", "assets/sprites", "assets/sfx",
572
+ "assets/fonts", "assets/maps", "scenes", "build"):
573
+ os.makedirs(os.path.join(root, sub), exist_ok=True)
574
+
575
+ # inscript.toml
576
+ with open(os.path.join(root, MANIFEST_FILE), "w") as f:
577
+ f.write(_INSCRIPT_TOML_TEMPLATE.format(name=name, inscript_version=_IV))
578
+
579
+ # src/main.ins
580
+ with open(os.path.join(root, "src", "main.ins"), "w") as f:
581
+ f.write(_MAIN_INS_TEMPLATE.format(name=name, inscript_version=_IV))
582
+
583
+ # scenes/main.inscene
584
+ with open(os.path.join(root, "scenes", "main.inscene"), "w") as f:
585
+ f.write(f'# InScript Scene File\n\n[scene]\nname = "main"\nroot = "MainScene"\n')
586
+
587
+ # .gitignore
588
+ with open(os.path.join(root, ".gitignore"), "w") as f:
589
+ f.write("build/\n*.ibc\n__pycache__/\n*.pyc\n.inscript/\n")
590
+
591
+ # README.md
592
+ with open(os.path.join(root, "README.md"), "w") as f:
593
+ f.write(textwrap.dedent(f"""\
594
+ # {name}
595
+
596
+ Built with [InScript](https://github.com/authorss81/inscript) v{_IV}.
597
+
598
+ ## Run
599
+ ```
600
+ inscript src/main.ins
601
+ ```
602
+
603
+ ## Build
604
+ ```
605
+ inscript build --target desktop
606
+ inscript build --target web
607
+ ```
608
+ """))
609
+
610
+ print(f"[inscript new] ✅ Created project '{name}' at {root}")
611
+ print(f"[inscript new] Run: cd {name} && inscript src/main.ins")
612
+ return root
613
+
614
+
615
+ # ─────────────────────────────────────────────────────────────────────────────
616
+ # Top-level dispatcher called from inscript.py
617
+ # ─────────────────────────────────────────────────────────────────────────────
618
+
619
+ def run_build(target: str, project_dir: str = ".") -> int:
620
+ """
621
+ Entry point called by --build flag in inscript.py.
622
+ Returns exit code (0 = success).
623
+ """
624
+ target = target.lower()
625
+ if target not in VALID_TARGETS:
626
+ print(f"[build] Unknown target '{target}'. Valid: {', '.join(VALID_TARGETS)}",
627
+ file=sys.stderr)
628
+ return 1
629
+
630
+ if target == "desktop":
631
+ r = build_desktop(project_dir)
632
+ elif target == "web":
633
+ r = build_web(project_dir)
634
+ elif target == "android":
635
+ r = build_android(project_dir)
636
+ elif target == "ibc":
637
+ # Delegate to existing compile pipeline
638
+ from inscript import cmd_compile
639
+ entry = _entry_point(project_dir, _read_manifest(project_dir))
640
+ return cmd_compile(entry) if os.path.isfile(entry) else 1
641
+ else:
642
+ return 1
643
+
644
+ if r.errors:
645
+ for e in r.errors:
646
+ print(f"[build] {e}", file=sys.stderr)
647
+ return 0 if r.success else 1
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.10.0"
27
+ VERSION = "2.11.0"
28
28
 
29
29
  MANIFEST_FILENAME = "inscript.toml"
30
30
  LOCK_FILENAME = "inscript.lock"
@@ -2721,6 +2721,13 @@ Examples:
2721
2721
  help="v2.8.0: Generate assets.toml manifest for DIR (default: current dir)")
2722
2722
  parser.add_argument("--bundle", metavar="DIR", nargs="?", const=".",
2723
2723
  help="v2.8.0: Copy all referenced assets into <DIR>/dist/assets")
2724
+ # v2.11.0: export pipeline
2725
+ parser.add_argument("--build", metavar="TARGET", nargs="?", const="desktop",
2726
+ help="v2.11.0: Build for target: desktop, web, android, ibc (default: desktop)")
2727
+ parser.add_argument("--project-dir", metavar="DIR", default=".",
2728
+ help="v2.11.0: Project root for --build (default: current dir)")
2729
+ parser.add_argument("--new", metavar="NAME",
2730
+ help="v2.11.0: Scaffold a new InScript project: inscript --new mygame")
2724
2731
  parser.add_argument("--lsp", action="store_true",
2725
2732
  help="Start the Language Server (requires: pip install pygls)")
2726
2733
  parser.add_argument("--game", action="store_true",
@@ -2987,6 +2994,24 @@ Examples:
2987
2994
  print(f"[bundle] {_be}", file=sys.stderr); return 1
2988
2995
  return 0
2989
2996
 
2997
+ # ── v2.11.0 export pipeline ────────────────────────────────────────────
2998
+ if getattr(args, 'new', None) is not None:
2999
+ try:
3000
+ from export_pipeline import scaffold_project
3001
+ scaffold_project(args.new, dest_dir=getattr(args, 'project_dir', '.'))
3002
+ except FileExistsError as _fe:
3003
+ print(f"[inscript new] {_fe}", file=sys.stderr); return 1
3004
+ except Exception as _ne:
3005
+ print(f"[inscript new] {_ne}", file=sys.stderr); return 1
3006
+ return 0
3007
+
3008
+ if getattr(args, 'build', None) is not None:
3009
+ try:
3010
+ from export_pipeline import run_build
3011
+ return run_build(args.build, getattr(args, 'project_dir', '.'))
3012
+ except Exception as _bld:
3013
+ print(f"[build] {_bld}", file=sys.stderr); return 1
3014
+
2990
3015
  # v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
2991
3016
  if getattr(args, 'no_typecheck', False):
2992
3017
  print(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.10.0
3
+ Version: 2.11.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -4,6 +4,7 @@ ast_nodes.py
4
4
  compiler.py
5
5
  environment.py
6
6
  errors.py
7
+ export_pipeline.py
7
8
  hot_reload.py
8
9
  inscript.py
9
10
  inscript_dap.py
@@ -3,6 +3,7 @@ ast_nodes
3
3
  compiler
4
4
  environment
5
5
  errors
6
+ export_pipeline
6
7
  hot_reload
7
8
  inscript
8
9
  inscript_dap
@@ -61,7 +61,7 @@ py-modules = [
61
61
  # ── Backends ──────────────────────────────────────────────────────────────
62
62
  "pygame_backend",
63
63
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────────
64
- "scene_tree", "hot_reload"
64
+ "scene_tree", "hot_reload", "export_pipeline"
65
65
  ]
66
66
 
67
67
  [tool.setuptools.package-data]
@@ -55,7 +55,7 @@ setup(
55
55
  # ── Backends ──────────────────────────────────────────────────────────
56
56
  "pygame_backend",
57
57
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────
58
- "scene_tree", "hot_reload",
58
+ "scene_tree", "hot_reload", "export_pipeline",
59
59
  ],
60
60
  package_data = {"": ["examples/*.ins", "lsp/*.py", "*.md"]},
61
61
  install_requires = [],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes