@aryacode/dsh-engineering 0.4.7 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +34 -26
  3. package/cordis.patch.yml +7 -6
  4. package/entries/config.js +62 -0
  5. package/entries/ftview.js +5 -4
  6. package/entries/l5x.js +47 -15
  7. package/entries/mwi.js +26 -6
  8. package/entries/optix.js +105 -0
  9. package/lib/client.js +335 -6
  10. package/package.json +11 -10
  11. package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
  12. package/vendor/mwi/README.md +39 -18
  13. package/vendor/mwi/SKILL.md +26 -20
  14. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -2763
  15. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
  16. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
  17. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
  18. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
  19. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
  20. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
  21. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
  22. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
  23. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
  24. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
  25. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
  26. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +128 -128
  27. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
  28. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
  29. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
  30. package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
  31. package/vendor/mwi/lib/core/code-engine.js +18 -1
  32. package/vendor/mwi/lib/edit.d.ts +1 -1
  33. package/vendor/mwi/lib/edit.js +1 -1
  34. package/vendor/mwi/lib/read/service.js +8 -1
  35. package/vendor/mwi/lib/session/backup.d.ts +55 -0
  36. package/vendor/mwi/lib/session/backup.js +232 -0
  37. package/vendor/mwi/lib/session/project.d.ts +4 -1
  38. package/vendor/mwi/lib/session/project.js +13 -2
  39. package/vendor/mwi/lib/session/service.d.ts +13 -6
  40. package/vendor/mwi/lib/session/service.js +127 -36
  41. package/vendor/mwi/lib/session/workspace.d.ts +10 -0
  42. package/vendor/mwi/lib/session/workspace.js +53 -3
  43. package/vendor/mwi/lib/skill.d.ts +2 -2
  44. package/vendor/mwi/lib/skill.js +2 -2
  45. package/vendor/mwi/lib/tools/edit.d.ts +3 -3
  46. package/vendor/mwi/lib/tools/edit.js +14 -15
  47. package/vendor/mwi/lib/tools/read.js +1 -1
  48. package/vendor/mwi/lib/tools/session.js +13 -9
  49. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
  50. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
  51. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
  52. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
  53. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  54. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
  55. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
  56. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
  57. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
  58. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
  59. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  60. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  61. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
  62. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
  63. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  64. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
  65. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
  66. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
  67. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
  68. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
  69. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
  70. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  71. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
  72. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
  73. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  74. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
  75. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
  76. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
  77. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
  78. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
  79. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
  80. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  81. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
  82. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
  83. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
  84. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
  85. package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
  86. package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  87. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
  88. package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
  89. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
  90. package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
  91. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
  92. package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  93. package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  94. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
  95. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
  96. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  97. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
  98. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
  99. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
  100. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
  101. package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
  102. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
  103. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  104. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
  105. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
  106. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
  107. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
  108. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
  109. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
  110. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
  111. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
  112. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
  113. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  114. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
  115. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
  116. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
  117. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
  118. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
  119. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
  120. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
  121. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
  122. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
  123. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
  124. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
  125. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
  126. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
  127. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
  128. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
  129. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
  130. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
  131. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
  132. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
  133. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
  134. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
  135. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  136. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
  137. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
  138. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
  139. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  140. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  141. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  142. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  143. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  144. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  145. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  146. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  147. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  148. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  149. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  150. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  151. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  152. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  153. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  154. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  155. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  156. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  157. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  158. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  159. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  160. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
