Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 89 additions & 9 deletions .github/workflows/PluginSkeletonGenerator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,89 @@ jobs:
id: set-matrix
shell: python
run: |
import json, os
import json
import os

def mk_answers(plugin_name, oop, custom_cfg, pre=None):
"""
Prompt order (interactive mode):
1) plugin name
2) output dir (Enter = current directory)
3) out-of-process (Y/N)
4) custom config (Y/N)
5+) interface path(s) ... then blank line
next) subsystems (Y/N)
if subsystems=Y:
Preconditions list entries ... blank
Terminations list entries ... blank
Controls list entries ... blank
next) (optional, per header with >1 root) pick indices or Enter for ALL
next) include location per header (Enter for default)
"""
if pre is None:
pre = {"pre": [], "term": [], "ctrl": []}

answers = [
plugin_name,
"", # output dir: Enter => current directory
oop,
custom_cfg,
"__INTERFACES_H__", # interface path injected later
"", # finish interface list
("Y" if (pre["pre"] or pre["term"] or pre["ctrl"]) else "N"),
]

if answers[-1] == "Y":
# Preconditions list + terminator
answers += list(pre["pre"]) + [""]
# Terminations list + terminator
answers += list(pre["term"]) + [""]
# Controls list + terminator
answers += list(pre["ctrl"]) + [""]

# After subsystems collection, generator may ask:
# - (optional) interface selection (Enter=ALL) for multi-root headers
# - include location (Enter=default 'interfaces')
answers += ["", ""]

return answers

variants = [
{ "name": "InProcess", "header": "IMath.h", "answers": ["InProcess", "N", "N", "__INTERFACES_H__", "", "N", ""] },
{ "name": "OutOfProcess", "header": "IBrowser.h", "answers": ["OutOfProcess", "Y", "N", "__INTERFACES_H__", "", "", "N", ""] },
{ "name": "InProcessConfig", "header": "IPower.h", "answers": ["InProcessConfig", "N", "Y", "__INTERFACES_H__", "", "N", ""] },
{ "name": "OutOfProcessConfig", "header": "INetworkControl.h", "answers": ["OutOfProcessConfig", "Y", "Y", "__INTERFACES_H__", "", "N", ""] },
{ "name": "InProcessPreconditions", "header": "ITimeSync.h", "answers": ["InProcessPreconditions", "N", "N", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] },
{ "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", "answers": ["OutOfProcessPreconditions", "Y", "N", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] },
{ "name": "InProcessConfigPreconditions", "header": "IDictionary.h", "answers": ["InProcessConfigPreconditions", "N", "Y", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] },
{ "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", "answers": ["OutOfProcessConfigPreconditions", "Y", "Y", "__INTERFACES_H__", "", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] }
{ "name": "InProcess", "header": "IMath.h",
"answers": mk_answers("InProcess", "N", "N") },

{ "name": "OutOfProcess", "header": "IBrowser.h",
"answers": mk_answers("OutOfProcess", "Y", "N") },

{ "name": "InProcessConfig", "header": "IPower.h",
"answers": mk_answers("InProcessConfig", "N", "Y") },

{ "name": "OutOfProcessConfig", "header": "INetworkControl.h",
"answers": mk_answers("OutOfProcessConfig", "Y", "Y") },

{ "name": "InProcessPreconditions", "header": "ITimeSync.h",
"answers": mk_answers(
"InProcessPreconditions", "N", "N",
pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]}
) },

Comment thread
nxtum marked this conversation as resolved.
{ "name": "OutOfProcessPreconditions", "header": "IMessageControl.h",
"answers": mk_answers(
"OutOfProcessPreconditions", "Y", "N",
pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]}
) },

{ "name": "InProcessConfigPreconditions", "header": "IVolumeControl.h",
"answers": mk_answers(
"InProcessConfigPreconditions", "N", "Y",
pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]}
) },

{ "name": "OutOfProcessConfigPreconditions", "header": "IWifiControl.h",
"answers": mk_answers(
"OutOfProcessConfigPreconditions", "Y", "Y",
pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]}
) },
]

