[FIX] OneCAD web: Odoo Documents integration sources (services_odoo.rs, odoo glue, build script); wasm32 picker freeze fixed with web_time::Instant
This commit is contained in:
1 parent
4e0601611c
commit
ac05e26934
9 files changed
+3028
No files matched your search
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OneBSpace (OneCAD) — build the SolidCraft wasm bundle into the one_cad Odoo addon.
|
||||
|
||||
Stdlib-only build chain (python3 scripts/build_onecad_addon.py [--dry-run]):
|
||||
|
||||
1. optional clean: ODA_CLEAN=1 wipes the dist target before building
|
||||
2. build: prefer trunk (apps/solidcraft-web/Trunk.toml); fallback is
|
||||
onepixel's tools/cargo-run chain verbatim:
|
||||
cargo build --release --target wasm32-unknown-unknown -p solidcraft-web
|
||||
wasm-bindgen --target web --out-dir <dist> --out-name onecad <wasm>
|
||||
wasm-opt -Oz (when on PATH)
|
||||
plus index.html + static assets copied into <dist>
|
||||
3. rebrand: <title>OneCAD</title> + data-app-name="OneCAD" in index.html;
|
||||
drop sw.js / manifest.webmanifest / build.json / icon files
|
||||
4. verify gate: FAIL on absolute /(assets|fonts|blobs)/ references in any text
|
||||
file of the dist; require index.html, a *_bg.wasm and a
|
||||
wasm-bindgen glue .js — exits non-zero on failure
|
||||
5. mirror-copy: dist -> /home/z/my-project/one-os-dev/core-components/one_cad/static/onecad/
|
||||
(rsync-like: stale files in the destination are removed)
|
||||
6. reminder: bump bundleVersion (static/src/one_cad_app.js) + addon manifest version
|
||||
|
||||
Exit codes: 0 ok, 1 gate failure, 2 build environment problem.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
APP = REPO / "apps" / "solidcraft-web"
|
||||
DIST = REPO / "dist" / "web"
|
||||
DEST = Path("/home/z/my-project/one-os-dev/core-components/one_cad/static/onecad")
|
||||
WASM_TARGET = "wasm32-unknown-unknown"
|
||||
WASM_ARTIFACT = REPO / "target" / WASM_TARGET / "release" / "solidcraft_web.wasm"
|
||||
OUT_NAME = "onecad"
|
||||
|
||||
TEXT_SUFFIXES = {".html", ".js", ".mjs", ".css", ".json", ".svg", ".txt", ".webmanifest", ".map"}
|
||||
ABS_REF = re.compile(r"""["'=(]/(?:assets|fonts|blobs)/""")
|
||||
ICON_FILE = re.compile(r"^(icon|apple-touch-icon|favicon).*", re.I)
|
||||
DROP_FILES = {"sw.js", "manifest.webmanifest", "site.webmanifest", "build.json"}
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(f"[onecad] {message}")
|
||||
|
||||
|
||||
def sh(cmd: list[str], cwd: Path | None = None) -> None:
|
||||
log("$ " + " ".join(cmd) + (f" (cwd={cwd})" if cwd else ""))
|
||||
try:
|
||||
subprocess.run([str(c) for c in cmd], cwd=str(cwd) if cwd else None, check=True)
|
||||
except FileNotFoundError as exc:
|
||||
sys.exit(f"[onecad] tool not found: {exc.filename}")
|
||||
except subprocess.CalledProcessError as exc:
|
||||
sys.exit(f"[onecad] command failed with exit code {exc.returncode}: {' '.join(str(c) for c in cmd)}")
|
||||
|
||||
|
||||
def clean_dist() -> None:
|
||||
if DIST.exists():
|
||||
log(f"ODA_CLEAN=1: wiping {DIST}")
|
||||
shutil.rmtree(DIST, ignore_errors=True)
|
||||
|
||||
|
||||
def build() -> Path:
|
||||
"""Build the wasm bundle; return the dist directory that holds index.html."""
|
||||
if shutil.which("trunk"):
|
||||
sh(["trunk", "build", "--release", "--public-url", "./"], cwd=APP)
|
||||
else:
|
||||
log("trunk not on PATH — using the cargo + wasm-bindgen chain (onepixel convention)")
|
||||
sh(["cargo", "build", "--release", "--target", WASM_TARGET, "-p", "solidcraft-web"], cwd=REPO)
|
||||
if not WASM_ARTIFACT.exists():
|
||||
sys.exit(f"[onecad] expected wasm artifact missing: {WASM_ARTIFACT}")
|
||||
DIST.mkdir(parents=True, exist_ok=True)
|
||||
sh(["wasm-bindgen", "--target", "web", "--out-dir", DIST, "--out-name", OUT_NAME, WASM_ARTIFACT])
|
||||
if shutil.which("wasm-opt"):
|
||||
optimized = DIST / f"{OUT_NAME}_bg.wasm.opt"
|
||||
sh(["wasm-opt", "-Oz", DIST / f"{OUT_NAME}_bg.wasm", "-o", optimized])
|
||||
os.replace(optimized, DIST / f"{OUT_NAME}_bg.wasm")
|
||||
else:
|
||||
log("wasm-opt not on PATH — skipping -Oz (larger bundle, still correct)")
|
||||
# Static assets trunk would have copied: index.html + the assets tree.
|
||||
if (APP / "index.html").exists():
|
||||
shutil.copy2(APP / "index.html", DIST / "index.html")
|
||||
if (APP / "assets").is_dir():
|
||||
shutil.copytree(APP / "assets", DIST / "assets", dirs_exist_ok=True)
|
||||
for candidate in (DIST, APP / "dist"):
|
||||
if (candidate / "index.html").is_file():
|
||||
return candidate
|
||||
sys.exit("[onecad] build produced no index.html in dist/web or apps/solidcraft-web/dist")
|
||||
|
||||
|
||||
def rebrand(dist: Path) -> None:
|
||||
index = dist / "index.html"
|
||||
html = index.read_text(encoding="utf-8")
|
||||
html, n_title = re.subn(r"<title\b[^>]*>.*?</title>", "<title>OneCAD</title>", html, flags=re.S | re.I)
|
||||
if 'data-app-name="' in html:
|
||||
html = re.sub(r'data-app-name="[^"]*"', 'data-app-name="OneCAD"', html)
|
||||
else:
|
||||
html = re.sub(r"<html\b", '<html data-app-name="OneCAD"', html, count=1)
|
||||
# Service worker, PWA manifest and icon links have no meaning inside the
|
||||
# Odoo iframe — drop the tags along with the files.
|
||||
html = re.sub(r"<link[^>]+rel=[\"']manifest[\"'][^>]*>\s*", "", html, flags=re.I)
|
||||
html = re.sub(r"<link[^>]+rel=[\"'][^\"']*(icon|apple-touch)[^\"']*[\"'][^>]*>\s*", "", html, flags=re.I)
|
||||
index.write_text(html, encoding="utf-8")
|
||||
log(f"rebranded {index} (title replacements: {n_title})")
|
||||
dropped = 0
|
||||
for path in sorted(dist.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.name in DROP_FILES or ICON_FILE.match(path.name):
|
||||
path.unlink()
|
||||
dropped += 1
|
||||
log(f"dropped {dropped} sw/manifest/icon files from the bundle")
|
||||
|
||||
|
||||
def verify(dist: Path) -> list[str]:
|
||||
"""The gate: absolute /(assets|fonts|blobs)/ refs fail; glue + wasm must exist."""
|
||||
errors: list[str] = []
|
||||
if not (dist / "index.html").is_file():
|
||||
errors.append("missing index.html")
|
||||
wasm_files = [p for p in dist.rglob("*.wasm")]
|
||||
if not any(p.name.endswith("_bg.wasm") for p in wasm_files):
|
||||
errors.append("no *_bg.wasm (wasm-bindgen output) in dist")
|
||||
glue = [
|
||||
p
|
||||
for p in dist.rglob("*.js")
|
||||
if "wasm_bindgen" in p.read_text(encoding="utf-8", errors="ignore")
|
||||
or "wasm-bindgen" in p.read_text(encoding="utf-8", errors="ignore")
|
||||
]
|
||||
if not glue:
|
||||
errors.append("no wasm-bindgen glue .js in dist")
|
||||
offenders: list[str] = []
|
||||
for path in sorted(dist.rglob("*")):
|
||||
if not path.is_file() or path.suffix.lower() not in TEXT_SUFFIXES:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
if ABS_REF.search(text):
|
||||
offenders.append(str(path.relative_to(dist)))
|
||||
if offenders:
|
||||
errors.append(
|
||||
"absolute /(assets|fonts|blobs)/ references (must be relative — the bundle is served "
|
||||
f"from /one_cad/static/onecad/): {', '.join(offenders[:10])}"
|
||||
+ (f" … (+{len(offenders) - 10} more)" if len(offenders) > 10 else "")
|
||||
)
|
||||
if errors:
|
||||
for error in errors:
|
||||
log(f"GATE FAIL: {error}")
|
||||
else:
|
||||
log(f"GATE PASS: {len(wasm_files)} wasm, {len(glue)} glue js, all references relative")
|
||||
return errors
|
||||
|
||||
|
||||
def mirror(src: Path, dst: Path) -> None:
|
||||
"""rsync-like: make dst exactly src (extra files in dst are removed)."""
|
||||
src_files = {p.relative_to(src) for p in src.rglob("*") if p.is_file()}
|
||||
src_dirs = {p.relative_to(src) for p in src.rglob("*") if p.is_dir()}
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
removed = 0
|
||||
for path in sorted(dst.rglob("*"), key=lambda p: len(p.parts), reverse=True):
|
||||
rel = path.relative_to(dst)
|
||||
if path.is_dir() and rel not in src_dirs:
|
||||
shutil.rmtree(path)
|
||||
removed += 1
|
||||
elif path.is_file() and rel not in src_files:
|
||||
path.unlink()
|
||||
removed += 1
|
||||
for rel in sorted(src_dirs):
|
||||
(dst / rel).mkdir(parents=True, exist_ok=True)
|
||||
for rel in sorted(src_files):
|
||||
shutil.copy2(src / rel, dst / rel)
|
||||
log(f"mirrored {len(src_files)} files -> {dst} ({removed} stale entries removed)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--dry-run", action="store_true", help="print the plan, no builds/writes; gates run read-only against an existing dist")
|
||||
args = parser.parse_args()
|
||||
|
||||
tools = {name: bool(shutil.which(name)) for name in ("trunk", "cargo", "wasm-bindgen", "wasm-opt")}
|
||||
tool_report = ", ".join(f"{name}={'yes' if present else 'no'}" for name, present in tools.items())
|
||||
log(f"tools: {tool_report}")
|
||||
if not args.dry_run and not (tools["trunk"] or (tools["cargo"] and tools["wasm-bindgen"])):
|
||||
log("need trunk, or cargo + wasm-bindgen, on PATH")
|
||||
return 2
|
||||
|
||||
if args.dry_run:
|
||||
log(f"DRY RUN: clean={'yes' if os.environ.get('ODA_CLEAN') == '1' else 'no'} -> build ({'trunk' if tools['trunk'] else 'cargo chain'}) -> {DIST} -> rebrand -> gate -> mirror {DEST}")
|
||||
if (DIST / "index.html").is_file():
|
||||
log("an existing dist was found; running the verification gate read-only")
|
||||
return 1 if verify(DIST) else 0
|
||||
log("no dist yet — plan only")
|
||||
return 0
|
||||
|
||||
if os.environ.get("ODA_CLEAN") == "1":
|
||||
clean_dist()
|
||||
dist = build()
|
||||
rebrand(dist)
|
||||
if verify(dist):
|
||||
return 1
|
||||
mirror(dist, DEST)
|
||||
log("NEXT STEPS:")
|
||||
log(" 1. bump bundleVersion in one-os-dev/core-components/one_cad/static/src/one_cad_app.js (cache-busts the iframe bundle)")
|
||||
log(" 2. bump the version in one-os-dev/core-components/one_cad/__manifest__.py to match the addon release")
|
||||
log(" 3. upgrade the module (odoo -u one_cad) so the new static bundle is served")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user