From 00d2dd8d9654713b4355a251ad44abcdbcc1e290 Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Sun, 26 Apr 2026 00:31:26 +0200 Subject: [PATCH] bump_project_version: also maintain _MCP_PROJECT_VERSION GVL Establishes the runtime-readable version anchor convention. Every bump (manual or auto) now ALSO ensures the Application has a GVL named '_MCP_PROJECT_VERSION' with: {attribute 'qualified_only'} VAR_GLOBAL CONSTANT sVersion : STRING := ''; END_VAR Created on first bump; updated in place thereafter. Soft-fails if the Application object can't be found or create_gvl() raises -- the primary outcome (Project Information.Version updated and saved) has already happened by the time GVL maintenance runs, so a GVL hiccup is logged as a WARNING but doesn't fail the whole tool. Why this matters: - Project Information.Version is metadata. The running PLC binary embeds it but exposing it at runtime requires the auto-generated Project_Info library helpers (GetVersion etc.), which not every project has wired up. - A plain VAR_GLOBAL CONSTANT in a known-name GVL is the simplest, most portable runtime anchor. Any IEC code can read it as `_MCP_PROJECT_VERSION.sVersion`. The future read_running_version_online tool will pull it via online connect + read_variable. The future SSH transport variant can pull it via libcmd-symbol-export or by grepping a debug log line that the project author can wire to write at startup. qualified_only is set so the symbol can't accidentally shadow a same-named local in user code. The GVL convention will be exercised end-to-end on MCPTest running on the local soft PLC (port 11740) once the read_running_version_online tool ships -- that's the next ship in this sequence. --- src/scripts/bump_project_version.py | 85 +++++++++++++++++++++++++++++ src/server.ts | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/scripts/bump_project_version.py b/src/scripts/bump_project_version.py index 89e2edf..e2732dd 100644 --- a/src/scripts/bump_project_version.py +++ b/src/scripts/bump_project_version.py @@ -21,6 +21,19 @@ LEVEL = "{LEVEL}" # major | minor | revision | build VALID_LEVELS = ('major', 'minor', 'revision', 'build') +# Standard runtime-readable version anchor. Lives as a constant in a GVL +# under Application so any IEC code can read it as +# _MCP_PROJECT_VERSION.sVersion, and a future read_running_version_online +# tool can pull it via online connect + read_variable. Kept as +# qualified_only so it can't accidentally shadow a same-named local. +VERSION_GVL_NAME = '_MCP_PROJECT_VERSION' +VERSION_GVL_DECLARATION_TEMPLATE = ( + "{attribute 'qualified_only'}\n" + "VAR_GLOBAL CONSTANT\n" + " sVersion : STRING := '%s';\n" + "END_VAR\n" +) + def parse_version(v): """Parse a version-like value into a 4-tuple of ints, defaulting missing @@ -45,6 +58,69 @@ def parse_version(v): return tuple(nums) +def maintain_version_gvl(primary_project, version_str): + """Find or create the _MCP_PROJECT_VERSION GVL under the active + Application, and set its declaration so the running PLC carries the + project version as a constant string. Soft-fails on any error -- the + primary outcome of the bump (Project Information.Version) has already + succeeded by the time this is called, so a GVL creation failure is + logged as a WARNING but does not fail the whole tool.""" + try: + app = getattr(primary_project, 'active_application', None) + except Exception: + app = None + if app is None: + try: + apps = primary_project.find('Application', True) + if apps: + app = apps[0] + except Exception: + pass + if app is None: + print("WARNING: no active Application found -- cannot maintain %s GVL" % VERSION_GVL_NAME) + return False + + decl = VERSION_GVL_DECLARATION_TEMPLATE % version_str + + # Try to find existing GVL with this name + existing = None + try: + for child in app.get_children(False): + try: + if child.get_name() == VERSION_GVL_NAME: + existing = child + break + except Exception: + pass + except Exception as e: + print("WARNING: walking Application children failed: %s" % e) + + if existing is not None: + try: + existing.textual_declaration.replace(decl) + print("DEBUG: updated %s -> sVersion := '%s'" % (VERSION_GVL_NAME, version_str)) + return True + except Exception as e: + print("WARNING: failed to update existing %s declaration: %s" % (VERSION_GVL_NAME, e)) + return False + + # Create it + if not hasattr(app, 'create_gvl'): + print("WARNING: Application object doesn't expose create_gvl -- cannot create %s" % VERSION_GVL_NAME) + return False + try: + new_gvl = app.create_gvl(name=VERSION_GVL_NAME) + if new_gvl is None: + print("WARNING: create_gvl returned None for %s" % VERSION_GVL_NAME) + return False + new_gvl.textual_declaration.replace(decl) + print("DEBUG: created %s with sVersion := '%s'" % (VERSION_GVL_NAME, version_str)) + return True + except Exception as e: + print("WARNING: failed to create %s: %s" % (VERSION_GVL_NAME, e)) + return False + + def bump(parts, level): major, minor, revision, build = parts if level == 'major': @@ -100,6 +176,11 @@ try: pi.version = after_str + # Maintain the runtime-readable version anchor (_MCP_PROJECT_VERSION GVL) + # so the running PLC carries the same string. Soft-fails so the primary + # bump still reports success even if GVL creation hits an edge case. + gvl_ok = maintain_version_gvl(primary_project, after_str) + try: primary_project.save() print("DEBUG: project.save() succeeded after version bump.") @@ -107,6 +188,10 @@ try: print("WARNING: project.save() raised %s -- bump applied in memory but may not persist across IDE close." % save_e) print("Project Information.Version: %s -> %s" % (before_str, after_str)) + if gvl_ok: + print("Runtime anchor: %s.sVersion := '%s'" % (VERSION_GVL_NAME, after_str)) + else: + print("Runtime anchor: %s NOT updated (see WARNING above)" % VERSION_GVL_NAME) print("SCRIPT_SUCCESS: bump_project_version complete.") sys.exit(0) except Exception as e: diff --git a/src/server.ts b/src/server.ts index 8d48882..dd65591 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1487,7 +1487,7 @@ export async function startMcpServer(config: ServerConfig): Promise { s.tool( 'bump_project_version', - "Bumps one part of the 4-part Project Information.Version field of the primary project (Major.Minor.Revision.Build) and saves the project. Convention: major = incompatible API break (rename FB / change public signature / remove method); minor = backward-compatible feature add (new FB / GVL / method); revision = bug fix only; build = internal counter, often 0 for hand-released versions. Bumping a higher part resets all lower parts to 0 (e.g. bumping minor resets revision and build to 0). FIRST-RUN: if no version is set yet (None/empty/0.0.0.0), seeds at 1.0.0.0 regardless of level so a first-time bump gives a canonical starting point instead of 0.0.0.1. The Version is exposed at runtime via the Project Information library's GetVersion() helper, which IEC code can call to surface the running version. AUTO MODE: if level='auto', the tool diffs the project's mcp-mirror/ folder against the latest v* git tag and classifies the change (deletion/rename -> major; addition -> minor; modification -> revision; nothing or first-run -> build/seed). Requires the project's parent dir to be a git repo with at least one v* tag for full classification; otherwise falls back to 'build' (which seeds at 1.0.0.0 when the version is unset).", + "Bumps one part of the 4-part Project Information.Version field of the primary project (Major.Minor.Revision.Build) and saves the project. Also maintains a `_MCP_PROJECT_VERSION` GVL under Application with the new version as `sVersion : STRING := ''` so the running PLC carries the version at a known address (read it via the read_running_version_online tool). The GVL is created on first bump and updated in place thereafter. Convention: major = incompatible API break (rename FB / change public signature / remove method); minor = backward-compatible feature add (new FB / GVL / method); revision = bug fix only; build = internal counter, often 0 for hand-released versions. Bumping a higher part resets all lower parts to 0. FIRST-RUN: if no version is set yet (None/empty/0.0.0.0), seeds at 1.0.0.0 regardless of level so a first-time bump gives a canonical starting point instead of 0.0.0.1. AUTO MODE: if level='auto', the tool diffs the project's mcp-mirror/ folder against the latest v* git tag and classifies the change (deletion/rename -> major; addition -> minor; modification -> revision; no changes -> short-circuits with no bump; first-run -> seed at 1.0.0.0).", { projectFilePath: z.string().describe("Path to the project file."), level: z.enum(['major', 'minor', 'revision', 'build', 'auto']).describe("Which part of the 4-part version to bump. Major = incompatible API break. Minor = backward-compatible feature add. Revision = bug fix only. Build = internal / CI counter. AUTO classifies via git diff of mcp-mirror/ against the latest v* tag."),