include = []
Expand Down Expand Up @@ -430,6 +502,14 @@ jobs:
Path("diffs/combined_ai.diff").write_text(text, encoding="utf-8")
PY

- name: Upload combined diff artifact
if: github.event_name == 'pull_request' && steps.build_site.outputs.has_diff == 'true'
uses: actions/upload-artifact@v4
with:
name: generated-plugin-diff
path: diffs/combined.diff
retention-days: 14

- name: AI summary (GitHub Models)
if: github.event_name == 'pull_request' && steps.build_site.outputs.has_diff == 'true'
id: ai
Expand Down
44 changes: 35 additions & 9 deletions PluginSkeletonGenerator/CombinedPlugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ fi
TARGET_DIR="$TARGET_ROOT_ABS/CombinedPlugins"
mkdir -p "$TARGET_DIR"

# Optional subsystem lists can be provided via environment variables:
# PSG_PRECONDITIONS (newline-separated)
# PSG_TERMINATIONS (newline-separated)
# PSG_CONTROLS (newline-separated)
# If any of these are set (non-empty), the script answers 'Y' to the subsystems prompt and
# feeds the lists (each terminated by a blank line). Otherwise it answers 'N'.
_psg_emit_list() {
local content="$1"
if [ -n "$content" ]; then
# Print as-is, ensure it ends with a newline
printf '%s\n' "$content"
fi
# terminating blank line
printf '\n'
}

