Empirical failure: 'No libraries found in the project (or Library Manager not found)' both before AND after a successful add_library on SP22 Patch 1, even though add_library writes the entry visibly to the IDE.
Root cause: list_project_libraries only walked has_library_manager-flagged containers; on some SPs the project root flags has_library_manager but children don't, leaving the read path with zero containers. The write path's find('Library Manager') legacy fallback was missing here.
Fix: after find_libman_containers() comes back empty, also try project.find('Library Manager', True) and append each match. The reference loop now accepts an item that IS already a libman (has .references / .get_libraries) instead of always calling .get_library_manager() on it.
Docs: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptLibManObject.html
The .references / get_libraries iteration was already in place from
prior commits; this commit just unifies the discovery path with
add_library.py so the read mirrors the write.
### Manual smoke test
1. Open a project with at least one library reference (e.g. Standard, * (System)).
2. mcp__codesys__list_project_libraries: expect a non-empty references[]
array per container, NOT 'No libraries found'.
3. mcp__codesys__add_library libraryName=Util followed by
list_project_libraries: expect Util to show up alongside Standard.
287 lines
12 KiB
Python
287 lines
12 KiB
Python
import sys, scriptengine as script_engine, os, traceback, json
|
|
|
|
# RTFM (helpme-codesys.com + local SP22 stub
|
|
# Stubs/scriptengine/ScriptLibManObject.pyi):
|
|
#
|
|
# - ScriptLibManObjectContainer is a marker interface added to BOTH the
|
|
# project AND every Application object. Two key members:
|
|
# has_library_manager -- @property (NOT a method) returning bool
|
|
# get_library_manager() -- method, returns the LibMan ScriptObject
|
|
# - ScriptLibManObject (the LibMan itself) exposes:
|
|
# .references -- @property, ScriptLibraryReferences (list-like) of
|
|
# ScriptLibraryReference. Best for structured data.
|
|
# get_libraries(recursive=False) -- list[str] of library names.
|
|
# - ScriptLibManObjectMarker.is_libman is the universal "is this a libman?"
|
|
# property added to every ScriptObject, useful as a fallback when walking
|
|
# the project tree.
|
|
#
|
|
# The previous version of this script searched the tree for an object whose
|
|
# NAME matched "Library Manager" (via find() / get_children name probe). That
|
|
# never worked because the libman's actual name is generated, not literal.
|
|
# Fixed by walking every container with has_library_manager and pulling
|
|
# references via lm.references.
|
|
|
|
|
|
def find_libman_containers(node, depth=0, max_depth=8):
|
|
"""Walk the project tree and yield every node where has_library_manager
|
|
is True. Depth-limited so a malformed project tree can't loop us."""
|
|
out = []
|
|
if depth > max_depth:
|
|
return out
|
|
try:
|
|
# has_library_manager is a property, not a method -- accessing it
|
|
# on a non-container ScriptObject can raise, so guard.
|
|
if hasattr(node, 'has_library_manager'):
|
|
try:
|
|
hlm = node.has_library_manager
|
|
except Exception:
|
|
hlm = False
|
|
if hlm:
|
|
out.append(node)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
children = node.get_children(False)
|
|
except Exception:
|
|
children = []
|
|
for child in children:
|
|
out.extend(find_libman_containers(child, depth + 1, max_depth))
|
|
return out
|
|
|
|
|
|
def safe_get(obj, attr, default=None):
|
|
"""getattr that swallows access exceptions (some properties throw on
|
|
placeholders / unmanaged refs depending on the SP). Returns default
|
|
if missing or raising; calls callables."""
|
|
try:
|
|
if not hasattr(obj, attr):
|
|
return default
|
|
v = getattr(obj, attr)
|
|
return v() if callable(v) else v
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def reference_to_dict(ref):
|
|
"""Capture as much structured info as the SP exposes, defensively."""
|
|
entry = {}
|
|
for prop in ('id', 'name', 'namespace', 'is_placeholder', 'is_managed',
|
|
'system_library', 'qualified_only', 'optional',
|
|
'placeholder_name', 'effective_resolution',
|
|
'default_resolution', 'is_redirected', 'resolution_info'):
|
|
v = safe_get(ref, prop)
|
|
if v is not None:
|
|
try:
|
|
entry[prop] = str(v) if not isinstance(v, bool) else v
|
|
except Exception:
|
|
pass
|
|
return entry
|
|
|
|
|
|
def collect_project_info(project):
|
|
"""Read the Project Information node (first child of project root):
|
|
version, title, company, author. Used to surface the project version
|
|
above the library list in the rendered markdown."""
|
|
info = {'version': None, 'title': None, 'company': None, 'author': None}
|
|
try:
|
|
for child in project.get_children(False):
|
|
try:
|
|
if child.get_name() != 'Project Information':
|
|
continue
|
|
except Exception:
|
|
continue
|
|
for attr in ('version', 'title', 'company', 'author'):
|
|
try:
|
|
v = getattr(child, attr, None)
|
|
if v is None:
|
|
continue
|
|
s = str(v)
|
|
if s and s != 'None':
|
|
info[attr] = s
|
|
except Exception:
|
|
pass
|
|
break
|
|
except Exception:
|
|
pass
|
|
return info
|
|
|
|
|
|
def collect_devices(project):
|
|
"""Walk the tree and capture every is_device=True node's get_device_identification()
|
|
(the offline target id from project settings: type/id/version). Lets the
|
|
markdown renderer show 'MainPLC target firmware version' alongside the
|
|
library list."""
|
|
devices = []
|
|
|
|
def walk(node, prefix='', depth=0, max_depth=10):
|
|
if depth > max_depth:
|
|
return
|
|
try:
|
|
name = node.get_name()
|
|
except Exception:
|
|
name = '?'
|
|
path = (prefix + '/' + name) if prefix else name
|
|
is_dev = False
|
|
try:
|
|
is_dev = bool(node.is_device)
|
|
except Exception:
|
|
pass
|
|
if is_dev:
|
|
entry = {'path': path, 'name': name}
|
|
try:
|
|
ident = node.get_device_identification()
|
|
for attr in ('type', 'id', 'version'):
|
|
v = getattr(ident, attr, None)
|
|
if v is not None:
|
|
entry['device_id_' + attr] = str(v)
|
|
except Exception:
|
|
pass
|
|
devices.append(entry)
|
|
try:
|
|
children = node.get_children(False)
|
|
except Exception:
|
|
children = []
|
|
for c in children:
|
|
walk(c, path, depth + 1, max_depth)
|
|
|
|
try:
|
|
for child in project.get_children(False):
|
|
walk(child)
|
|
except Exception:
|
|
pass
|
|
return devices
|
|
|
|
|
|
try:
|
|
print("DEBUG: list_project_libraries: Project='%s'" % PROJECT_FILE_PATH)
|
|
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
project_basename = os.path.basename(PROJECT_FILE_PATH)
|
|
|
|
project_info = collect_project_info(primary_project)
|
|
devices = collect_devices(primary_project)
|
|
ide_version = sys.version # IronPython under CODESYS reports the IDE
|
|
# Project-wide compiler version selector. Available since ScriptEngine
|
|
# 4.2.0.0; older IDEs return None / raise. Captured here so a manual
|
|
# change in the IDE ("Project > Project Settings > Compiler version")
|
|
# leaves a textual trace in mcp-mirror/library.md instead of being
|
|
# invisible to the release classifier (binary-only diff -> SHA fallback).
|
|
compiler_version = None
|
|
try:
|
|
if hasattr(primary_project, 'get_compilerversion'):
|
|
cv = primary_project.get_compilerversion()
|
|
if cv is not None:
|
|
compiler_version = str(cv)
|
|
except Exception as e:
|
|
print("DEBUG: get_compilerversion() failed: %s" % e)
|
|
print("DEBUG: project_info: %s" % project_info)
|
|
print("DEBUG: compiler_version: %s" % compiler_version)
|
|
print("DEBUG: %d device(s) found." % len(devices))
|
|
|
|
containers = find_libman_containers(primary_project)
|
|
print("DEBUG: %d libman container(s) found in tree." % len(containers))
|
|
|
|
# Per OPEN-BUGS-CROSS-REFERENCE Bug 3: mirror add_library.py's
|
|
# discovery pattern -- if the recursive walk found nothing, also try
|
|
# the find('Library Manager') legacy fallback. Some SPs render the
|
|
# has_library_manager property only on the project root, so the
|
|
# walk may not catch nested libmans on bare-bones projects.
|
|
if not containers:
|
|
try:
|
|
found_list = primary_project.find("Library Manager", True)
|
|
if found_list:
|
|
# find() returns ScriptObject wrappers around the libman
|
|
# itself, not the container; treat it as a libman and
|
|
# synthesize a pseudo-container. We append the libman
|
|
# directly so the consumer below can call get_library_manager()
|
|
# OR use the libman as-is if it already exposes
|
|
# references/get_libraries.
|
|
for libman_obj in found_list:
|
|
print("DEBUG: find('Library Manager') fallback yielded %s" % type(libman_obj).__name__)
|
|
containers.append(libman_obj)
|
|
except Exception as e:
|
|
print("DEBUG: find('Library Manager') fallback failed: %s" % e)
|
|
if not containers:
|
|
print("DEBUG: no libman containers via has_library_manager NOR find() fallback.")
|
|
|
|
result = {
|
|
'project': project_basename,
|
|
'project_info': project_info,
|
|
'ide_version': ide_version,
|
|
'compiler_version': compiler_version,
|
|
'devices': devices,
|
|
'containers': [],
|
|
'total_references': 0,
|
|
}
|
|
|
|
for container in containers:
|
|
container_name = safe_get(container, 'get_name', '?')
|
|
# If the find()-fallback appended a libman directly (it already
|
|
# has .references or .get_libraries), use it as-is. Otherwise
|
|
# call get_library_manager() to descend from the container.
|
|
lm = None
|
|
if hasattr(container, 'references') or hasattr(container, 'get_libraries'):
|
|
lm = container
|
|
print("DEBUG: using container '%s' as libman directly (find-fallback path)" % container_name)
|
|
elif hasattr(container, 'get_library_manager'):
|
|
try:
|
|
lm = container.get_library_manager()
|
|
except Exception as e:
|
|
print("DEBUG: get_library_manager() failed on '%s': %s" % (container_name, e))
|
|
continue
|
|
else:
|
|
print("DEBUG: container '%s' has neither references/get_libraries nor get_library_manager." % container_name)
|
|
continue
|
|
if lm is None:
|
|
print("DEBUG: container '%s' returned None for get_library_manager()." % container_name)
|
|
continue
|
|
lm_name = safe_get(lm, 'get_name', '?')
|
|
|
|
# Try the structured .references property first; fall back to the
|
|
# name-only get_libraries() method.
|
|
refs_struct = []
|
|
ref_iter = None
|
|
try:
|
|
ref_iter = lm.references
|
|
except Exception as e:
|
|
print("DEBUG: lm.references failed on '%s': %s" % (lm_name, e))
|
|
|
|
if ref_iter is not None:
|
|
try:
|
|
for r in ref_iter:
|
|
refs_struct.append(reference_to_dict(r))
|
|
except Exception as e:
|
|
print("DEBUG: iterating lm.references on '%s' failed: %s" % (lm_name, e))
|
|
|
|
# Fallback: name-only enumeration via get_libraries(recursive=False).
|
|
if not refs_struct and hasattr(lm, 'get_libraries'):
|
|
try:
|
|
names = lm.get_libraries(False)
|
|
for n in names:
|
|
refs_struct.append({'name': str(n), 'source': 'get_libraries-fallback'})
|
|
print("DEBUG: get_libraries fallback returned %d name(s) on '%s'" % (len(names), lm_name))
|
|
except Exception as e:
|
|
print("DEBUG: get_libraries() fallback failed on '%s': %s" % (lm_name, e))
|
|
|
|
result['containers'].append({
|
|
'container_name': container_name,
|
|
'libman_name': lm_name,
|
|
'references': refs_struct,
|
|
})
|
|
result['total_references'] += len(refs_struct)
|
|
print("DEBUG: container '%s' (libman '%s'): %d reference(s)" % (
|
|
container_name, lm_name, len(refs_struct)))
|
|
|
|
print("### LIBRARIES_START ###")
|
|
print(json.dumps(result))
|
|
print("### LIBRARIES_END ###")
|
|
print("Total references across %d container(s): %d" % (
|
|
len(result['containers']), result['total_references']))
|
|
print("SCRIPT_SUCCESS: Project libraries listed.")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
detailed = traceback.format_exc()
|
|
msg = "Error in list_project_libraries for project '%s': %s\n%s" % (
|
|
PROJECT_FILE_PATH, e, detailed)
|
|
print(msg)
|
|
print("SCRIPT_ERROR: %s" % msg)
|
|
sys.exit(1)
|