C4 Assembly Dependency Resolution during Plugin Installation
These are the fundamental mechanisms in the Component Manager
Interface assemblies
In a given installation, interface dlls are only ever present in the newest version. During plugin installation, each interface assembly reference from the plugin is examined the following way:
- Search for the interface assembly dll in the same directory where the plugin is. If the assembly is not found there, abort with an error.
- If the assembly is found, check whether there is already an interface installed with the same name.
- If no interface assembly of the same name is already installed, install the interface assembly which comes with the plugin.
- If there is a newer version of the interface already installed, silently skip that interface.
- If there is the same or an older version of the interface already installed, perform the compatibility check:
- If the check fails, abort with an error.
- If the check passes, install the interface assembly which comes with the plugin, including all its satellite assemblies for localization.
This set of rules guarantees that interface assemblies are always present in the newest version only. During development, interfaces are still overwritten in the post-build step with IPMCLI, even when the version numbers are not increased with every build.
Note: To overwrite installed interfaces, Shadow Copying must be active. The Component Manager does enable shadow copying - however, at that point in time, the Core.dll is already loaded. To allow packages to install a newer core.dll, all executables need to enable shadow copying before the Core.dll is active. In CODESYS, this is implemented since V3.5 SP5 Patch 2 (CDS-32746).
GAC assemblies
For GAC assemblies, different versions can be installed in the Global Assembly Cache at the same time, and all referenced versions will be loaded at runtime simultaneously.
As GAC assemblies can reference other GAC assemblies, this may introduce collisions when different versions of the same GAC assembly are referenced (directly or indirectly) by your plugin. Thus, the simple solution of copying the GAC assembiles just into the same directory as the plugin during build via "Copy Local" won't work in all cases, and a different process is applied:
During plugin installation, each referenced GAC assembly is examined the following way: ($name
denotes the name and $version
the version of the searched gac assembly.)
- Search for the GAC assembly dll in the following search pathes:
- The same directory as the plugin dll (This covers the simple, collision free "Copy Local" case.)
- The location
$name\$version\$name.dll
relative to the directory where the plugin resides. (This case covers packages who need to carry several versions of the same GAC assembly, and developers who copy the GAC assemblies into the correct layout during the post build step before callingIPMCLI.exe
.) - The path
..\..\..\..\GAC Binaries\$name\$version\$name.dll
relative to the directory where the plugin resides. (This case covers a common directory layout where the "GAC Binaries" directory from the
SDK is copied in a location relative to the build directory of the project.) - For each directory path given in in the additional GAC installation search path list,
$path\$name.dll
and$path\$name\$version\$name.dll
are probed. (The pathes in that list may be absolute, or relative to the directory containing the plugin file. This case covers the Automation Platform SDK users / plugin developers who don't adhere to this directory layout, and pass the location of the GAC Binaries directory of the SDK to IPMCLI via the--gacsearchpath
command line option.)
- If the GAC assembly is found during the probing above, it is installed into the GAC, overwriting an existing DLL with the same FullName (Name, Version, PublicKeyToken and Culture).
- If the GAC assembly is not found during the probing above, but it is already installed in the GAC on the current machine, the reference is assumed to be satisifed, and the reference is skipped without action. IPMCLI issues a warning in this case, the PackageManager silently installs the plugin. (This case is mainly for backwards compatibility with old, broken packages which do not come with all necessary GAC assemblies.)
- If the GAC assembly is neither present in the search path, nor in the GAC, the plugin installation aborts with an error message.
This set of rules guarantees that all GAC assemblies which are needed by a plugin are actually satisfied when the plugin is installed.
Additional File References (since V3.5 SP5)
In V3.5 SP5, this scheme was extended with a custom [AdditionalFileReferences]
attribute which allows explicit file references to be defined. The main purpose is to allow native dll files to be referenced, so they will be copied along with the plugin into the plugin installation directory. This attribute also allows overriding of assembly references, so they are also copied as ordinary files along with the plugin instead of being installed into the GAC or the Common directory. This attribute may be specified multiple times.
Additionally, copying of externally linked resource files referenced by the assembly (e. G. mixed mode assemblies written in C++/CLI) should now also work for interfaces and plugins (this used to work for GAC assemblies as the .NET Framework recognizes those references, and the GAC installation procedure automatically takes care for them).
Example:
Add the following definitions to the AssemblyInfo.cs
:
[assembly: AdditionalFileReferences("test.txt", "anothertest.txt")]
[assembly: AdditionalFileReferences("optionaltest.txt", "anotheroptionaltest.txt", Optional = true)]
[assembly: AdditionalFileReferences("wildcard*.txt", UsePatternMatching = true)]
[assembly: AdditionalFileReferences("HelperLibrary.dll", "HelperLibrarySigned.dll")]
Syntax Definitions:
The [AdditionalFileReferences]-
Attribute takes a list of one or more pathes, those are interpretet as pathes relative to the plugin directory.
The optional parameter UsePatternMatching=true
allows interpretation of the pathes using wildcard based pattern matching. The wildcard matching is done with semantics equal to the .NET function Directory.GetFiles(string, string) using the plugin directory as the base directory. However, the patterns may include path components, then the pattern components are applied to the corresponding path components. As a special case, the pattern component ** matches arbitrary levels of directories. Examples:
wildcard*.txt
Matches wildcard.txt
and wildcard2.txt
, but not wildcard.doc
F*lder/Folder*txt
Matches Folder/Foldertest.txt and Finefolder/Foldertest2.txt,
but not Folder/NonCopyFolder.txt.
html/**/*.*
Matches all files and directories in the html
directory
recursively.
*/Foo.resources.dll
Matches all localization resources for the Foo.dll
library.
The optional parameter Optional=True
supresses the error message when a file referenced via [AdditionalFileReferences]
is not found during the lookup, or when a pattern matches no files at all.
If a file is matched both by an explicit [AdditionalFileReferences]
declaration and the implicit recognition of interfaces and GAC assemblies, the explicit [AdditionalFileReferences]-Declaration has precedence. This allows utility .NET assemblies (e. G. COM Interop assemblies) to be copied along into the plugin directory, instead of them being forced into the Common directory (unsigned assemblies) or the Global Assembly Cache (signed assemblies).
Location of the files at runtime:
Note that this mechanism does not solve the problem of how to locate those files at runtime. Due to the shadow copying mechanism, the Assembly.Location property does not work in all cases. References which are recognized by the .NET framework (e. G. referenced .NET assemblies and externally linked resources) are copied along into the shadow copy directory.
However, files which are not recognized by the .NET framework will remain in the CODESYS installation directory. It should be possible to use the Assembly.CodeBase property to locate the installation directory of the plugin.
Notes:
- You should not keep open filehandles for files in the installation directory for a longer time, as it undermines the purpose of the shadow copy mechanism: Allowing overwriting and deletion of those files during (de-)installation of packages. So either try to close the files quickly, or use your own mechanism to copy those files into a safe place.
- At runtime, the installation directory should be considered read-only (and it actually is on most installations), so don't put databases, repositories or other mutable data there. (You may well use this mechanism for template files for such mutable data, however.)
- This mechanism relies on the presence of the V3.5 SP5 Component Manager (
IPMCLI.exe, Core.dll, CoreInstallerSupport.exe and Utilities.dll
), so it will only work for installations of V3.5 SP5 and newer. For package creators, it means that this mechanism cannot be used for packages intended to work in older CODESYS versions. Thus, it is strictly recommended that package creators set the <RequiredInstallerVersion
> setting to 3.5.5.0 or higher when using this feature.
Historic / legacy behaviour
In CODESYS Version 3.5.1.X and older, the Component Manager did notexplicitly resolve the GAC dependencies, but just treated all signed assemblies in the same directory as GAC assemblies, and installed them. This had the following disadvantages:
- There was no mechanism for handling multiple revisions of the same GAC assembly - if the a plugin (directly or indirectly) referenced several versions of the same GAC assembly, it was not possible to create a complete, self-contained package. Additionally, Visual Studio issued build warnings when the "Copy Local" mechanism was used for those GAC assemblies.
- GAC assemblies which were missing in a package were not reported to the developer. Packages always worked on the developers machine (as the assemblies were present in the GAC there), but then failed on the customers machine.
- If a package had several components, and the user selected only some of the components for installation, the GAC binaries of deselected components could be accidentally installed.
Nice, but what to do now?
Automation Platform developers
Assuming that you are auto-installing your packages with a post build script using IPMCLI.exe
:
For interface references, set the "Copy Local" flag to true in the assembly reference properties in Visual Studio. This will instruct visual studio to copy the interface DLLs to the same directory as the plugin dll.
For GAC references, set the "Copy Local" flag to false, and use one of the three following possibilities:
- Adopt your directory layout so that the path
..\..\..\..\GAC Binaries\
points to the GAC Binaries directory from the SDK. - Or adopt your post-build script with the "
--gacsearchpath
" option. (Hint: You can access environment variables in your post build script.) - Or add the appropriate commands to your post-build script to copy the needed GAC binaries to a
$name\$version\
subdirectory relative to your plugin.
An example for a post build step could look like this:
"%CODESYS_OUTPUTDIR%\Common\IPMCLI.exe" /i:"$(TargetPath)" --gacsearchpath="%CODESYS_GAC_BINARIES%;..\..\..\OEMToolBox\1.0"
In this example, the environment variable %CODESYS_OUTPUTDIR%
points to the CODESYS installation directory, and %CODESYS_GAC_BINARIES%
points to the "GAC Binaries" directory from the SDK.
Package Designer users
The Package Designer will automatically create the appropriate layout within the package, and it will warn you during package validation when it finds unresolved references.
Developers creating Packages by other means
When creating the directory tree for the package contents, simply copy all the needed interfaces into the same directory as the plugin dll file, and copy all the needed GAC binaries into $name\$version\
subdirectories relative to the plugin dll file. When the plugin is installed, the interfaces and GAC binaries will be automatically installed as appropriate.
Auto-build maintainers
If you maintain an autobuild server or continous integration system, and need to analyze the dependencies of a plugin to discover the appropriate set of files to pack into a package or installer, you can callipmcli.exe
with the --resolvegacreferences
switch. This outputs all resolved and unresolved references of the plugin, with their respective pathes, in a human readable and machine parseable format. For each of them, it also outputs the relative pathes of all satellite assemblies and external ressource files which need to be copied along, if any. You can use the "--gacsearchpath
" option to specify additional search pathes for the GAC libraries, like the GAC Binaries directories from our SDK and the build directories of your own GAC binaries. (Multiple pathes are separated by ;)
Examples:
$>ipmcli.exe
--resolvegacreferences="D:\Example\bin\debug\Example.plugin.dll"
--gacsearchpath="D:\SDK\GAC Binaries;D:\SharpSvn\bin\debug"
May give the following output:
Installation and Profile Manager
Copyright © 1994-2012 by 3S-Smart Software Solutions GmbH. All rights reserved.
Main Plugin: 1
Example.plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: D:\Example\bin\debug\FileTransfer.plugin.dll
Interfaces: 4
SubversionIntegration, Version=3.5.1.0, Culture=neutral, PublicKeyToken=null: D:\Example\bin\debug\SubversionIntegration.dll
Core, Version=3.5.1.3, Culture=neutral, PublicKeyToken=null: D:\Example\bin\debug\Core.dll
ScriptEngine, Version=3.5.1.0, Culture=neutral, PublicKeyToken=null: D:\Example\bin\debug\ScriptEngine.dll
DeviceObject, Version=3.5.1.3, Culture=neutral, PublicKeyToken=null: D:\Example\bin\debug\DeviceObject.dll
GAC Binaries: 3
Compression, Version=3.0.17.0, Culture=neutral, PublicKeyToken=83380e73b2486719: D:\test\CoDeSys SVN 3.5SP1\PlugIns\Compression.dll
Utilities, Version=3.0.18.0, Culture=neutral, PublicKeyToken=83380e73b2486719: D:\test\CoDeSys SVN 3.5SP1\PlugIns\Utilities.dll
SatelliteAssembly: de\Utilities.resources.dll
SatelliteAssembly: en\Utilities.resources.dll
SatelliteAssembly: es\Utilities.resources.dll
SatelliteAssembly: fr\Utilities.resources.dll
SatelliteAssembly: it\Utilities.resources.dll
SatelliteAssembly: ru\Utilities.resources.dll
SatelliteAssembly: zh-CHS\Utilities.resources.dll
SharpSvn, Version=1.7005.2203.13684, Culture=neutral, PublicKeyToken=d729672594885a28: D:\SharpSvn\bin\debug\SharpSvn.dll
ModuleFile: SharpPlink-Win32.svnExe
LinkedResource: SharpPlink-Win32.svnExe
SatelliteAssembly: de\SharpSvn.resources.dll
SatelliteAssembly: es\SharpSvn.resources.dll
SatelliteAssembly: fr\SharpSvn.resources.dll
SatelliteAssembly: it\SharpSvn.resources.dll
SatelliteAssembly: ja\SharpSvn.resources.dll
SatelliteAssembly: ko\SharpSvn.resources.dll
SatelliteAssembly: pl\SharpSvn.resources.dll
SatelliteAssembly: sv\SharpSvn.resources.dll
SatelliteAssembly: zh-TW\SharpSvn.resources.dll
SatelliteAssembly: pt-BR\SharpSvn.resources.dll
SatelliteAssembly: zh-CN\SharpSvn.resources.dll
SatelliteAssembly: nb\SharpSvn.resources.dll
Failed (but exist in GAC): 1
PInvoke, Version=3.0.19.0, Culture=neutral, PublicKeyToken=83380e73b2486719: Did not find any matching assembly in the search path while searching for PInvoke, Version=3.0.19.0, Culture=neutral, PublicKeyToken=83380e73b2486719. However, the Assembly was found in the Global Assembly Cache.
Failed: 0