0
0
Fork 0

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 := '<X.Y.Z.W>';
  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.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-26 00:31:26 +02:00
parent bd06fe76d7
commit 00d2dd8d96
2 changed files with 86 additions and 1 deletions

View file

@ -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:

View file

@ -1487,7 +1487,7 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
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 := '<X.Y.Z.W>'` 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."),