@@ -1,339 +1,339 @@
1
- """Drive a real MotionWorks build, and use the result to verify or revert a write.
2
-
3
- The motivation is a measured limitation rather than a design preference. Static
4
- checks can predict *some* declaration failures -- a non-``BOOL`` type on a bit
5
- address, or a dangling reference -- but not all of them: one variable delete was
6
- verified as unreferenced and still failed to compile, for reasons that remain
7
- unexplained (see docs/tier2-format-notes.md).
8
-
9
- The only trustworthy validation of a change is compiling it. So instead of
10
- predicting, this module:
11
-
12
- 1. applies a write,
13
- 2. drives a real Build/Rebuild through COM,
14
- 3. reads whether the project compiled,
15
- 4. **restores the backup automatically if it did not.**
16
-
17
- That turns an unpredictable operation into one whose failure is detected and
18
- undone, which is a stronger guarantee than prediction.
19
-
20
- The COM mechanism is the one proven in docs/tier3-com-build.md: a 32-bit shell is
21
- required, the automation server only works while the IDE is running, and a trial
22
- install's startup dialog must be dismissed first. All of that lives in
23
- ``tools/mw_build.ps1``; this module just invokes it and interprets the result.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import json
29
- import os
30
- import shutil
31
- import subprocess
32
- import sys
33
- from dataclasses import dataclass, field
34
- from pathlib import Path
35
-
36
- from .errors import MotionWorksError
37
-
38
- #: Marker the PowerShell helper puts in front of its JSON document, so the
39
- #: result can be found even when the IDE writes to the same stream.
40
- _JSON_MARKER = "MWBUILD_JSON:"
41
-
42
- #: 32-bit Windows PowerShell. Required because mwt.exe is 32-bit and its COM
43
- #: wrapper is registered only in the 32-bit registry view.
44
- POWERSHELL_32 = Path(
45
- os.environ.get("SystemRoot", r"C:\Windows")
46
- ) / "SysWOW64/WindowsPowerShell/v1.0/powershell.exe"
47
-
48
- #: The build helper lives in the repository's tools/ directory.
49
- _HELPER = Path(__file__).resolve().parents[2] / "tools" / "mw_build.ps1"
50
-
51
-
52
-
53
- class BuildUnavailable(MotionWorksError):
54
- """The build could not be driven at all.
55
-
56
- Distinct from a build that ran and failed: this means the mechanism is
57
- missing, so no conclusion about the project can be drawn.
58
- """
59
-
60
-
61
- @dataclass
62
- class BuildResult:
63
- """Outcome of one build attempt."""
64
-
65
- ok: bool = False
66
- project: str = ""
67
- loaded: bool = False
68
- compiled: bool | None = None
69
- modified: bool | None = None
70
- dlls: int = 0
71
- err: str = ""
72
- stage: str = ""
73
- error: str = ""
74
-
75
- @property
76
- def ide_would_not_load(self) -> bool:
77
- """True when the project never loaded, so no compile was attempted.
78
-
79
- This is distinct from a genuine compile failure and must not be treated
80
- as one. Observed cause: the IDE sitting at a startup prompt, in which
81
- case *every* automation call -- including ``IsProjectOpen()`` on an
82
- unmodified project -- returns "Internal error". Reporting that as "the
83
- code does not compile" would be badly misleading.
84
- """
85
- return self.error.startswith("project did not load") or self.stage == "load failed"
86
-
87
- @classmethod
88
- def from_payload(cls, payload: dict[str, object]) -> "BuildResult":
89
- return cls(
90
- ok=bool(payload.get("ok")),
91
- project=str(payload.get("project", "")),
92
- loaded=bool(payload.get("loaded")),
93
- compiled=payload.get("compiled"), # type: ignore[arg-type]
94
- modified=payload.get("modified"), # type: ignore[arg-type]
95
- dlls=int(payload.get("dlls") or 0),
96
- err=str(payload.get("err") or ""),
97
- stage=str(payload.get("stage") or ""),
98
- error=str(payload.get("error") or ""),
99
- )
100
-
101
- def summary(self) -> str:
102
- bits = [
103
- f"compiled={self.compiled}",
104
- f"pou_dlls={self.dlls}",
105
- ]
106
- if self.stage:
107
- bits.append(f"stage={self.stage}")
108
- if self.err.strip():
109
- bits.append(f"compiler_errors={self.err.strip()[:200]!r}")
110
- if self.error:
111
- bits.append(f"error={self.error}")
112
- return " ".join(bits)
113
-
114
-
115
- def build_available() -> tuple[bool, str]:
116
- """Whether a build can be driven here, with the reason if not."""
117
- if sys.platform != "win32":
118
- return False, "not running on Windows"
119
- if not POWERSHELL_32.is_file():
120
- return False, f"32-bit PowerShell not found at {POWERSHELL_32}"
121
- if not _HELPER.is_file():
122
- return False, f"build helper not found at {_HELPER}"
123
- exe = Path(
124
- r"C:\Program Files (x86)\Yaskawa\MotionWorks IEC 3 Pro\Mwt.exe"
125
- )
126
- if not exe.is_file():
127
- return False, f"MotionWorks not found at {exe}"
128
- return True, "ok"
129
-
130
-
131
- def _ps_quote(value: str) -> str:
132
- """Quote a literal for PowerShell -Command, including apostrophes in paths."""
133
- return "'" + value.replace("'", "''") + "'"
134
-
135
-
136
- def run_build(project_mwt: Path, timeout: int = 600) -> BuildResult:
137
- """Drive a Build/Rebuild of ``project_mwt`` and return the outcome.
138
-
139
- Raises :class:`BuildUnavailable` when the mechanism is missing, so callers
140
- can distinguish "could not try" from "tried and it failed".
141
- """
142
- available, reason = build_available()
143
- if not available:
144
- raise BuildUnavailable(f"cannot drive a build: {reason}")
145
-
146
- # A build opens the project. The wrapper's embedded directory is what loads,
147
- # so an outside path here is the same wrong-project failure as an IDE open.
148
- from .mwt_bind import embedded_paths
149
- from .staging import StagingRefused, assert_proven
150
-
151
- project_mwt = Path(project_mwt)
152
- assert_proven(project_mwt)
153
- paths = embedded_paths(project_mwt)
154
- if not paths:
155
- raise StagingRefused("REFUSED: wrapper has no verified project binding")
156
- for embedded in paths:
157
- if Path(embedded).resolve() != project_mwt.with_suffix("").resolve():
158
- raise StagingRefused(
159
- f"REFUSED: '{project_mwt}' still points at '{embedded}', which is outside "
160
- "the staging root. Stage it with mw_ide_stage before building it."
161
- )
162
-
163
- # Keep the build in the workspace; quote paths rather than relocating them.
164
- staged = project_mwt
165
- # Invoked with -ExecutionPolicy Bypass AND -Command.
166
- #
167
- # Both are needed, which was established by testing each form:
168
- # * with -File, PowerShell ignores the policy flag when launched from a
169
- # non-shell parent and refuses the script with "is not digitally signed";
170
- # * with -Command but no policy flag, the same refusal occurs;
171
- # * -Command *with* the policy flag runs it.
172
- # The helper reports its own outcome in JSON, so the process exit code is not
173
- # used to decide success.
174
- command = [
175
- str(POWERSHELL_32),
176
- "-NoProfile",
177
- "-NonInteractive",
178
- "-ExecutionPolicy", "Bypass",
179
- "-Command",
180
- "& " + _ps_quote(str(_HELPER)) + " -Project " + _ps_quote(str(staged)),
181
- ]
182
- try:
183
- completed = subprocess.run(
184
- command,
185
- capture_output=True,
186
- text=True,
187
- timeout=timeout,
188
- encoding="utf-8",
189
- errors="replace",
190
- )
191
- except subprocess.TimeoutExpired as exc:
192
- raise BuildUnavailable(
193
- f"build did not finish within {timeout}s"
194
- ) from exc
195
-
196
- payload = _extract_payload(completed.stdout) or _extract_payload(completed.stderr)
197
- if payload is None:
198
- tail = (completed.stdout or "")[-400:]
199
- raise BuildUnavailable(
200
- f"build helper produced no result (exit {completed.returncode}). "
201
- f"Last output: {tail!r}"
202
- )
203
- return BuildResult.from_payload(payload)
204
-
205
-
206
- def _extract_payload(text: str) -> dict[str, object] | None:
207
- """Find and decode the JSON document the helper emits."""
208
- if not text:
209
- return None
210
- for line in reversed(text.splitlines()):
211
- index = line.find(_JSON_MARKER)
212
- if index < 0:
213
- continue
214
- raw = line[index + len(_JSON_MARKER):].strip()
215
- try:
216
- payload = json.loads(raw)
217
- except json.JSONDecodeError:
218
- continue
219
- if isinstance(payload, dict):
220
- return payload
221
- return None
222
-
223
-
224
- @dataclass
225
- class VerifiedWrite:
226
- """A write plus the build verdict, and whether it had to be undone."""
227
-
228
- written: dict[str, object] = field(default_factory=dict)
229
- build: BuildResult | None = None
230
- reverted: bool = False
231
- revert_paths: list[str] = field(default_factory=list)
232
- notes: list[str] = field(default_factory=list)
233
-
234
- @property
235
- def ok(self) -> bool:
236
- if self.build is None:
237
- return False
238
- return bool(self.build.ok) and not self.reverted
239
-
240
-
241
- def apply_and_verify(
242
- plan: object,
243
- project_root: Path,
244
- project_mwt: Path | None = None,
245
- revert_on_failure: bool = True,
246
- timeout: int = 600,
247
- ) -> VerifiedWrite:
248
- """Apply ``plan``, build the project, and revert if it did not compile.
249
-
250
- ``plan`` is a :class:`motionworks_iec_mcp.writer.WritePlan`. Typed loosely to
251
- avoid an import cycle.
252
- """
253
- from . import writer as W
254
-
255
- outcome = VerifiedWrite()
256
- result = W.apply_plan(plan, project_root) # type: ignore[arg-type]
257
- outcome.written = result
258
- backups = [Path(p) for p in result.get("backups", [])] # type: ignore[union-attr]
259
-
260
- if not result.get("applied"):
261
- outcome.notes.append("nothing was written, so nothing to verify")
262
- return outcome
263
-
264
- if project_mwt is None:
265
- project_mwt = _default_mwt(project_root)
266
- if project_mwt is None or not project_mwt.is_file():
267
- outcome.notes.append(
268
- "no .mwt found, so the write could not be verified by building"
269
- )
270
- return outcome
271
-
272
- try:
273
- outcome.build = run_build(project_mwt, timeout=timeout)
274
- except BuildUnavailable as exc:
275
- outcome.notes.append(f"build unavailable: {exc}")
276
- return outcome
277
-
278
- if outcome.build.ok:
279
- return outcome
280
-
281
- # A project that never loaded is not a compile failure, and reverting because
282
- # of it would destroy a perfectly good change on the strength of an
283
- # environmental problem. Report it and leave the write in place.
284
- if outcome.build.ide_would_not_load:
285
- outcome.notes.append(
286
- "the IDE did not load the project, so no compile was attempted. "
287
- "This is an environment problem, NOT a compile failure; the write "
288
- "has been left in place. Check the MotionWorks window for a startup "
289
- "or licensing prompt."
290
- )
291
- return outcome
292
-
293
- outcome.notes.append(f"build failed: {outcome.build.summary()}")
294
- if not revert_on_failure:
295
- outcome.notes.append("not reverting")
296
- return outcome
297
-
298
- pairs = result.get("backup_pairs") or []
299
- if not pairs:
300
- outcome.notes.append("no backup recorded, so the change could not be reverted")
301
- return outcome
302
-
303
- outcome.revert_paths = _restore(
304
- [{"original": p["original"], "backup": p["backup"]} for p in pairs] # type: ignore[index]
305
- )
306
- outcome.reverted = True
307
- outcome.notes.append(
308
- f"reverted {len(outcome.revert_paths)} file(s) from backup"
309
- )
310
- return outcome
311
-
312
-
313
- def _default_mwt(project_root: Path) -> Path | None:
314
- """The ``.mwt`` beside an expanded project directory."""
315
- candidate = project_root.with_suffix(".mwt")
316
- if candidate.is_file():
317
- return candidate
318
- sibling = project_root.parent / f"{project_root.name}.mwt"
319
- return sibling if sibling.is_file() else None
320
-
321
-
322
- def _restore(pairs: list[dict[str, str]]) -> list[str]:
323
- """Copy backups back over the exact files they came from.
324
-
325
- ``pairs`` entries are ``{"original": <path>, "backup": <path>}``. The
326
- original path is supplied rather than inferred: an earlier version searched
327
- the backup path for the project name, which finds the wrong position when the
328
- name occurs twice in the path and would restore to the wrong file.
329
- """
330
- restored: list[str] = []
331
- for pair in pairs:
332
- original = Path(pair["original"])
333
- backup = Path(pair["backup"])
334
- if not backup.is_file():
335
- continue
336
- original.parent.mkdir(parents=True, exist_ok=True)
337
- shutil.copy2(backup, original)
338
- restored.append(str(original))
339
- return restored
1
+ """Drive a real MotionWorks build, and use the result to verify or revert a write.
2
+
3
+ The motivation is a measured limitation rather than a design preference. Static
4
+ checks can predict *some* declaration failures -- a non-``BOOL`` type on a bit
5
+ address, or a dangling reference -- but not all of them: one variable delete was
6
+ verified as unreferenced and still failed to compile, for reasons that remain
7
+ unexplained (see docs/tier2-format-notes.md).
8
+
9
+ The only trustworthy validation of a change is compiling it. So instead of
10
+ predicting, this module:
11
+
12
+ 1. applies a write,
13
+ 2. drives a real Build/Rebuild through COM,
14
+ 3. reads whether the project compiled,
15
+ 4. **restores the backup automatically if it did not.**
16
+
17
+ That turns an unpredictable operation into one whose failure is detected and
18
+ undone, which is a stronger guarantee than prediction.
19
+
20
+ The COM mechanism is the one proven in docs/tier3-com-build.md: a 32-bit shell is
21
+ required, the automation server only works while the IDE is running, and a trial
22
+ install's startup dialog must be dismissed first. All of that lives in
23
+ ``tools/mw_build.ps1``; this module just invokes it and interprets the result.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import shutil
31
+ import subprocess
32
+ import sys
33
+ from dataclasses import dataclass, field
34
+ from pathlib import Path
35
+
36
+ from .errors import MotionWorksError
37
+
38
+ #: Marker the PowerShell helper puts in front of its JSON document, so the
39
+ #: result can be found even when the IDE writes to the same stream.
40
+ _JSON_MARKER = "MWBUILD_JSON:"
41
+
42
+ #: 32-bit Windows PowerShell. Required because mwt.exe is 32-bit and its COM
43
+ #: wrapper is registered only in the 32-bit registry view.
44
+ POWERSHELL_32 = Path(
45
+ os.environ.get("SystemRoot", r"C:\Windows")
46
+ ) / "SysWOW64/WindowsPowerShell/v1.0/powershell.exe"
47
+
48
+ #: The build helper lives in the repository's tools/ directory.
49
+ _HELPER = Path(__file__).resolve().parents[2] / "tools" / "mw_build.ps1"
50
+
51
+
52
+
53
+ class BuildUnavailable(MotionWorksError):
54
+ """The build could not be driven at all.
55
+
56
+ Distinct from a build that ran and failed: this means the mechanism is
57
+ missing, so no conclusion about the project can be drawn.
58
+ """
59
+
60
+
61
+ @dataclass
62
+ class BuildResult:
63
+ """Outcome of one build attempt."""
64
+
65
+ ok: bool = False
66
+ project: str = ""
67
+ loaded: bool = False
68
+ compiled: bool | None = None
69
+ modified: bool | None = None
70
+ dlls: int = 0
71
+ err: str = ""
72
+ stage: str = ""
73
+ error: str = ""
74
+
75
+ @property
76
+ def ide_would_not_load(self) -> bool:
77
+ """True when the project never loaded, so no compile was attempted.
78
+
79
+ This is distinct from a genuine compile failure and must not be treated
80
+ as one. Observed cause: the IDE sitting at a startup prompt, in which
81
+ case *every* automation call -- including ``IsProjectOpen()`` on an
82
+ unmodified project -- returns "Internal error". Reporting that as "the
83
+ code does not compile" would be badly misleading.
84
+ """
85
+ return self.error.startswith("project did not load") or self.stage == "load failed"
86
+
87
+ @classmethod
88
+ def from_payload(cls, payload: dict[str, object]) -> "BuildResult":
89
+ return cls(
90
+ ok=bool(payload.get("ok")),
91
+ project=str(payload.get("project", "")),
92
+ loaded=bool(payload.get("loaded")),
93
+ compiled=payload.get("compiled"), # type: ignore[arg-type]
94
+ modified=payload.get("modified"), # type: ignore[arg-type]
95
+ dlls=int(payload.get("dlls") or 0),
96
+ err=str(payload.get("err") or ""),
97
+ stage=str(payload.get("stage") or ""),
98
+ error=str(payload.get("error") or ""),
99
+ )
100
+
101
+ def summary(self) -> str:
102
+ bits = [
103
+ f"compiled={self.compiled}",
104
+ f"pou_dlls={self.dlls}",
105
+ ]
106
+ if self.stage:
107
+ bits.append(f"stage={self.stage}")
108
+ if self.err.strip():
109
+ bits.append(f"compiler_errors={self.err.strip()[:200]!r}")
110
+ if self.error:
111
+ bits.append(f"error={self.error}")
112
+ return " ".join(bits)
113
+
114
+
115
+ def build_available() -> tuple[bool, str]:
116
+ """Whether a build can be driven here, with the reason if not."""
117
+ if sys.platform != "win32":
118
+ return False, "not running on Windows"
119
+ if not POWERSHELL_32.is_file():
120
+ return False, f"32-bit PowerShell not found at {POWERSHELL_32}"
121
+ if not _HELPER.is_file():
122
+ return False, f"build helper not found at {_HELPER}"
123
+ exe = Path(
124
+ r"C:\Program Files (x86)\Yaskawa\MotionWorks IEC 3 Pro\Mwt.exe"
125
+ )
126
+ if not exe.is_file():
127
+ return False, f"MotionWorks not found at {exe}"
128
+ return True, "ok"
129
+
130
+
131
+ def _ps_quote(value: str) -> str:
132
+ """Quote a literal for PowerShell -Command, including apostrophes in paths."""
133
+ return "'" + value.replace("'", "''") + "'"
134
+
135
+
136
+ def run_build(project_mwt: Path, timeout: int = 600) -> BuildResult:
137
+ """Drive a Build/Rebuild of ``project_mwt`` and return the outcome.
138
+
139
+ Raises :class:`BuildUnavailable` when the mechanism is missing, so callers
140
+ can distinguish "could not try" from "tried and it failed".
141
+ """
142
+ available, reason = build_available()
143
+ if not available:
144
+ raise BuildUnavailable(f"cannot drive a build: {reason}")
145
+
146
+ # A build opens the project. The wrapper's embedded directory is what loads,
147
+ # so an outside path here is the same wrong-project failure as an IDE open.
148
+ from .mwt_bind import embedded_paths
149
+ from .staging import StagingRefused, assert_proven
150
+
151
+ project_mwt = Path(project_mwt)
152
+ assert_proven(project_mwt)
153
+ paths = embedded_paths(project_mwt)
154
+ if not paths:
155
+ raise StagingRefused("REFUSED: wrapper has no verified project binding")
156
+ for embedded in paths:
157
+ if Path(embedded).resolve() != project_mwt.with_suffix("").resolve():
158
+ raise StagingRefused(
159
+ f"REFUSED: '{project_mwt}' still points at '{embedded}', which is outside "
160
+ "the staging root. Stage it with mw_ide_stage before building it."
161
+ )
162
+
163
+ # Keep the build in the workspace; quote paths rather than relocating them.
164
+ staged = project_mwt
165
+ # Invoked with -ExecutionPolicy Bypass AND -Command.
166
+ #
167
+ # Both are needed, which was established by testing each form:
168
+ # * with -File, PowerShell ignores the policy flag when launched from a
169
+ # non-shell parent and refuses the script with "is not digitally signed";
170
+ # * with -Command but no policy flag, the same refusal occurs;
171
+ # * -Command *with* the policy flag runs it.
172
+ # The helper reports its own outcome in JSON, so the process exit code is not
173
+ # used to decide success.
174
+ command = [
175
+ str(POWERSHELL_32),
176
+ "-NoProfile",
177
+ "-NonInteractive",
178
+ "-ExecutionPolicy", "Bypass",
179
+ "-Command",
180
+ "& " + _ps_quote(str(_HELPER)) + " -Project " + _ps_quote(str(staged)),
181
+ ]
182
+ try:
183
+ completed = subprocess.run(
184
+ command,
185
+ capture_output=True,
186
+ text=True,
187
+ timeout=timeout,
188
+ encoding="utf-8",
189
+ errors="replace",
190
+ )
191
+ except subprocess.TimeoutExpired as exc:
192
+ raise BuildUnavailable(
193
+ f"build did not finish within {timeout}s"
194
+ ) from exc
195
+
196
+ payload = _extract_payload(completed.stdout) or _extract_payload(completed.stderr)
197
+ if payload is None:
198
+ tail = (completed.stdout or "")[-400:]
199
+ raise BuildUnavailable(
200
+ f"build helper produced no result (exit {completed.returncode}). "
201
+ f"Last output: {tail!r}"
202
+ )
203
+ return BuildResult.from_payload(payload)
204
+
205
+
206
+ def _extract_payload(text: str) -> dict[str, object] | None:
207
+ """Find and decode the JSON document the helper emits."""
208
+ if not text:
209
+ return None
210
+ for line in reversed(text.splitlines()):
211
+ index = line.find(_JSON_MARKER)
212
+ if index < 0:
213
+ continue
214
+ raw = line[index + len(_JSON_MARKER):].strip()
215
+ try:
216
+ payload = json.loads(raw)
217
+ except json.JSONDecodeError:
218
+ continue
219
+ if isinstance(payload, dict):
220
+ return payload
221
+ return None
222
+
223
+
224
+ @dataclass
225
+ class VerifiedWrite:
226
+ """A write plus the build verdict, and whether it had to be undone."""
227
+
228
+ written: dict[str, object] = field(default_factory=dict)
229
+ build: BuildResult | None = None
230
+ reverted: bool = False
231
+ revert_paths: list[str] = field(default_factory=list)
232
+ notes: list[str] = field(default_factory=list)
233
+
234
+ @property
235
+ def ok(self) -> bool:
236
+ if self.build is None:
237
+ return False
238
+ return bool(self.build.ok) and not self.reverted
239
+
240
+
241
+ def apply_and_verify(
242
+ plan: object,
243
+ project_root: Path,
244
+ project_mwt: Path | None = None,
245
+ revert_on_failure: bool = True,
246
+ timeout: int = 600,
247
+ ) -> VerifiedWrite:
248
+ """Apply ``plan``, build the project, and revert if it did not compile.
249
+
250
+ ``plan`` is a :class:`motionworks_iec_mcp.writer.WritePlan`. Typed loosely to
251
+ avoid an import cycle.
252
+ """
253
+ from . import writer as W
254
+
255
+ outcome = VerifiedWrite()
256
+ result = W.apply_plan(plan, project_root) # type: ignore[arg-type]
257
+ outcome.written = result
258
+ backups = [Path(p) for p in result.get("backups", [])] # type: ignore[union-attr]
259
+
260
+ if not result.get("applied"):
261
+ outcome.notes.append("nothing was written, so nothing to verify")
262
+ return outcome
263
+
264
+ if project_mwt is None:
265
+ project_mwt = _default_mwt(project_root)
266
+ if project_mwt is None or not project_mwt.is_file():
267
+ outcome.notes.append(
268
+ "no .mwt found, so the write could not be verified by building"
269
+ )
270
+ return outcome
271
+
272
+ try:
273
+ outcome.build = run_build(project_mwt, timeout=timeout)
274
+ except BuildUnavailable as exc:
275
+ outcome.notes.append(f"build unavailable: {exc}")
276
+ return outcome
277
+
278
+ if outcome.build.ok:
279
+ return outcome
280
+
281
+ # A project that never loaded is not a compile failure, and reverting because
282
+ # of it would destroy a perfectly good change on the strength of an
283
+ # environmental problem. Report it and leave the write in place.
284
+ if outcome.build.ide_would_not_load:
285
+ outcome.notes.append(
286
+ "the IDE did not load the project, so no compile was attempted. "
287
+ "This is an environment problem, NOT a compile failure; the write "
288
+ "has been left in place. Check the MotionWorks window for a startup "
289
+ "or licensing prompt."
290
+ )
291
+ return outcome
292
+
293
+ outcome.notes.append(f"build failed: {outcome.build.summary()}")
294
+ if not revert_on_failure:
295
+ outcome.notes.append("not reverting")
296
+ return outcome
297
+
298
+ pairs = result.get("backup_pairs") or []
299
+ if not pairs:
300
+ outcome.notes.append("no backup recorded, so the change could not be reverted")
301
+ return outcome
302
+
303
+ outcome.revert_paths = _restore(
304
+ [{"original": p["original"], "backup": p["backup"]} for p in pairs] # type: ignore[index]
305
+ )
306
+ outcome.reverted = True
307
+ outcome.notes.append(
308
+ f"reverted {len(outcome.revert_paths)} file(s) from backup"
309
+ )
310
+ return outcome
311
+
312
+
313
+ def _default_mwt(project_root: Path) -> Path | None:
314
+ """The ``.mwt`` beside an expanded project directory."""
315
+ candidate = project_root.with_suffix(".mwt")
316
+ if candidate.is_file():
317
+ return candidate
318
+ sibling = project_root.parent / f"{project_root.name}.mwt"
319
+ return sibling if sibling.is_file() else None
320
+
321
+
322
+ def _restore(pairs: list[dict[str, str]]) -> list[str]:
323
+ """Copy backups back over the exact files they came from.
324
+
325
+ ``pairs`` entries are ``{"original": <path>, "backup": <path>}``. The
326
+ original path is supplied rather than inferred: an earlier version searched
327
+ the backup path for the project name, which finds the wrong position when the
328
+ name occurs twice in the path and would restore to the wrong file.
329
+ """
330
+ restored: list[str] = []
331
+ for pair in pairs:
332
+ original = Path(pair["original"])
333
+ backup = Path(pair["backup"])
334
+ if not backup.is_file():
335
+ continue
336
+ original.parent.mkdir(parents=True, exist_ok=True)
337
+ shutil.copy2(backup, original)
338
+ restored.append(str(original))
339
+ return restored