diff --git a/gui/wxpython/xml/module_tree.xml b/gui/wxpython/xml/module_tree.xml index edc2e2ae6c5..45b06cf71fb 100644 --- a/gui/wxpython/xml/module_tree.xml +++ b/gui/wxpython/xml/module_tree.xml @@ -13,6 +13,7 @@ + diff --git a/gui/wxpython/xml/toolboxes.xml b/gui/wxpython/xml/toolboxes.xml index 34bcbb0de95..9eff9131984 100644 --- a/gui/wxpython/xml/toolboxes.xml +++ b/gui/wxpython/xml/toolboxes.xml @@ -27,15 +27,8 @@ - - - - - - - @@ -70,6 +63,9 @@ + + + @@ -166,6 +162,25 @@ + + + + + + + + + + + + + + + + + + + @@ -191,6 +206,18 @@ + + + + + + + + + + + + @@ -377,6 +404,12 @@ + + + + + + @@ -468,6 +501,9 @@ + + + @@ -485,6 +521,9 @@ + + + @@ -522,6 +561,14 @@ + + + + + + + + @@ -569,6 +616,9 @@ + + + @@ -617,6 +667,10 @@ + + + + @@ -637,6 +691,10 @@ + + + + @@ -694,6 +752,13 @@ + + + + + + + @@ -719,6 +784,9 @@ + + + @@ -1143,6 +1211,15 @@ + + + + + + + + + @@ -1189,6 +1266,15 @@ + + + + + + + + + @@ -1271,6 +1357,9 @@ + + + @@ -1433,6 +1522,12 @@ + + + + + + @@ -1516,6 +1611,12 @@ + + + + + + @@ -1707,6 +1808,12 @@ + + + + + + @@ -1796,6 +1903,9 @@ + + + @@ -1847,6 +1957,12 @@ + + + + + + @@ -1860,9 +1976,18 @@ + + + + + + + + + @@ -1905,6 +2030,13 @@ + + + + + + + @@ -1988,6 +2120,9 @@ + + + @@ -2006,6 +2141,13 @@ + + + + + + + @@ -2055,4 +2197,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/utils/find_missing_toolboxes.py b/utils/find_missing_toolboxes.py new file mode 100644 index 00000000000..d968472d939 --- /dev/null +++ b/utils/find_missing_toolboxes.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +""" +find_missing_toolboxes.py -- Report tools present in the source tree +but absent from the GUI toolbox configuration. + +A directory is treated as a tool when its name matches the GRASS naming +convention (prefix.name) and it contains either a Python entry point +(.py) or a C entry point (main.c). + +Tools already accessible in the GUI are collected from three sources: + - entries in toolboxes.xml + - entries in wxgui_items.xml + - entries in wxgui_items.xml + +Usage: + +python utils/find_missing_toolboxes.py + +@author +""" + +import re +import sys +from pathlib import Path +from xml.etree import ElementTree as ET + +# --------------------------------------------------------------------------- +# Tools intentionally absent from the GUI menu. +# d.* Display tools are driven from the map display, not the module menu. +# g.* Internal plumbing tools not meant for direct user interaction. +# --------------------------------------------------------------------------- +EXCLUDED = { + "d.*", + "g.cairocomp", + "g.dirseps", + "g.filename", + "g.findetc", + "g.findfile", + "g.gui", + "g.html2man", + "g.message", + "g.mkfontcap", + "g.parser", + "g.pnmcomp", + "g.ppmtopng", + "g.search.modules", + "g.setproj", + "g.tempfile", + "g.version", +} + +TOOL_PATTERN = re.compile(r"^[a-z][a-z0-9]*\.[a-z][a-z0-9._]*$") + + +def find_source_tools(root: Path) -> set[str]: + """Return names of all tools found in the source tree. + + A directory qualifies when its name matches the GRASS tool naming + convention and it contains a Python entry point or a C main.c. + Searches up to three directory levels deep to catch tools nested + inside parent directories (e.g. raster/r.sim/r.sim.water). + """ + tools = set() + for depth in range(2, 5): # mindepth 2, maxdepth 4 + pattern = "/".join(["*"] * depth) + for candidate in root.glob(pattern): + if not candidate.is_dir(): + continue + if candidate.parts[len(root.parts)] == "doc": + continue + if not TOOL_PATTERN.match(candidate.name): + continue + if (candidate / f"{candidate.name}.py").exists() or ( + candidate / "main.c" + ).exists(): + tools.add(candidate.name) + return tools + + +def find_registered_tools(toolboxes: Path, wxgui_items: Path) -> set[str]: + """Return tool names already accessible somewhere in the GUI.""" + registered = set() + + tree = ET.parse(toolboxes) + for el in tree.iter("module-item"): + name = el.get("name") + if name: + registered.add(name) + + tree = ET.parse(wxgui_items) + for el in tree.iter("related-module"): + if el.text: + registered.add(el.text.strip()) + for el in tree.iter("command"): + if el.text: + # Extract only the tool name (first word before any flags). + tool = el.text.strip().split()[0] + if TOOL_PATTERN.match(tool): + registered.add(tool) + + return registered + + +def is_excluded(name: str) -> bool: + """Return True if *name* matches any entry in EXCLUDED. + + Entries ending with '.*' are treated as prefix wildcards: + 'g.*' matches any name beginning with 'g.'. + All other entries are matched literally. + """ + for pattern in EXCLUDED: + if pattern.endswith(".*"): + prefix = pattern[:-1] # strip the '*', keep 'g.' + if name.startswith(prefix): + return True + elif name == pattern: + return True + return False + + +def main() -> int: + root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() + + toolboxes = root / "gui" / "wxpython" / "xml" / "toolboxes.xml" + wxgui_items = root / "gui" / "wxpython" / "xml" / "wxgui_items.xml" + + for path in (toolboxes, wxgui_items): + if not path.exists(): + print( + f"ERROR: {path} not found. Run from the repository root.", + file=sys.stderr, + ) + return 1 + + source_tools = find_source_tools(root) + registered = find_registered_tools(toolboxes, wxgui_items) + + missing = sorted( + name for name in source_tools - registered if not is_excluded(name) + ) + + if not missing: + print("All source tools are registered in toolboxes.xml.") + return 0 + + print(f"Tools in source tree but missing from toolboxes.xml ({len(missing)}):\n") + for name in missing: + print(f" {name}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())