#!/usr/bin/env python3 """Stage official source/runtime files for the headless BIM probe.""" from __future__ import annotations import argparse import hashlib import shutil import subprocess import sys from pathlib import Path def copy_tree(source: Path, target: Path) -> None: target.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(source, target, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) def copy_file(source: Path, target: Path) -> None: target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, target) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() root = Path(__file__).resolve().parents[1] source = root / "freecad-source" python_root = source / "build-web" / "python-install" / "lib" / "python3.12" output = args.output.resolve() if output.exists(): shutil.rmtree(output) copy_tree(source / "src" / "Mod" / "BIM", output / "share" / "Mod" / "BIM") parser_source = source / "src" / "Mod" / "BIM" / "Resources" / "ArchSqlParserGenerator.py" grammar = source / "src" / "Mod" / "BIM" / "Resources" / "ArchSql.lark" generated_parser = output / "share" / "Mod" / "BIM" / "generated_sql_parser.py" subprocess.run([sys.executable, str(parser_source), str(grammar), str(generated_parser)], check=True) copy_tree(source / "src" / "Mod" / "Draft", output / "freecad-mod" / "Draft") copy_tree(source / "src" / "3rdParty" / "lazy_loader", output / "freecad-mod" / "lazy_loader") schema_candidates = ( root.parent / "FreeCADBaseLib" / "sources" / "src" / "Qt-5.15.13" / "qt3d" / "src" / "3rdparty" / "assimp" / "src" / "scripts" / "StepImporter" / "schema_ifc2x3.exp", root / "FreeCADBaseLib" / "sources" / "src" / "Qt-5.15.13" / "qt3d" / "src" / "3rdparty" / "assimp" / "src" / "scripts" / "StepImporter" / "schema_ifc2x3.exp", ) schema = next((path for path in schema_candidates if path.exists()), None) if schema is None: raise FileNotFoundError("official IFC EXPRESS schema resource is unavailable") copy_file(schema, output / "share" / "Mod" / "BIM" / "schema_ifc2x3.exp") (output / "share" / "Mod" / "BIM" / "legacy-fixture.ifc").write_text( "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('FreeCAD Web legacy probe'),'2;1');\nENDSEC;\n" "DATA;\n#1=IFCPERSON($,$,$,$,$,$,$,$);\nENDSEC;\nEND-ISO-10303-21;\n", encoding="ascii", ) # DAE imports xml.sax.saxutils -> urllib.request, which needs both email # and http.client even when the document is read from a local file. for name in ( "encodings", "json", "xml", "re", "collections", "logging", "importlib", "zipfile", "urllib", "email", "http", ): copy_tree(python_root / name, output / "freecad-python" / name) # Stage every top-level pure-Python standard-library module produced by the # same Emscripten Python build. Import chains such as Draft -> uuid -> # random -> pickle are implementation details of that runtime and a manual # allow-list repeatedly omitted valid dependencies. Native extension # modules remain excluded because this only selects ``*.py`` files. for module in sorted(python_root.glob("*.py")): copy_file(module, output / "freecad-python" / module.name) manifest = [] for resource in sorted(path for path in output.rglob("*") if path.is_file()): digest = hashlib.sha256(resource.read_bytes()).hexdigest() manifest.append(f"{digest} {resource.relative_to(output).as_posix()}") (output / ".resource-manifest.sha256").write_text("\n".join(manifest) + "\n", encoding="ascii") print(output) if __name__ == "__main__": main()