ghostbt 0.1.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.
ghostbt/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
ghostbt/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ import sys
2
+ from .cli import main
3
+
4
+ sys.exit(main())
ghostbt/build.py ADDED
@@ -0,0 +1,41 @@
1
+ import pathlib
2
+ import subprocess
3
+ import sys
4
+
5
+ from .config import load_manifest, detect_target
6
+ from .esp_idf import idf_command
7
+
8
+
9
+ def build_app(
10
+ app_dir: str = ".",
11
+ target: str = None,
12
+ skip_set_target: bool = False,
13
+ ) -> pathlib.Path:
14
+ app_path = pathlib.Path(app_dir).resolve()
15
+ if not (app_path / "CMakeLists.txt").exists():
16
+ print(f"not an app project: {app_path}", file=sys.stderr)
17
+ raise SystemExit(2)
18
+
19
+ if target is None:
20
+ try:
21
+ manifest = load_manifest(app_path)
22
+ target = detect_target(manifest)
23
+ except (FileNotFoundError, ValueError):
24
+ target = "esp32s3"
25
+
26
+ if not skip_set_target:
27
+ cmd, env = idf_command("set-target", target)
28
+ subprocess.check_call(cmd, cwd=app_path, env=env)
29
+ cmd, env = idf_command("build")
30
+ subprocess.check_call(cmd, cwd=app_path, env=env)
31
+
32
+ try:
33
+ manifest = load_manifest(app_path)
34
+ so_path = app_path / "build" / manifest["entry"]
35
+ if so_path.exists():
36
+ print(so_path)
37
+ return so_path
38
+ except (FileNotFoundError, ValueError):
39
+ pass
40
+
41
+ return app_path / "build"
ghostbt/cli.py ADDED
@@ -0,0 +1,171 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from . import __version__
5
+
6
+
7
+ def main(argv=None) -> int:
8
+ parser = argparse.ArgumentParser(
9
+ prog="gbt",
10
+ description="Ghost Build Tool - build and package GhostESP native SD apps",
11
+ )
12
+ parser.add_argument("--version", action="version", version=f"gbt {__version__}")
13
+ parser.add_argument("-v", "--verbose", action="store_true")
14
+
15
+ sub = parser.add_subparsers(dest="command")
16
+
17
+ p_create = sub.add_parser("create", help="Scaffold a new app from template")
18
+ p_create.add_argument("app_id", help="Safe app id, e.g. my_tool")
19
+ p_create.add_argument("--name", help="Display name (default: title-cased app_id)")
20
+ p_create.add_argument("--template", default="basic_app", help="Template name under plugins/templates/")
21
+ p_create.add_argument("--out", default=".", help="Output parent directory (default: current dir)")
22
+
23
+ p_build = sub.add_parser("build", help="Build an app project")
24
+ p_build.add_argument("app_dir", nargs="?", default=".", help="App project directory (default: .)")
25
+ p_build.add_argument("--target", default=None, help="ESP-IDF target (default: from manifest or esp32s3)")
26
+ p_build.add_argument("--skip-set-target", action="store_true", help="Skip idf.py set-target step")
27
+
28
+ p_package = sub.add_parser("package", help="Package a built app")
29
+ p_package.add_argument("app_dir", nargs="?", default=".", help="App project directory (default: .)")
30
+ p_package.add_argument("--out", default=None, help="Output dist directory")
31
+ p_package.add_argument("--gapp", action="store_true", help="Also create a .gapp archive")
32
+
33
+ p_dist = sub.add_parser("dist", help="Build and package in one step")
34
+ p_dist.add_argument("app_dir", nargs="?", default=".", help="App project directory (default: .)")
35
+ p_dist.add_argument("--target", default=None, help="ESP-IDF target (default: from manifest or esp32s3)")
36
+ p_dist.add_argument("--out", default=None, help="Output dist directory")
37
+ p_dist.add_argument("--gapp", action="store_true", help="Also create a .gapp archive")
38
+
39
+ p_setup = sub.add_parser("setup", help="Install or configure ESP-IDF toolchain")
40
+ p_setup.add_argument("--target", nargs="+", default=["esp32s3"], help="ESP-IDF targets to install (default: esp32s3)")
41
+ p_setup.add_argument("--idf-version", default=None, help="ESP-IDF version (default: v6.0.1)")
42
+ p_setup.add_argument("--install-dir", default=None, help="Custom install directory (default: ~/.ghostbt/esp-idf)")
43
+
44
+ p_boards = sub.add_parser("boards", help="List available firmware board configs")
45
+
46
+ p_fw = sub.add_parser("firmware", help="Build GhostESP firmware for a board")
47
+ p_fw.add_argument("board", help="Board config name (e.g. cardputer, ghostboard). Use 'gbt boards' to list.")
48
+ p_fw.add_argument("--repo", default=None, help="Path to GhostESP repo (default: auto-detect)")
49
+ p_fw.add_argument("--skip-set-target", action="store_true", help="Skip idf.py set-target step")
50
+
51
+ p_flash = sub.add_parser("flash", help="Flash firmware or app to device")
52
+ p_flash.add_argument("target", nargs="?", default="firmware", choices=["firmware", "app"],
53
+ help="What to flash: firmware (default) or app")
54
+ p_flash.add_argument("--port", "-p", default=None, help="Serial port (auto-detected if omitted)")
55
+ p_flash.add_argument("--baud", "-b", type=int, default=None, help="Baud rate (default: 460800)")
56
+ p_flash.add_argument("--board", default=None, help="Board config (builds firmware if needed)")
57
+ p_flash.add_argument("--app-dir", default=None, help="App directory (for 'app' target)")
58
+ p_flash.add_argument("--build-dir", default=None, help="Custom build directory")
59
+ p_flash.add_argument("--verify", action="store_true", help="Verify after writing")
60
+ p_flash.add_argument("--erase", action="store_true", help="Erase entire flash before writing")
61
+ p_flash.add_argument("--monitor", "-m", action="store_true", help="Open serial monitor after flashing")
62
+
63
+ p_mon = sub.add_parser("monitor", help="Open serial monitor")
64
+ p_mon.add_argument("--port", "-p", default=None, help="Serial port (auto-detected if omitted)")
65
+ p_mon.add_argument("--baud", "-b", type=int, default=115200, help="Baud rate (default: 115200)")
66
+
67
+ p_ports = sub.add_parser("ports", help="List available serial ports")
68
+
69
+ args = parser.parse_args(argv)
70
+
71
+ if not args.command:
72
+ parser.print_help()
73
+ return 0
74
+
75
+ if args.command == "create":
76
+ from .create import create_app
77
+ create_app(
78
+ app_id=args.app_id,
79
+ name=args.name,
80
+ template=args.template,
81
+ out_dir=args.out,
82
+ )
83
+ elif args.command == "build":
84
+ from .build import build_app
85
+ build_app(
86
+ app_dir=args.app_dir,
87
+ target=args.target,
88
+ skip_set_target=args.skip_set_target,
89
+ )
90
+ elif args.command == "package":
91
+ from .package import package_app
92
+ package_app(
93
+ app_dir=args.app_dir,
94
+ out=args.out,
95
+ make_gapp=args.gapp,
96
+ )
97
+ elif args.command == "dist":
98
+ from .dist import dist_app
99
+ dist_app(
100
+ app_dir=args.app_dir,
101
+ target=args.target,
102
+ out=args.out,
103
+ make_gapp=args.gapp,
104
+ )
105
+ elif args.command == "setup":
106
+ from .esp_idf import setup_idf
107
+ setup_idf(
108
+ targets=args.target,
109
+ idf_version=args.idf_version,
110
+ install_dir=args.install_dir,
111
+ )
112
+ elif args.command == "boards":
113
+ from .firmware import list_boards
114
+ boards = list_boards()
115
+ if not boards:
116
+ print("No board configs found. Run from inside the GhostESP repo.")
117
+ return 1
118
+ print(f"{'Board':<30} {'Target':<12}")
119
+ print("-" * 42)
120
+ for b in boards:
121
+ print(f"{b['id']:<30} {b['target']:<12}")
122
+ print(f"\n{len(boards)} boards available")
123
+ elif args.command == "firmware":
124
+ from .firmware import build_firmware
125
+ build_firmware(
126
+ board=args.board,
127
+ repo_root=args.repo,
128
+ skip_set_target=args.skip_set_target,
129
+ )
130
+ elif args.command == "flash":
131
+ from .flash import flash_firmware, flash_app, monitor
132
+ if args.target == "app":
133
+ if not args.app_dir:
134
+ print("--app-dir is required for 'app' target", file=sys.stderr)
135
+ return 2
136
+ used_port = flash_app(
137
+ app_dir=args.app_dir,
138
+ port=args.port,
139
+ baud=args.baud,
140
+ )
141
+ else:
142
+ used_port = flash_firmware(
143
+ port=args.port,
144
+ baud=args.baud,
145
+ board=args.board,
146
+ build_dir=args.build_dir,
147
+ verify=args.verify,
148
+ erase_flash=args.erase,
149
+ )
150
+ if args.monitor and used_port:
151
+ monitor(port=used_port)
152
+ elif args.command == "monitor":
153
+ from .flash import monitor
154
+ monitor(port=args.port, baud=args.baud)
155
+ elif args.command == "ports":
156
+ from .flash import list_serial_ports
157
+ ports = list_serial_ports()
158
+ if not ports:
159
+ print("No serial ports found.")
160
+ return 1
161
+ print(f"{'Port':<20} {'Description'}")
162
+ print("-" * 60)
163
+ for p in ports:
164
+ print(f"{p['device']:<20} {p['description']}")
165
+ print(f"\n{len(ports)} port(s) available")
166
+
167
+ return 0
168
+
169
+
170
+ if __name__ == "__main__":
171
+ raise SystemExit(main())
ghostbt/config.py ADDED
@@ -0,0 +1,92 @@
1
+ import json
2
+ import os
3
+ import pathlib
4
+ import shutil
5
+
6
+
7
+ REPO_MARKERS = ["plugins", "main", "components"]
8
+ SDK_RELATIVE = pathlib.Path("plugins") / "sdk"
9
+ TEMPLATES_RELATIVE = pathlib.Path("plugins") / "templates"
10
+
11
+
12
+ def _data_root() -> pathlib.Path:
13
+ here = pathlib.Path(__file__).resolve().parent
14
+ return here / "data"
15
+
16
+
17
+ def find_repo_root(start: pathlib.Path = None) -> pathlib.Path:
18
+ env = os.environ.get("GHOSTBT_ROOT")
19
+ if env:
20
+ p = pathlib.Path(env)
21
+ if all((p / m).is_dir() for m in REPO_MARKERS):
22
+ return p
23
+ cur = (start or pathlib.Path.cwd()).resolve()
24
+ for _ in range(20):
25
+ if all((cur / m).is_dir() for m in REPO_MARKERS):
26
+ return cur
27
+ parent = cur.parent
28
+ if parent == cur:
29
+ break
30
+ cur = parent
31
+ raise FileNotFoundError(
32
+ "Cannot find GhostESP repo root. Run from inside the repo or set GHOSTBT_ROOT."
33
+ )
34
+
35
+
36
+ def resolve_sdk_path(start: pathlib.Path = None) -> pathlib.Path:
37
+ env = os.environ.get("GHOSTBT_SDK")
38
+ if env:
39
+ p = pathlib.Path(env)
40
+ if p.is_file():
41
+ return p
42
+ try:
43
+ root = find_repo_root(start)
44
+ sdk = root / SDK_RELATIVE / "ghostesp_plugin_api.h"
45
+ if sdk.is_file():
46
+ return sdk
47
+ except FileNotFoundError:
48
+ pass
49
+ bundled = _data_root() / "sdk" / "ghostesp_plugin_api.h"
50
+ if bundled.is_file():
51
+ return bundled
52
+ raise FileNotFoundError(
53
+ "SDK header not found. Set GHOSTBT_SDK environment variable, run from a GhostESP repo, or reinstall ghostbt."
54
+ )
55
+
56
+
57
+ def resolve_template(name: str, start: pathlib.Path = None) -> pathlib.Path:
58
+ try:
59
+ repo = find_repo_root(start)
60
+ tpl = repo / TEMPLATES_RELATIVE / name
61
+ if tpl.is_dir():
62
+ return tpl
63
+ except FileNotFoundError:
64
+ pass
65
+ bundled = _data_root() / "templates" / name
66
+ if bundled.is_dir():
67
+ return bundled
68
+ raise FileNotFoundError(f"Template not found: {name}")
69
+
70
+
71
+ def copy_bundled_sdk(dst_dir: pathlib.Path) -> pathlib.Path:
72
+ dst = dst_dir / "sdk" / "ghostesp_plugin_api.h"
73
+ dst.parent.mkdir(parents=True, exist_ok=True)
74
+ src = resolve_sdk_path()
75
+ if not dst.exists() or src.stat().st_mtime > dst.stat().st_mtime:
76
+ shutil.copy2(src, dst)
77
+ return dst
78
+
79
+
80
+ def load_manifest(app_dir: pathlib.Path) -> dict:
81
+ manifest_path = app_dir / "manifest.json"
82
+ if not manifest_path.is_file():
83
+ raise FileNotFoundError(f"manifest.json not found in {app_dir}")
84
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
85
+ for field in ("id", "entry"):
86
+ if field not in manifest:
87
+ raise ValueError(f"manifest.json missing required field: {field}")
88
+ return manifest
89
+
90
+
91
+ def detect_target(manifest: dict) -> str:
92
+ return manifest.get("target", "esp32s3")
ghostbt/create.py ADDED
@@ -0,0 +1,54 @@
1
+ import pathlib
2
+ import shutil
3
+ import sys
4
+
5
+ from .config import resolve_template, copy_bundled_sdk
6
+
7
+
8
+ def _render_file(path: pathlib.Path, values: dict) -> None:
9
+ text = path.read_text(encoding="utf-8")
10
+ for key, value in values.items():
11
+ text = text.replace("{{" + key + "}}", value)
12
+ path.write_text(text, encoding="utf-8")
13
+
14
+
15
+ def create_app(
16
+ app_id: str,
17
+ name: str = None,
18
+ template: str = "basic_app",
19
+ out_dir: str = ".",
20
+ ) -> pathlib.Path:
21
+ if not app_id.replace("_", "").replace("-", "").isalnum():
22
+ print("app_id may only contain letters, numbers, _ and -", file=sys.stderr)
23
+ raise SystemExit(2)
24
+
25
+ src = resolve_template(template)
26
+ out_parent = pathlib.Path(out_dir).resolve()
27
+ dst = out_parent / app_id
28
+ if dst.exists():
29
+ print(f"destination already exists: {dst}", file=sys.stderr)
30
+ raise SystemExit(2)
31
+
32
+ shutil.copytree(src, dst)
33
+ copy_bundled_sdk(dst)
34
+
35
+ symbol = app_id.replace("-", "_")
36
+ display_name = name or app_id.replace("_", " ").replace("-", " ").title()
37
+
38
+ values = {
39
+ "APP_ID": app_id,
40
+ "APP_NAME": display_name,
41
+ "APP_SYMBOL": symbol,
42
+ }
43
+
44
+ for path in sorted(dst.rglob("*"), key=lambda p: len(p.parts), reverse=True):
45
+ if "{{APP_SYMBOL}}" in path.name:
46
+ path = path.rename(path.with_name(path.name.replace("{{APP_SYMBOL}}", symbol)))
47
+ if path.is_file() and (
48
+ path.suffix in {".c", ".h", ".json", ".txt", ".cmake", ".yml"}
49
+ or path.name == "CMakeLists.txt"
50
+ ):
51
+ _render_file(path, values)
52
+
53
+ print(dst)
54
+ return dst