From 57dc1f6884a5a7599746f9808594dfb6e2f482f8 Mon Sep 17 00:00:00 2001 From: Alon Nusem Date: Tue, 1 Sep 2026 08:02:30 +1000 Subject: [PATCH 1/5] Moved library export to avoid clashing with regular export The library export used to export to the device folder and wiped out any files that had been exported with the normal export to files command. Now the export lib files land in a POUs folder beside the device folder so that they can be exported without collision. --- CHANGELOG.md | 1 + README.md | 2 +- src/entrypoint.py | 6 ++++++ src/script_lib_export_to_files.py | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7709e8a..e4aa9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `Export To Files` writes the export into a sibling folder named `.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed. If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds; if that also fails, the error dialog reports the staging folder path, so the completed export is preserved. Syncing also removes files the export did not write, as the swap would have, so a rendering from a previous export is never left beside a newer native xml; any that cannot be removed are named. - Device-tree sibling devices (Ethernet, Modbus and fieldbus devices that sit next to `Plc Logic` as direct children of the PLC device) are exported to `/devices/.xml`, one file per top-level device. On import, each tracked device is removed and recreated from its exported xml; devices fixed by the device package (for example `Local_IO` and `HMI` on IFM hardware) cannot be removed and are skipped with a message, with their configuration carried by the project template. A `_NO_EXPORT` folder added as a direct child of the PLC device disables the device-tree export. - New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files`, without the device and application folder levels. Importing a library export back into a project is not yet supported. +- The library export now lands in its own `POUs` folder, a sibling of the device/application export folder, so the two exports have independent folders and no longer wipe each other out. ## v0.2.0 diff --git a/README.md b/README.md index d2f425d..9b73a0c 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Exports made with older versions of CODESCRIBE use different filenames for some ## Export Lib To Files -Library projects keep their POUs, DUTs, GVLs and folders directly under the project root rather than under a Device, so `Export To Files` (which walks device entrypoints) exports nothing for them. `Export Lib To Files` walks the project root instead and writes the same on-disk format as `Export To Files`, without the device and application folder levels. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. +Library projects keep their POUs, DUTs, GVLs and folders directly under the project root rather than under a Device, so `Export To Files` (which walks device entrypoints) exports nothing for them. `Export Lib To Files` walks the project root instead and writes the same on-disk format as `Export To Files` in a sibling folder called `POUs`, without the device and application folder levels. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. Importing a library export back into a project is not yet supported. diff --git a/src/entrypoint.py b/src/entrypoint.py index 2a99c86..8f7ca46 100644 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -11,6 +11,12 @@ def get_src_folder(project): return src_folder +def get_lib_src_folder(project): + # Sibling of the device/application export folder so the two exports never clash. + working_dir = os.path.dirname(project.path) + return os.path.join(working_dir, "POUs") + + def get_device_entrypoints(project): children = project.get_children() for child in children: diff --git a/src/script_lib_export_to_files.py b/src/script_lib_export_to_files.py index 33520e0..f3daa22 100644 --- a/src/script_lib_export_to_files.py +++ b/src/script_lib_export_to_files.py @@ -5,7 +5,7 @@ import scriptengine # type: ignore -from entrypoint import get_src_folder +from entrypoint import get_lib_src_folder import graphical_export from import_export import OBJECT_TYPE_TO_EXPORT_FUNCTION, SERVICE_EXPORT_FUNCTIONS, write_native from object_type import ObjectType, get_object_type @@ -69,7 +69,7 @@ def export_child(child_obj, parent_obj, parent_folder_path): assert_project_open() graphical_export.reset_stats() - src_folder = get_src_folder(scriptengine.projects.primary) + src_folder = get_lib_src_folder(scriptengine.projects.primary) print("Writing to: " + src_folder) # A device project keeps its objects under Devices, which this walker never From 737889463c392c335ecf1f31c1e1aac1af776723 Mon Sep 17 00:00:00 2001 From: Alon Nusem Date: Tue, 1 Sep 2026 08:12:42 +1000 Subject: [PATCH 2/5] Added support for importing library files back into project This adds a new command that enables importing a codesys project library files (those that are underneath the POUs tab) back in from an export. Some files need to be ignored in the current project as they cannot be deleted as they are core to codesys. --- CHANGELOG.md | 4 ++-- README.md | 5 +++-- config.json | 7 +++++++ icons/import_lib_from_files.ico | Bin 0 -> 210 bytes icons/import_lib_from_files.png | Bin 0 -> 188 bytes src/import_export.py | 15 ++++++++++++-- src/import_from_files.py | 19 ++++++++++++++++- src/script_lib_import_from_files.py | 31 ++++++++++++++++++++++++++++ 8 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 icons/import_lib_from_files.ico create mode 100644 icons/import_lib_from_files.png create mode 100644 src/script_lib_import_from_files.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e4aa9aa..12680fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ - Visualisation service objects (visualisation manager and related service GUIDs) are recognised and no longer exported. - `Export To Files` writes the export into a sibling folder named `.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed. If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds; if that also fails, the error dialog reports the staging folder path, so the completed export is preserved. Syncing also removes files the export did not write, as the swap would have, so a rendering from a previous export is never left beside a newer native xml; any that cannot be removed are named. - Device-tree sibling devices (Ethernet, Modbus and fieldbus devices that sit next to `Plc Logic` as direct children of the PLC device) are exported to `/devices/.xml`, one file per top-level device. On import, each tracked device is removed and recreated from its exported xml; devices fixed by the device package (for example `Local_IO` and `HMI` on IFM hardware) cannot be removed and are skipped with a message, with their configuration carried by the project template. A `_NO_EXPORT` folder added as a direct child of the PLC device disables the device-tree export. -- New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files`, without the device and application folder levels. Importing a library export back into a project is not yet supported. -- The library export now lands in its own `POUs` folder, a sibling of the device/application export folder, so the two exports have independent folders and no longer wipe each other out. +- New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files` in a sibling folder called `POUs`, without the device and application folder levels, so the two exports live side by side without overwriting each other. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. +- New `Import Lib From Files` script reads the `POUs` folder back into a library project. Objects that CODESYS generates implicitly (for example functions generated from the project information) cannot be removed and are left in place during the import instead of aborting it. ## v0.2.0 diff --git a/README.md b/README.md index 9b73a0c..a0f2c78 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ CODESCRIBE supplies five scripts that run inside the CODESYS ScriptEngine and ap - `Save As Template` generates a "template" project by making a copy of the current project and deleting all exportable objects - `Update From Template` updates the current working project by copying the template file and importing the plaintext files - `Export Lib To Files` exports a library project, which keeps its objects directly under the project root rather than under a Device (see [Export Lib To Files](#export-lib-to-files)) +- `Import Lib From Files` imports the files exported by `Export Lib To Files` back into a library project A typical workflow: @@ -117,7 +118,7 @@ Exports made with older versions of CODESCRIBE use different filenames for some Library projects keep their POUs, DUTs, GVLs and folders directly under the project root rather than under a Device, so `Export To Files` (which walks device entrypoints) exports nothing for them. `Export Lib To Files` walks the project root instead and writes the same on-disk format as `Export To Files` in a sibling folder called `POUs`, without the device and application folder levels. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. -Importing a library export back into a project is not yet supported. +`Import Lib From Files` reads the `POUs` folder back into a library project: it removes tracked objects from the project root and recreates them from the exported files. ## Project Templates @@ -209,7 +210,7 @@ Once installed, proceed to [Adding the Script Toolbar to CODESYS](#adding-the-sc 5. Under Categories, scroll down, select ScriptEngine Commands and pick the script you want to add - - Scripts supplied by this repo are `Export To Files`, `Export Lib To Files`, `Import From Files`, `Save As Template` and `Update From Template` + - Scripts supplied by this repo are `Export To Files`, `Export Lib To Files`, `Import From Files`, `Import Lib From Files`, `Save As Template` and `Update From Template` ![Step 5](docs/step_5.png) diff --git a/config.json b/config.json index d37de0b..aeb7bc7 100644 --- a/config.json +++ b/config.json @@ -20,6 +20,13 @@ "Path": "codescribe\\src\\script_import_from_files.py", "Params": [] }, + { + "Name": "Import Lib From Files", + "Desc": "Import a library project from files on disk.", + "Icon": "codescribe\\icons\\import_lib_from_files.ico", + "Path": "codescribe\\src\\script_lib_import_from_files.py", + "Params": [] + }, { "Name": "Save As Template", "Desc": "Save as template.", diff --git a/icons/import_lib_from_files.ico b/icons/import_lib_from_files.ico new file mode 100644 index 0000000000000000000000000000000000000000..a4a1ffcda1517669f3a70ad3b48896cb3191a2c5 GIT binary patch literal 210 zcmZQzU<5(|0R|wcz_5pbfk6z2I|KaOdAX#xfJ|Ob50@Yy4OGD(z`+J2gv%S$HfOnx9AV%w)dZ8+b&*oj2it6x$6Zep{2o#!h2_kCII?!ad*Q(jQ#x zkaKl$Ro=ze)6-LMy=fB1CdC(G4I)W97*Y=WpIpP!^K8CELMBfNhuK*d$pw2(ioaX5 zyKc5&keZ`7Pat!G94o7mqdtS^<1^=O6j@fiV`nHlJmKPDv(kq^=P-D>`njxgN@xNA D_K87R literal 0 HcmV?d00001 diff --git a/icons/import_lib_from_files.png b/icons/import_lib_from_files.png new file mode 100644 index 0000000000000000000000000000000000000000..eac1d079a149a6680df8d71684debd0b5c3981c9 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`&7LlfAr}70fB0E=EE<}hNAk>M zzTF#mM8cgn;eZs|4JLkDlf=eOV}Xy7O)|ng-{jICTbAlWztCFKW lgXrTk=WY~PR=s0qC_Fsj;$gGWhd}2rc)I$ztaD0e0stXxKtuol literal 0 HcmV?d00001 diff --git a/src/import_export.py b/src/import_export.py index 1d14074..6f3ab3c 100644 --- a/src/import_export.py +++ b/src/import_export.py @@ -446,5 +446,16 @@ def export_visualisation_manager(child_obj, parent_obj, parent_folder_path, expo def remove_tracked_objects(obj_list): for obj in obj_list: if get_object_type(obj) in OBJECT_TYPE_TO_EXPORT_FUNCTION: - print("Removing " + obj.get_name()) - obj.remove() + try: + print("Removing " + obj.get_name()) + obj.remove() + except StandardError as remove_error: + # Implicitly generated objects (e.g. functions generated from the project + # information) cannot be removed; leave them in place. + print( + "Warning: could not remove '" + + obj.get_name() + + "' (" + + str(remove_error) + + "); leaving it in place" + ) diff --git a/src/import_from_files.py b/src/import_from_files.py index 593b352..c2d2581 100644 --- a/src/import_from_files.py +++ b/src/import_from_files.py @@ -2,7 +2,13 @@ from communication_import_export import import_communication from device_tree_import_export import import_device_tree_siblings -from entrypoint import find_application, find_communication, get_device_entrypoints, get_src_folder +from entrypoint import ( + find_application, + find_communication, + get_device_entrypoints, + get_lib_src_folder, + get_src_folder, +) from import_export import * from util import * @@ -97,3 +103,14 @@ def import_from_files(project): import_communication(communication, device_folder) import_device_tree_siblings(device_obj, device_folder) + + +def import_lib_from_files(project): + pous_folder = get_lib_src_folder(project) + print("Reading from: " + pous_folder) + assert_path_exists(pous_folder) + + # Service/manager objects are not in OBJECT_TYPE_TO_EXPORT_FUNCTION, so they are + # left in place here (they are ignored by the lib export too). + remove_tracked_objects(project.get_children()) + import_directory(pous_folder, project) diff --git a/src/script_lib_import_from_files.py b/src/script_lib_import_from_files.py new file mode 100644 index 0000000..83bd5a9 --- /dev/null +++ b/src/script_lib_import_from_files.py @@ -0,0 +1,31 @@ +# REMEMBER: this is python 2.7 +from __future__ import print_function + +import scriptengine # type: ignore + +from import_from_files import import_lib_from_files +from util import * + +try: + print_python_version() + assert_project_open() + + ui_continue = scriptengine.system.ui.prompt( + "Import Lib From Files will overwrite unexported changes in the current project!\n\nDo you want to continue?", + choice=scriptengine.PromptChoice.YesNo, + default_result=scriptengine.PromptResult.No, + store_description="Don't show again", + store_key="import_lib_from_files_confirm", + ) + + if ui_continue == scriptengine.PromptResult.Yes: + import_lib_from_files(scriptengine.projects.primary) + ui_info("Import Lib From Files complete.") + else: + print("Import cancelled.") +except Exception as e: + print(e) + ui_error_with_traceback("Import Lib From Files failed!") + raise e + +print("Done!") From e950d7f827c2a4ad9f5a03574b76f7c2bd1a033b Mon Sep 17 00:00:00 2001 From: Alon Nusem Date: Wed, 9 Sep 2026 08:47:49 +1000 Subject: [PATCH 3/5] Renamed library export to not conflict with other local projects --- src/entrypoint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/entrypoint.py b/src/entrypoint.py index 8f7ca46..a476dcd 100644 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -14,7 +14,8 @@ def get_src_folder(project): def get_lib_src_folder(project): # Sibling of the device/application export folder so the two exports never clash. working_dir = os.path.dirname(project.path) - return os.path.join(working_dir, "POUs") + project_name, _ = os.path.splitext(os.path.basename(project.path)) + return os.path.join(working_dir, "{}.POUs".format(project_name)) def get_device_entrypoints(project): From 7219dc18d1a749bc84f71ab25b4245a965c922fe Mon Sep 17 00:00:00 2001 From: Alon Nusem Date: Mon, 14 Sep 2026 13:47:41 +1000 Subject: [PATCH 4/5] Ignore library information and assosciated functions during export These were causing errors on import as they cannot be removed and would conflict. --- src/script_lib_export_to_files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/script_lib_export_to_files.py b/src/script_lib_export_to_files.py index f3daa22..ead3227 100644 --- a/src/script_lib_export_to_files.py +++ b/src/script_lib_export_to_files.py @@ -17,6 +17,7 @@ SKIP_NAMES = [ "Library Manager", "Project Information", + "Library Information", "Project Settings", "Task Configuration", "Symbol Configuration", From 1a42e83635df95f0b753dc586dac49912ac1b9ee Mon Sep 17 00:00:00 2001 From: Alon Nusem Date: Wed, 16 Sep 2026 14:22:05 +1000 Subject: [PATCH 5/5] Added library information to import skip list Previous exports generated this folder (no longer exported). However if they were exported, they would conflict with the existing instances that can't be removed from a codesys project. --- src/import_from_files.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/import_from_files.py b/src/import_from_files.py index c2d2581..6c43416 100644 --- a/src/import_from_files.py +++ b/src/import_from_files.py @@ -12,6 +12,7 @@ from import_export import * from util import * +SKIP_NAMES = ["Library Information"] def first_word_of_line_iter(f): for line in f.readlines(): @@ -112,5 +113,9 @@ def import_lib_from_files(project): # Service/manager objects are not in OBJECT_TYPE_TO_EXPORT_FUNCTION, so they are # left in place here (they are ignored by the lib export too). - remove_tracked_objects(project.get_children()) - import_directory(pous_folder, project) + tracked_children = [obj for obj in project.get_children() if obj.get_name() not in SKIP_NAMES] + remove_tracked_objects(tracked_children) + + for child in sorted(os.listdir(pous_folder), key=lambda x: x.count(".")): + if os.path.splitext(child)[0] not in SKIP_NAMES: + import_directory_child(child, pous_folder, project)