Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
127 lines
6.4 KiB
Python
127 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Assemble an unpublished desktop application; never run acceptance or typecheck."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
|
|
|
|
def active_native_builder(native):
|
|
for process in Path('/proc').glob('[0-9]*'):
|
|
try:
|
|
if (process / 'cwd').resolve() == native.parent and (process / 'comm').read_text().strip() in {'ninja', 'make', 'gmake', 'cmake', 'wasm-ld', 'wasm-opt'}:
|
|
return process.name
|
|
except OSError:
|
|
continue
|
|
|
|
def write_json(path, value):
|
|
path.write_text(json.dumps(value, indent=2) + "\n")
|
|
|
|
|
|
def share_candidate_assets(resources, application):
|
|
cache = resources.parent.parent / ".candidate-assets"
|
|
cache.mkdir(exist_ok=True)
|
|
for source in resources.rglob("*"):
|
|
target = application / source.relative_to(resources)
|
|
if source.is_symlink() or target.is_symlink() or not source.is_file() or not target.is_file():
|
|
continue
|
|
if (source.stat().st_mode & 0o777) != (target.stat().st_mode & 0o777):
|
|
continue
|
|
with source.open("rb") as stream:
|
|
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
with target.open("rb") as stream:
|
|
if hashlib.file_digest(stream, "sha256").hexdigest() != digest:
|
|
continue
|
|
anchor = cache / f"{digest}-{source.stat().st_mode & 0o777:o}"
|
|
if not anchor.exists():
|
|
os.link(source, anchor)
|
|
with anchor.open("rb") as stream:
|
|
if anchor.is_symlink() or hashlib.file_digest(stream, "sha256").hexdigest() != digest:
|
|
raise RuntimeError(f"Candidate asset cache differs: {anchor.name}")
|
|
for path in (source, target):
|
|
if os.path.samefile(anchor, path):
|
|
continue
|
|
temporary = path.with_name(f".{path.name}.share-{os.getpid()}")
|
|
os.link(anchor, temporary)
|
|
os.replace(temporary, path)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--output", required=True, help="New local candidate directory")
|
|
parser.add_argument("--native-bin", default="build_web_blender6/bin",
|
|
help="Directory containing web_engine.js and web_engine.wasm, or either file")
|
|
args = parser.parse_args()
|
|
root = Path(__file__).resolve().parents[2]
|
|
output = (root / args.output).resolve()
|
|
native = (root / args.native_bin).resolve()
|
|
if native.is_file() and native.name in {"web_engine.js", "web_engine.wasm"}:
|
|
native = native.parent
|
|
# Reject a wrong input before copying the public tree or leaving a partial
|
|
# candidate. This is packaging input handling, not runtime verification.
|
|
missing = [name for name in ("web_engine.js", "web_engine.wasm")
|
|
if not (native / name).is_file()]
|
|
if missing:
|
|
parser.error(f"Native directory {native} is missing: {', '.join(missing)}")
|
|
# A fresh destination preserves both previous candidates and published assets.
|
|
if builder := active_native_builder(native):
|
|
parser.error(f"Native build PID {builder} is still active; retain pending edits and package after it exits")
|
|
native_stamps = {name: (native / name).stat() for name in ("web_engine.js", "web_engine.wasm")}
|
|
output.mkdir(parents=True, exist_ok=False)
|
|
resources = output / "public"
|
|
subprocess.run(["cp", "-a", "--reflink=auto", str(root / "web/app/public"),
|
|
str(resources)], check=True)
|
|
vendor = resources / "vendor/blender"
|
|
hashes = {}
|
|
for name in ("web_engine.js", "web_engine.wasm"):
|
|
shutil.copy2(native / name, vendor / name)
|
|
with (vendor / name).open("rb") as stream:
|
|
hashes[name] = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
# Startup scripts, fonts, color management and Python's library are embedded
|
|
# by the native target. Retain the source startup resources with the candidate
|
|
# as well, without replacing any source or published runtime files.
|
|
shutil.copytree(root / "blender-5.2.0/scripts/startup", output / "scripts/startup")
|
|
shutil.copytree(root / "blender-5.2.0/scripts/presets", output / "scripts/presets")
|
|
module_path = resources / "wasm-module-manifest.json"
|
|
modules = json.loads(module_path.read_text())
|
|
for module in modules["modules"]:
|
|
resource = module["resources"].get("single")
|
|
if resource and resource["fileName"] == "web_engine.wasm":
|
|
resource.update(url="/vendor/blender/web_engine.wasm", sha256=hashes["web_engine.wasm"])
|
|
write_json(module_path, modules)
|
|
engine_path = resources / "engine-manifest.json"
|
|
engine = json.loads(engine_path.read_text())
|
|
for variant in engine["variants"]:
|
|
if variant["id"] == "single":
|
|
for kind, name in (("js", "web_engine.js"), ("wasm", "web_engine.wasm")):
|
|
variant["resources"][kind].update(url=f"/vendor/blender/{name}", sha256=hashes[name])
|
|
write_json(engine_path, engine)
|
|
env = dict(os.environ, WEB_DESKTOP_CANDIDATE_PUBLIC=str(resources),
|
|
WEB_DESKTOP_CANDIDATE_FACTORY=str(vendor / "web_engine.js"))
|
|
# Vite emits frontend and Worker from current source; no test/typecheck wrapper.
|
|
subprocess.run(["node", "node_modules/vite/bin/vite.js", "build", "--config",
|
|
"app/vite.config.ts", "--outDir", str(output / "app")],
|
|
cwd=root / "web", env=env, check=True)
|
|
share_candidate_assets(resources, output / "app")
|
|
if active_native_builder(native):
|
|
parser.error("Native build started during packaging; no candidate manifest published; retain pending edits")
|
|
for name, before in native_stamps.items():
|
|
after = (native / name).stat()
|
|
if (before.st_ino, before.st_size, before.st_mtime_ns) != (after.st_ino, after.st_size, after.st_mtime_ns):
|
|
parser.error(f"Native input changed during packaging: {name}; no candidate manifest published")
|
|
write_json(output / "candidate.json", {
|
|
"stageId": "main-desktop", "status": "implemented_unverified",
|
|
"scopeComplete": False, "nativeUIComplete": False,
|
|
"entry": "app/index.html", "nativeBin": str(native), "sha256": hashes,
|
|
"runtimeURL": "/vendor/blender/web_engine.wasm",
|
|
"startupResources": "embedded in WASM; source copy in scripts/",
|
|
"deployment": "not_performed", "validation": "not_run",
|
|
})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|