Files
workinf_Blender_Wasm/tools/web/build-main-desktop-candidate.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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()