feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
@@ -36,15 +39,147 @@ def probe_object(document, type_id):
|
||||
}
|
||||
|
||||
|
||||
ALL_REFERENCE_MODULES = [
|
||||
"AddonManager", "Assembly", "BIM", "CAM", "Cloud", "Draft", "Fem", "Help",
|
||||
"Idf", "Import", "Inspection", "JtReader", "Material", "Measure", "Mesh",
|
||||
"MeshPart", "OpenSCAD", "Part", "PartDesign", "Plot", "Points",
|
||||
"ReverseEngineering", "Robot", "Sandbox", "Show", "Sketcher", "Spreadsheet",
|
||||
"Start", "Surface", "TechDraw", "TemplatePyMod", "Test", "Tux", "Web",
|
||||
]
|
||||
|
||||
GUI_REQUIRED_MODULES = {
|
||||
"AddonManager", "Assembly", "BIM", "CAM", "Draft", "Fem", "Help", "Idf",
|
||||
"Import", "Inspection", "JtReader", "Material", "Measure", "Mesh", "MeshPart",
|
||||
"OpenSCAD", "Part", "PartDesign", "Plot", "Points", "ReverseEngineering", "Robot",
|
||||
"Sandbox", "Show", "Sketcher", "Spreadsheet", "Start", "Surface", "TechDraw", "Tux",
|
||||
"Web",
|
||||
}
|
||||
|
||||
|
||||
def module_is_installed(name):
|
||||
suffixes = ("", ".py", ".so", ".pyd", ".dll", ".dylib")
|
||||
roots = [path for path in sys.path if path]
|
||||
try:
|
||||
resource_dir = App.getResourceDir()
|
||||
roots.extend([resource_dir, os.path.join(resource_dir, "Mod")])
|
||||
except Exception:
|
||||
pass
|
||||
for root in roots:
|
||||
for suffix in suffixes:
|
||||
if os.path.exists(os.path.join(root, name + suffix)):
|
||||
return True
|
||||
if os.path.exists(os.path.join(root, name, "__init__" + suffix)):
|
||||
return True
|
||||
if os.path.exists(os.path.join(root, "Mod", name + suffix)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def probe_module(name):
|
||||
gui_required = name in GUI_REQUIRED_MODULES
|
||||
installed = module_is_installed(name)
|
||||
spec = None
|
||||
try:
|
||||
spec = importlib.util.find_spec(name)
|
||||
except Exception:
|
||||
spec = None
|
||||
try:
|
||||
module = importlib.import_module(name)
|
||||
return {"name": name, "available": True, "file": getattr(module, "__file__", None)}
|
||||
importable = True
|
||||
import_error = None
|
||||
file = getattr(module, "__file__", None)
|
||||
except Exception as caught_error:
|
||||
importable = False
|
||||
import_error = caught_error
|
||||
file = getattr(spec, "origin", None) if spec else None
|
||||
compiled = bool(installed or spec)
|
||||
if importable and gui_required and not bool(getattr(App, "GuiUp", False)):
|
||||
runtime_status = "gui-only-unprobeable"
|
||||
elif importable:
|
||||
runtime_status = "compiled-importable"
|
||||
elif compiled:
|
||||
runtime_status = "compiled-import-failure"
|
||||
else:
|
||||
runtime_status = "not-built"
|
||||
result = {
|
||||
"name": name,
|
||||
"available": importable,
|
||||
"compiled": compiled,
|
||||
"importable": importable,
|
||||
"guiRequired": gui_required,
|
||||
"guiAvailable": bool(getattr(App, "GuiUp", False)),
|
||||
"runtimeStatus": runtime_status,
|
||||
"file": file,
|
||||
}
|
||||
if import_error is not None:
|
||||
result["error"] = str(import_error)
|
||||
return result
|
||||
|
||||
|
||||
def probe_gui_commands(document, selected_object):
|
||||
if not bool(getattr(App, "GuiUp", False)):
|
||||
return {"available": False, "workbenches": [], "commands": []}
|
||||
try:
|
||||
import FreeCADGui as Gui
|
||||
except Exception as error:
|
||||
return {"name": name, "available": False, "error": str(error)}
|
||||
return {"available": False, "workbenches": [], "commands": [], "error": str(error)}
|
||||
|
||||
try:
|
||||
workbench_names = sorted(Gui.listWorkbenches().keys())
|
||||
except Exception:
|
||||
workbench_names = []
|
||||
command_observations = {}
|
||||
|
||||
def action_enabled(command_id):
|
||||
try:
|
||||
command = Gui.Command.get(command_id)
|
||||
if command is None:
|
||||
return None
|
||||
actions = command.getAction()
|
||||
if not actions:
|
||||
return None
|
||||
return bool(actions[0].isEnabled())
|
||||
except Exception as error:
|
||||
return {"error": str(error)}
|
||||
|
||||
workbenches = []
|
||||
for workbench_name in workbench_names:
|
||||
try:
|
||||
Gui.activateWorkbench(workbench_name)
|
||||
command_ids = sorted(set(Gui.listCommands()))
|
||||
Gui.Selection.clearSelection()
|
||||
empty_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
|
||||
Gui.Selection.addSelection(document.Name, selected_object.Name)
|
||||
selected_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
|
||||
Gui.Selection.clearSelection()
|
||||
observations = {
|
||||
command_id: {
|
||||
"emptySelection": empty_selection.get(command_id),
|
||||
"selectedPartBox": selected_selection.get(command_id),
|
||||
}
|
||||
for command_id in command_ids
|
||||
}
|
||||
for command_id, observation in observations.items():
|
||||
command_observations.setdefault(command_id, []).append({"workbench": workbench_name, **observation})
|
||||
workbenches.append({"name": workbench_name, "commandCount": len(command_ids), "status": "probed"})
|
||||
except Exception as error:
|
||||
try:
|
||||
Gui.Selection.clearSelection()
|
||||
except Exception:
|
||||
pass
|
||||
workbenches.append({"name": workbench_name, "commandCount": 0, "status": "probe-failure", "error": str(error)})
|
||||
return {
|
||||
"available": True,
|
||||
"workbenches": workbenches,
|
||||
"commands": [
|
||||
{"id": command_id, "observations": observations}
|
||||
for command_id, observations in sorted(command_observations.items())
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
document = App.newDocument("ReferenceProbe")
|
||||
box_object = document.addObject("Part::Box", "CommandProbeBox")
|
||||
version = App.Version()
|
||||
result = {
|
||||
"schemaVersion": 1,
|
||||
@@ -54,15 +189,20 @@ result = {
|
||||
"gitBranch": str(version[6]) if len(version) > 6 else "",
|
||||
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
||||
"guiUp": bool(getattr(App, "GuiUp", False)),
|
||||
"buildPurpose": "headless-reference-oracle",
|
||||
"modules": [probe_module(name) for name in [
|
||||
"Part", "Material", "Measure", "Sketcher", "PartDesign", "TechDraw",
|
||||
"Spreadsheet", "Draft", "Mesh", "Fem", "CAM", "Assembly",
|
||||
]],
|
||||
"buildPurpose": "desktop-reference-oracle" if bool(getattr(App, "GuiUp", False)) else "headless-reference-oracle",
|
||||
"moduleCount": len(ALL_REFERENCE_MODULES),
|
||||
"modules": [probe_module(name) for name in ALL_REFERENCE_MODULES],
|
||||
"objects": [probe_object(document, type_id) for type_id in [
|
||||
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||
]],
|
||||
"guiCommands": probe_gui_commands(document, box_object),
|
||||
}
|
||||
result["moduleStatusSummary"] = {
|
||||
status: sum(1 for module in result["modules"] if module["runtimeStatus"] == status)
|
||||
for status in sorted({module["runtimeStatus"] for module in result["modules"]})
|
||||
}
|
||||
App.closeDocument(document.Name)
|
||||
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
|
||||
Reference in New Issue
Block a user