lucent-pack 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.
- lucent_pack/__init__.py +3 -0
- lucent_pack/__main__.py +3 -0
- lucent_pack/build.py +244 -0
- lucent_pack/cli.py +119 -0
- lucent_pack/serve.py +41 -0
- lucent_pack/static/lucent/dist/httpuv-sw.d.ts +2 -0
- lucent_pack/static/lucent/dist/httpuv-sw.js +1478 -0
- lucent_pack/static/lucent/dist/httpuv-sw.js.map +1 -0
- lucent_pack/static/lucent/dist/httpuv-web.d.ts +263 -0
- lucent_pack/static/lucent/dist/httpuv-web.js +794 -0
- lucent_pack/static/lucent/dist/httpuv-web.js.map +1 -0
- lucent_pack/static/lucent/dist/rWasmWorker.d.ts +2 -0
- lucent_pack/static/lucent/dist/rWasmWorker.js +1662 -0
- lucent_pack/static/lucent/dist/rWasmWorker.js.map +1 -0
- lucent_pack/static/lucent/dist/runApp.d.ts +8 -0
- lucent_pack/static/lucent/dist/runApp.js +1056 -0
- lucent_pack/static/lucent/dist/runApp.js.map +1 -0
- lucent_pack/static/lucent/dist/shiny-socket.d.ts +53 -0
- lucent_pack/static/lucent/dist/shiny-socket.js +388 -0
- lucent_pack/static/lucent/dist/shiny-socket.js.map +1 -0
- lucent_pack/templates/favicon.svg +7 -0
- lucent_pack/templates/index.html +96 -0
- lucent_pack/templates/style.css +73 -0
- lucent_pack-0.1.0.dist-info/METADATA +90 -0
- lucent_pack-0.1.0.dist-info/RECORD +28 -0
- lucent_pack-0.1.0.dist-info/WHEEL +4 -0
- lucent_pack-0.1.0.dist-info/entry_points.txt +2 -0
- lucent_pack-0.1.0.dist-info/licenses/LICENSE +28 -0
lucent_pack/__init__.py
ADDED
lucent_pack/__main__.py
ADDED
lucent_pack/build.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Build a static Lucent site from an existing wasm prefix and Shiny app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from importlib.resources import as_file, files
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# VFS mount used by Lucent's startApp(appDir = "webApp", ...).
|
|
12
|
+
WEB_APP_MOUNT = "/webApp"
|
|
13
|
+
WEB_APP_ARCHIVE = "webApp.tar.gz"
|
|
14
|
+
EMPACK_META_NAME = "empack_env_meta.json"
|
|
15
|
+
|
|
16
|
+
RUNTIME_BIN_FILES = ("Rmain.js", "Rmain.wasm")
|
|
17
|
+
RUNTIME_LIB_FILES = ("libR.so", "libRblas.so", "libRlapack.so")
|
|
18
|
+
|
|
19
|
+
REQUIRED_LUCENT_DIST_FILES = (
|
|
20
|
+
"runApp.js",
|
|
21
|
+
"rWasmWorker.js",
|
|
22
|
+
"httpuv-web.js",
|
|
23
|
+
"httpuv-sw.js",
|
|
24
|
+
"shiny-socket.js",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
SHELL_TEMPLATE_FILES = ("style.css", "favicon.svg")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BuildError(Exception):
|
|
31
|
+
"""User-facing build failure."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build(
|
|
35
|
+
*,
|
|
36
|
+
prefix_dir: Path,
|
|
37
|
+
app_dir: Path,
|
|
38
|
+
out_dir: Path,
|
|
39
|
+
title: str = "Shiny App",
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Pack ``prefix_dir`` + ``app_dir`` into ``out_dir``.
|
|
42
|
+
|
|
43
|
+
Steps:
|
|
44
|
+
1. Validate the prefix (Rmain / libR*) and app directory
|
|
45
|
+
2. Run empack pack env / pack dir / pack append into ``out_dir/packages/``
|
|
46
|
+
3. Copy Rmain binaries and libR*.so into ``out_dir/runtime/``
|
|
47
|
+
4. Assemble Lucent JS, shell templates, SW alias, and app sources
|
|
48
|
+
"""
|
|
49
|
+
prefix_dir = prefix_dir.expanduser().resolve()
|
|
50
|
+
app_dir = app_dir.expanduser().resolve()
|
|
51
|
+
out_dir = out_dir.expanduser().resolve()
|
|
52
|
+
|
|
53
|
+
validate_prefix(prefix_dir)
|
|
54
|
+
validate_app(app_dir)
|
|
55
|
+
|
|
56
|
+
packages_dir = out_dir / "packages"
|
|
57
|
+
runtime_dir = out_dir / "runtime"
|
|
58
|
+
|
|
59
|
+
_log(f"preparing output directory {out_dir}")
|
|
60
|
+
if out_dir.exists():
|
|
61
|
+
shutil.rmtree(out_dir)
|
|
62
|
+
packages_dir.mkdir(parents=True)
|
|
63
|
+
(runtime_dir / "bin").mkdir(parents=True)
|
|
64
|
+
(runtime_dir / "lib" / "R" / "lib").mkdir(parents=True)
|
|
65
|
+
|
|
66
|
+
pack_env(prefix_dir, packages_dir)
|
|
67
|
+
pack_app(app_dir, packages_dir)
|
|
68
|
+
append_app(packages_dir)
|
|
69
|
+
copy_runtime(prefix_dir, runtime_dir)
|
|
70
|
+
|
|
71
|
+
_log(f"packed packages → {packages_dir}")
|
|
72
|
+
_log(f"copied runtime → {runtime_dir}")
|
|
73
|
+
|
|
74
|
+
assemble_site(out_dir=out_dir, app_dir=app_dir, title=title)
|
|
75
|
+
_log(f"done → {out_dir}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_prefix(prefix_dir: Path) -> None:
|
|
79
|
+
if not prefix_dir.is_dir():
|
|
80
|
+
raise BuildError(f"prefix directory does not exist: {prefix_dir}")
|
|
81
|
+
|
|
82
|
+
missing: list[str] = []
|
|
83
|
+
for name in RUNTIME_BIN_FILES:
|
|
84
|
+
if not (prefix_dir / "bin" / name).is_file():
|
|
85
|
+
missing.append(f"bin/{name}")
|
|
86
|
+
for name in RUNTIME_LIB_FILES:
|
|
87
|
+
if not (prefix_dir / "lib" / "R" / "lib" / name).is_file():
|
|
88
|
+
missing.append(f"lib/R/lib/{name}")
|
|
89
|
+
if missing:
|
|
90
|
+
raise BuildError(
|
|
91
|
+
"prefix looks incomplete (not an emscripten-wasm32 R env with r-main?). "
|
|
92
|
+
f"Missing under {prefix_dir}: {', '.join(missing)}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_app(app_dir: Path) -> None:
|
|
97
|
+
if not app_dir.is_dir():
|
|
98
|
+
raise BuildError(f"app directory does not exist: {app_dir}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def pack_env(prefix_dir: Path, packages_dir: Path) -> None:
|
|
102
|
+
_log(f"empack pack env --env-prefix {prefix_dir}")
|
|
103
|
+
_run_empack(
|
|
104
|
+
[
|
|
105
|
+
"pack",
|
|
106
|
+
"env",
|
|
107
|
+
"--env-prefix",
|
|
108
|
+
str(prefix_dir),
|
|
109
|
+
"--outdir",
|
|
110
|
+
str(packages_dir),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
meta = packages_dir / EMPACK_META_NAME
|
|
114
|
+
if not meta.is_file():
|
|
115
|
+
raise BuildError(f"empack pack env did not write {meta}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def pack_app(app_dir: Path, packages_dir: Path) -> None:
|
|
119
|
+
_log(f"empack pack dir {app_dir} → {WEB_APP_MOUNT}")
|
|
120
|
+
_run_empack(
|
|
121
|
+
[
|
|
122
|
+
"pack",
|
|
123
|
+
"dir",
|
|
124
|
+
"--host-dir",
|
|
125
|
+
str(app_dir),
|
|
126
|
+
"--mount-dir",
|
|
127
|
+
WEB_APP_MOUNT,
|
|
128
|
+
"--outname",
|
|
129
|
+
WEB_APP_ARCHIVE,
|
|
130
|
+
"--outdir",
|
|
131
|
+
str(packages_dir),
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
archive = packages_dir / WEB_APP_ARCHIVE
|
|
135
|
+
if not archive.is_file():
|
|
136
|
+
raise BuildError(f"empack pack dir did not write {archive}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def append_app(packages_dir: Path) -> None:
|
|
140
|
+
meta = packages_dir / EMPACK_META_NAME
|
|
141
|
+
archive = packages_dir / WEB_APP_ARCHIVE
|
|
142
|
+
_log(f"empack pack append {archive.name} → {meta.name}")
|
|
143
|
+
_run_empack(
|
|
144
|
+
[
|
|
145
|
+
"pack",
|
|
146
|
+
"append",
|
|
147
|
+
"--env-meta",
|
|
148
|
+
str(meta),
|
|
149
|
+
"--tarfile",
|
|
150
|
+
str(archive),
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def copy_runtime(prefix_dir: Path, runtime_dir: Path) -> None:
|
|
156
|
+
"""Copy Rmain bootstrap assets needed before VFS populate."""
|
|
157
|
+
_log("copying Rmain.js / Rmain.wasm / libR*.so into runtime/")
|
|
158
|
+
bin_dst = runtime_dir / "bin"
|
|
159
|
+
lib_dst = runtime_dir / "lib" / "R" / "lib"
|
|
160
|
+
bin_dst.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
lib_dst.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
for name in RUNTIME_BIN_FILES:
|
|
164
|
+
shutil.copy2(prefix_dir / "bin" / name, bin_dst / name)
|
|
165
|
+
for name in RUNTIME_LIB_FILES:
|
|
166
|
+
shutil.copy2(prefix_dir / "lib" / "R" / "lib" / name, lib_dst / name)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def assemble_site(*, out_dir: Path, app_dir: Path, title: str) -> None:
|
|
170
|
+
"""Copy Lucent assets, shell templates, SW alias, and app sources into ``out_dir``.
|
|
171
|
+
|
|
172
|
+
Expects ``out_dir/packages`` and ``out_dir/runtime`` to already exist from packing.
|
|
173
|
+
"""
|
|
174
|
+
_log("assembling site shell and Lucent assets")
|
|
175
|
+
|
|
176
|
+
lucent_dist = files("lucent_pack").joinpath("static", "lucent", "dist")
|
|
177
|
+
missing = [name for name in REQUIRED_LUCENT_DIST_FILES if not lucent_dist.joinpath(name).is_file()]
|
|
178
|
+
if missing:
|
|
179
|
+
raise BuildError(
|
|
180
|
+
"bundled Lucent browser assets are missing from the package. "
|
|
181
|
+
f"Missing: {', '.join(missing)}. "
|
|
182
|
+
"Build them into src/lucent_pack/static/lucent/dist/ "
|
|
183
|
+
"(see README Development), or install a released lucent-pack wheel."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
dist_dest = out_dir / "lucent" / "dist"
|
|
187
|
+
with as_file(lucent_dist) as dist_src:
|
|
188
|
+
if dist_dest.exists():
|
|
189
|
+
shutil.rmtree(dist_dest)
|
|
190
|
+
shutil.copytree(dist_src, dist_dest)
|
|
191
|
+
|
|
192
|
+
sw_src = dist_dest / "httpuv-sw.js"
|
|
193
|
+
shutil.copy2(sw_src, out_dir / "httpuv-sw.js")
|
|
194
|
+
sw_map = dist_dest / "httpuv-sw.js.map"
|
|
195
|
+
if sw_map.is_file():
|
|
196
|
+
shutil.copy2(sw_map, out_dir / "httpuv-sw.js.map")
|
|
197
|
+
|
|
198
|
+
templates = files("lucent_pack").joinpath("templates")
|
|
199
|
+
index_src = templates.joinpath("index.html")
|
|
200
|
+
if not index_src.is_file():
|
|
201
|
+
raise BuildError("package is missing templates/index.html")
|
|
202
|
+
index_html = index_src.read_text(encoding="utf-8")
|
|
203
|
+
(out_dir / "index.html").write_text(
|
|
204
|
+
index_html.replace("{{ title }}", html.escape(title)),
|
|
205
|
+
encoding="utf-8",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
for name in SHELL_TEMPLATE_FILES:
|
|
209
|
+
asset = templates.joinpath(name)
|
|
210
|
+
if not asset.is_file():
|
|
211
|
+
raise BuildError(f"package is missing templates/{name}")
|
|
212
|
+
(out_dir / name).write_bytes(asset.read_bytes())
|
|
213
|
+
|
|
214
|
+
webapp_dest = out_dir / "webApp"
|
|
215
|
+
if webapp_dest.exists():
|
|
216
|
+
shutil.rmtree(webapp_dest)
|
|
217
|
+
# Follow symlinks inside the app tree (same idea as prepare-pages.sh `cp -aL`).
|
|
218
|
+
shutil.copytree(app_dir, webapp_dest, symlinks=False)
|
|
219
|
+
|
|
220
|
+
_log("wrote index.html, style.css, favicon.svg, lucent/dist/, httpuv-sw.js, webApp/")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _empack_executable() -> str:
|
|
224
|
+
exe = shutil.which("empack")
|
|
225
|
+
if exe is None:
|
|
226
|
+
raise BuildError(
|
|
227
|
+
"empack not found on PATH. Install lucent-pack (pulls in empack) "
|
|
228
|
+
"and ensure its environment is active."
|
|
229
|
+
)
|
|
230
|
+
return exe
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _run_empack(args: list[str]) -> None:
|
|
234
|
+
cmd = [_empack_executable(), *args]
|
|
235
|
+
try:
|
|
236
|
+
subprocess.run(cmd, check=True)
|
|
237
|
+
except subprocess.CalledProcessError as exc:
|
|
238
|
+
raise BuildError(
|
|
239
|
+
f"empack failed with exit code {exc.returncode}: {' '.join(cmd)}"
|
|
240
|
+
) from exc
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _log(message: str) -> None:
|
|
244
|
+
print(f"[lucent] {message}", flush=True)
|
lucent_pack/cli.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Command-line entry point for the ``lucent`` console script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="lucent",
|
|
13
|
+
description="Pack a Shiny app and wasm R prefix into a static Lucent site.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--version",
|
|
17
|
+
action="version",
|
|
18
|
+
version=f"%(prog)s {_package_version()}",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
build = subparsers.add_parser(
|
|
24
|
+
"build",
|
|
25
|
+
help="Pack an existing wasm prefix and Shiny app into a static site",
|
|
26
|
+
)
|
|
27
|
+
build.add_argument(
|
|
28
|
+
"--prefix-dir",
|
|
29
|
+
type=Path,
|
|
30
|
+
required=True,
|
|
31
|
+
help="Existing emscripten-wasm32 conda/micromamba prefix",
|
|
32
|
+
)
|
|
33
|
+
build.add_argument(
|
|
34
|
+
"--app",
|
|
35
|
+
type=Path,
|
|
36
|
+
required=True,
|
|
37
|
+
help="Shiny app directory (mounted at /webApp in the VFS)",
|
|
38
|
+
)
|
|
39
|
+
build.add_argument(
|
|
40
|
+
"--outdir",
|
|
41
|
+
type=Path,
|
|
42
|
+
default=Path("_site"),
|
|
43
|
+
help="Output directory for the static site (default: _site)",
|
|
44
|
+
)
|
|
45
|
+
build.add_argument(
|
|
46
|
+
"--title",
|
|
47
|
+
default="Shiny App",
|
|
48
|
+
help='HTML <title> for the app shell (default: "Shiny App")',
|
|
49
|
+
)
|
|
50
|
+
build.set_defaults(func=_cmd_build)
|
|
51
|
+
|
|
52
|
+
serve = subparsers.add_parser(
|
|
53
|
+
"serve",
|
|
54
|
+
help="Serve a built static site directory over HTTP",
|
|
55
|
+
)
|
|
56
|
+
serve.add_argument(
|
|
57
|
+
"directory",
|
|
58
|
+
type=Path,
|
|
59
|
+
help="Output directory from `lucent build` (e.g. _site/)",
|
|
60
|
+
)
|
|
61
|
+
serve.add_argument(
|
|
62
|
+
"--port",
|
|
63
|
+
type=int,
|
|
64
|
+
default=8000,
|
|
65
|
+
help="Port to listen on (default: 8000)",
|
|
66
|
+
)
|
|
67
|
+
serve.set_defaults(func=_cmd_serve)
|
|
68
|
+
|
|
69
|
+
return parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _package_version() -> str:
|
|
73
|
+
from lucent_pack import __version__
|
|
74
|
+
|
|
75
|
+
return __version__
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _cmd_build(args: argparse.Namespace) -> int:
|
|
79
|
+
from lucent_pack.build import BuildError, build
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
build(
|
|
83
|
+
prefix_dir=args.prefix_dir,
|
|
84
|
+
app_dir=args.app,
|
|
85
|
+
out_dir=args.outdir,
|
|
86
|
+
title=args.title,
|
|
87
|
+
)
|
|
88
|
+
except BuildError as exc:
|
|
89
|
+
print(f"lucent: error: {exc}", file=sys.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
95
|
+
from lucent_pack.serve import ServeError, serve
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
serve(directory=args.directory, port=args.port)
|
|
99
|
+
except ServeError as exc:
|
|
100
|
+
print(f"lucent: error: {exc}", file=sys.stderr)
|
|
101
|
+
return 1
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv: list[str] | None = None) -> int:
|
|
106
|
+
parser = _build_parser()
|
|
107
|
+
args = parser.parse_args(argv)
|
|
108
|
+
try:
|
|
109
|
+
return args.func(args)
|
|
110
|
+
except NotImplementedError as exc:
|
|
111
|
+
print(f"lucent: {exc}", file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
except Exception as exc: # noqa: BLE001 — top-level CLI boundary
|
|
114
|
+
print(f"lucent: error: {exc}", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
raise SystemExit(main())
|
lucent_pack/serve.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Serve a built static Lucent site directory over HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
from functools import partial
|
|
7
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ServeError(Exception):
|
|
12
|
+
"""User-facing serve failure."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def serve(*, directory: Path, port: int = 8000) -> None:
|
|
16
|
+
"""Serve ``directory`` with a stdlib HTTP server."""
|
|
17
|
+
directory = directory.expanduser().resolve()
|
|
18
|
+
if not directory.is_dir():
|
|
19
|
+
raise ServeError(f"directory does not exist: {directory}")
|
|
20
|
+
if not (1 <= port <= 65535):
|
|
21
|
+
raise ServeError(f"invalid port: {port}")
|
|
22
|
+
|
|
23
|
+
# Ensure Rmain.wasm is served with the correct type (may be missing in older MIME DBs).
|
|
24
|
+
mimetypes.add_type("application/wasm", ".wasm")
|
|
25
|
+
|
|
26
|
+
handler = partial(SimpleHTTPRequestHandler, directory=str(directory))
|
|
27
|
+
try:
|
|
28
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), handler)
|
|
29
|
+
except OSError as exc:
|
|
30
|
+
raise ServeError(f"could not bind to port {port}: {exc}") from exc
|
|
31
|
+
|
|
32
|
+
url = f"http://127.0.0.1:{port}/"
|
|
33
|
+
print(f"[lucent] serving {directory}", flush=True)
|
|
34
|
+
print(f"[lucent] {url}", flush=True)
|
|
35
|
+
print("[lucent] press Ctrl+C to stop", flush=True)
|
|
36
|
+
try:
|
|
37
|
+
server.serve_forever()
|
|
38
|
+
except KeyboardInterrupt:
|
|
39
|
+
print("\n[lucent] stopped", flush=True)
|
|
40
|
+
finally:
|
|
41
|
+
server.server_close()
|