# Work inside CombinedPlugins in a subshell so the parent shell CWD is unchanged
(
cd "$TARGET_DIR"
Expand All @@ -56,15 +72,25 @@ mkdir -p "$TARGET_DIR"
for i in $(seq 1 "$COUNT"); do
plugin="Plugin$i"

# Answers:
# 1) $plugin
# 2) N
# 3) N
# 4) $IFACE_ABS
# 5) <Enter>
# 6) N
# 7) <Enter>
printf '%s\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py"
if [ -n "${PSG_PRECONDITIONS:-}" ] || [ -n "${PSG_TERMINATIONS:-}" ] || [ -n "${PSG_CONTROLS:-}" ]; then
# Subsystems enabled
{
printf '%s\n' "$plugin" # name
printf '\n' # output dir (default)
printf 'N\n' # out-of-process
printf 'N\n' # custom config
printf '%s\n' "$IFACE_ABS"# interface path
printf '\n' # finish interface list
printf 'Y\n' # subsystems enabled
_psg_emit_list "${PSG_PRECONDITIONS:-}" # Preconditions
_psg_emit_list "${PSG_TERMINATIONS:-}" # Terminations
_psg_emit_list "${PSG_CONTROLS:-}" # Controls
printf '\n' # include location (default)
Comment thread
nxtum marked this conversation as resolved.
} | python3 "$START_DIR/PluginSkeletonGenerator.py"
else
# No subsystems
printf '%s\n\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py"
Comment thread
nxtum marked this conversation as resolved.
fi
done

# Create CMakeLists.txt with one add_subdirectory line per plugin
Expand Down
22 changes: 21 additions & 1 deletion PluginSkeletonGenerator/core/GeneratorCoordinator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
'''
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2026 Metrological
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
'''

from core.PluginBlueprint import PluginBlueprint
from data.FileData import HeaderData, SourceData, CMakeData, JSONData, ConfData
from generators.PluginRepositoryGenerator import PluginRepositoryGenerator
Expand All @@ -12,7 +31,7 @@ class GenerationTask:

class GeneratorCoordinator:
def __init__(self, name, out_of_process, configuration, parsed_data, header_lookup, locations,
preconditions=None, terminations=None, controls=None):
preconditions=None, terminations=None, controls=None, output_dir=None):
self.m_blueprint = PluginBlueprint(
name=name,
out_of_process=out_of_process,
Expand All @@ -23,6 +42,7 @@ def __init__(self, name, out_of_process, configuration, parsed_data, header_look
preconditions=preconditions,
terminations=terminations,
controls=controls,
output_dir=output_dir,
)

def generateAll(self):
Expand Down
19 changes: 19 additions & 0 deletions PluginSkeletonGenerator/core/GlobalVariables.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
'''
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2026 Metrological
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
'''

import os

def findRepoRoot(filename="PluginSkeletonGenerator.py"):
Expand Down
53 changes: 48 additions & 5 deletions PluginSkeletonGenerator/core/PluginBlueprint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
from typing import Dict, List, Tuple
'''
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2026 Metrological
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
'''

from typing import Dict, List, Tuple

class PluginBlueprint:
class ParsedPluginInfo:
Expand All @@ -10,6 +28,7 @@ def __init__(self, parsed_data: Dict[str, Tuple]):
self._jsonrpc_interfaces: List[str] = []
self._notification_interfaces: List[str] = []
self._notification_entries: List[Tuple[str, object]] = []
self._event_notification_entries: List[Tuple[str, object]] = []

self.processInterfaces(parsed_data)

Expand All @@ -25,11 +44,17 @@ def processInterfaces(self, parsed_data: Dict[str, Tuple]):
json_name = f"J{root_name[1:]}" if root_name.startswith("I") else f"J{root_name}"
self._jsonrpc_interfaces.append(json_name)

if self.hasNotification(cls_data):
self._notification_interfaces.append(root_name)

self.gatherNotificationEntries(full_name, cls_data)

all_roots: List[str] = []
seen = set()
for fq, _ in self._notification_entries:
root = fq.split("::")[-2]
if root not in seen:
seen.add(root)
all_roots.append(root)
self._notification_interfaces = all_roots
Comment thread
nxtum marked this conversation as resolved.

@staticmethod
def extractRootName(full_name: str) -> str:
return full_name.split("::")[-1]
Expand All @@ -53,6 +78,9 @@ def gatherNotificationEntries(self, full_name: str, cls_data):
if "Notification" in current.m_name:
fq = f"{exchange_ns}::{iface}::{current.m_name}"
self._notification_entries.append((fq, current))
# Only mark the notification struct itself is tagged @event?
Comment thread
nxtum marked this conversation as resolved.
Comment thread
nxtum marked this conversation as resolved.
if "event" in getattr(current, "m_tags", []):
self._event_notification_entries.append((fq, current))
for child in current.m_children.values():
stack.append((child, iface))

Expand All @@ -79,6 +107,10 @@ def file_interface_map(self) -> Dict[str, str]:
def notification_entries(self) -> List[Tuple[str, object]]:
return self._notification_entries

@property
def event_notification_entries(self) -> List[Tuple[str, object]]:
return self._event_notification_entries

def __init__(self,
name,
out_of_process,
Expand All @@ -88,13 +120,15 @@ def __init__(self,
locations,
preconditions=None,
terminations=None,
controls=None):
controls=None,
output_dir=None):
self._name = name
self._out_of_process = out_of_process
self._configuration = configuration
self._parsed_data = parsed_data
self._header_lookup = header_lookup
self._locations = locations
self._output_dir = output_dir

preconditions = preconditions or []
terminations = terminations or []
Expand All @@ -111,6 +145,7 @@ def __init__(self,
self._notification_interfaces = parsed_info.notification_interfaces
self._file_interface_map = parsed_info.file_interface_map
self._notification_entries = parsed_info.collectNotificationEntries()
self._event_notification_entries = parsed_info.event_notification_entries

@property
def name(self) -> str:
Expand Down Expand Up @@ -156,6 +191,10 @@ def file_interface_map(self) -> Dict[str, str]:
def notification_entries(self) -> List[Tuple[str, object]]:
return self._notification_entries

@property
def event_notification_entries(self) -> List[Tuple[str, object]]:
return self._event_notification_entries

@property
def preconditions(self) -> List[str]:
return self._PRECONDITIONS
Expand All @@ -168,5 +207,9 @@ def terminations(self) -> List[str]:
def controls(self) -> List[str]:
return self._CONTROLS

@property
def output_dir(self) -> str:
return self._output_dir

def isJsonRpcPlugin(self) -> bool:
return any("json" in cls_data.m_tags for cls_data, _ in self._parsed_data.values())
Loading
Loading