Include third party dependencies in Ignition module

I am trying to build an Ignition SDK module that integrates Ignition with third party Java dependencies that are on my local computer. I am following the Gradle example that was used in the Ignition SDK course on Inductive University. My module successfully builds and installs; however, when I test the module by running a command in the Execute console in Designer, I get an error that the dependencies are not found. How would I include these with my module?

Can you share your gradle build file?

Have you opened up the resulting modl file (it's just a zip file) to see if your dependencies are included and declared in the module XML file?

2 Likes

Hi Kevin,

Thanks for the quick response and I apologize my response was delayed. I have opened the resulting modl file and verified that the dependencies were not included. They seem to be available at compile time because I did a build with and without linking the dependencies in the gradle build file and the build was only successful when the dependencies were linked.

Here is the gradle build file I am using. It is an edited version of the gradle build file included with the Ignition sample SDK module. I tried using the shadowJar plugin to bundle the dependencies - is that the right approach?


plugins {
    id("io.ia.sdk.modl") version("0.1.1")
    java
    id("com.github.johnrengelman.shadow") version("7.1.2")
}

val sdk_version by extra("8.1.20")

allprojects {
    version = "0.0.1-SNAPSHOT"
}

ignitionModule {
    /*
     * Human readable name of the module, as will be displayed on the gateway status page
     */
    name.set("Ignition DDS Module")

    /*
     * Name of the '.modl' file to be created, without file extension.
     */
    fileName.set("Ignition-DDS-Module.modl")
    /*
     * Unique identifier for the module.  Reverse domain convention is recommended (e.g.: com.mycompany.charting-module)
     */
    id.set("com.inductiveautomation.ignition.examples.scripting.IgnitionDDSModule")

    /*
     * Version of the module.  Here being set to the same version that gradle uses, up above in this file.
     */
    moduleVersion.set("${project.version}")

    moduleDescription.set("This module allows Ignition to publish and subscribe to DDS topics.")

    /*
     * Minimum version of Ignition required for the module to function correctly.  This typically won't change over
     * the course of a major Ignition (7.9, 8.0, etc) version, except for when the Ignition Platform adds/changes APIs
     * used by the module.
     */
    requiredIgnitionVersion.set("8.1.11")
    /*
     *  This is a map of String: String, where the 'key' represents the fully qualified path to the project
     *  (using gradle path syntax), and the value is the shorthand Scope string.
     *  Example entry: listOf( ":gateway" to "G", ":common" to "GC", ":vision-client" to "C" )
     */
    projectScopes.putAll(mapOf(
        ":client" to "CD",
        ":common" to "GCD",
        ":designer" to "D",
        ":gateway" to "G"
    ))

    /*
     * Add your module dependencies here, following the examples, with scope being one or more of G, C or D,
     * for (G)ateway, (D)esigner, Vision (C)lient.
     * Example:
     * moduleDependencies = mapOf(
     *    "CD" to "com.inductiveautomation.vision",
     *    "G" to "com.inductiveautomation.opcua"
     *  )
     */
    moduleDependencies.set(mapOf<String, String>())

    dependencies {
        implementation(fileTree(mapOf("dir" to System.getenv("NDDSHOME") + "/lib/java", "include" to listOf("*.jar"))))
        // Other dependencies
    }

    /*
     * Map of fully qualified hook class to the shorthand scope.  Only one scope may apply to a class, and each scope
     * must have no more than single class registered.  You may omit scope registrations if they do not apply.
     *
     * Example entry: "com.myorganization.vectorizer.VectorizerDesignerHook" to "D"
     */
    hooks.putAll(mapOf(
        "com.inductiveautomation.ignition.examples.scripting.gateway.GatewayHook" to "G",
        "com.inductiveautomation.ignition.examples.scripting.client.ClientHook" to "C",
        "com.inductiveautomation.ignition.examples.scripting.designer.DesignerHook" to "D"
    ))

    /*
     * Optional 'documentation' settings.  Supply the files that would be desired to end up in the 'doc' dir of the
     * assembled module, and specify the path to the index.html file inside that folder. In this commented-out
     * example, the html files being collected are located in the module root project in `src/docs/`
     */
    // the files to collect into the documentation dir, with example implementation
    // documentationFiles.from(project.file("src/docs/"))

    /* The path from the root documentation dir to the index file, or filename if in the root doc dir. */
    // documentationIndex.set("index.html")

    /*
     * Optional unsigned modl settings. If true, modl signing will be skipped. This is not for production and should
     * be used merely for development testing
     */
    skipModlSigning.set(true)
}

tasks {
    shadowJar {
        // Set the classifier to an empty string to replace the default JAR
        archiveClassifier.set("")

        // Include the compile classpath in the JAR
        configurations = listOf(project.configurations.compileClasspath.get())

        // Set the main class in the manifest (if your application has a main class)
        manifest {
            attributes("Main-Class" to "com.your.package.MainClass")
        }
    }
}

repositories {
    mavenCentral()
    maven(url = "https://nexus.inductiveautomation.com/repository/public")

    // Adding a flat directory repository
    flatDir {
        dirs(System.getenv("NDDSHOME") + "/lib/java")
    }
}

Thanks again for your help.

I don't think it's one we recommend, so you're on your own with that.

In this case, though, you would need to dig further into the main JAR file(s) to see if your dependencies were correctly bundled by the shadow plugin and exist there.

What approach (or other plugin) would you recommend for bundling the dependencies into the the modl file?

You don't need an additional plugin to bundle them. The Ignition plugin is making sure they end up in the modl file.

Ok, I see. I was trying the shadowJar plugin because I was having issues getting them included with the Ignition plugin. I will remove that from the gradle file.

Am I missing anything that I should include to make the Ignition plugin include the dependencies?

You should be defining your dependencies in the sub-modules you create for each scope, not this parent build file.

Ok, I had defined the dependencies in each of the submodules as well.

I added this code to the dependencies section in the gradle build file for each submodule:

implementation(fileTree(mapOf("dir" to System.getenv("NDDSHOME") + "/lib/java", "include" to listOf("*.jar"))))

I added this code to the repositories section in the gradle build file for each submodule:

flatDir {
    dirs(System.getenv("NDDSHOME") + "/lib/java")
}

Anything else I should have added?

Nothing I can think of :man_shrugging:

I'm not very familiar with our gradle plugin at all, my only advice is to mimic the example the best you can: https://github.com/inductiveautomation/ignition-sdk-examples/tree/master/perspective-component

The rest of what you're dealing with is just Ignition-independent gradle stuff.

@PerryAJ any ideas?

Thanks Kevin, I will take a look at that.

Is Maven or Gradle more preferred to use with Ignition SDK module development? Gradle was used at https://github.com/inductiveautomation/ignition-sdk-training but it looks like a lot of the examples at https://github.com/inductiveautomation/ignition-sdk-examples use Maven.

The Maven plugin was the original but isn't maintained any more.

The Gradle plugin is the current and supported one.

Most of the examples pre-date the Gradle plugin's existence.

1 Like

You need to be declaring the dependencies you want your final module to include with an modlApi or modlImplementation declaration, rather than implementation. implementation is what allows it to compile, but modlApi is the signal to the module plugin that it's actually going to be part of the output and therefore should be included there. See example here: https://github.com/inductiveautomation/ignition-sdk-examples/blob/master/perspective-component%2Fgateway%2Fbuild.gradle.kts#L17

I don't know if that will work with your file based approach, though; the recommended pattern is to either use public Maven repositories or host your own compatible instance locally, rather than relying on raw JARs on your local filesystem, so I don't think our plugin has ever been tested with your approach. You can certainly give it a try, though.

1 Like

Thank you, I will give that a try. Is there a difference between modlApi and modlImplementation, or are they equivalent?

I believe there's no actual difference in the way our current module system works, but it's theoretically more clear to a third party coming and looking at your code.
That is, implementation should be used for "private" dependencies that only matter to one particular execution scope, and api should be used for dependencies that will be re-exported for further use downstream.

I used modlImplementation and successfully included the jar file in the modl file. However, I am still running into errors when I test the module by running a command in Script Console in designer that seem to indicate that the third-party jar is not available. How would I check to ensure that the third-party jar is available in the classpath at runtime?

My Script Console output is below:

Jython 2.7.2 (uncontrolled:000000000000, Jan 25 2022, 14:39:15)
[OpenJDK 64-Bit Server VM (Azul Systems, Inc.)] on java11.0.18

>>> 
Java Traceback:
Traceback (most recent call last):
  File "<input>", line 1, in <module>
	at com.sun.proxy.$Proxy58.startPublishing(Unknown Source)
	at com.inductiveautomation.ignition.examples.scripting.client.ClientScriptModule.startPublishingOnServer(ClientScriptModule.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException

	at org.python.core.Py.JavaError(Py.java:547)
	at org.python.core.Py.JavaError(Py.java:538)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:192)
	at com.inductiveautomation.ignition.common.script.ScriptManager$ReflectedInstanceFunction.__call__(ScriptManager.java:552)
	at org.python.core.PyObject.__call__(PyObject.java:461)
	at org.python.core.PyObject.__call__(PyObject.java:465)
	at org.python.pycode._pyx6.f$0(<input>:1)
	at org.python.pycode._pyx6.call_function(<input>)
	at org.python.core.PyTableCode.call(PyTableCode.java:173)
	at org.python.core.PyCode.call(PyCode.java:18)
	at org.python.core.Py.runCode(Py.java:1687)
	at org.python.core.Py.exec(Py.java:1731)
	at org.python.util.PythonInterpreter.exec(PythonInterpreter.java:277)
	at org.python.util.InteractiveInterpreter.runcode(InteractiveInterpreter.java:130)
	at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:626)
	at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:614)
	at java.desktop/javax.swing.SwingWorker$1.call(Unknown Source)
	at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
	at java.desktop/javax.swing.SwingWorker.run(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.reflect.UndeclaredThrowableException
	at com.sun.proxy.$Proxy58.startPublishing(Unknown Source)
	at com.inductiveautomation.ignition.examples.scripting.client.ClientScriptModule.startPublishingOnServer(ClientScriptModule.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
	... 19 more
Caused by: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: Could not initialize class com.rti.dds.domain.DomainParticipantFactory
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.newGatewayException(GatewayInterface.java:351)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:325)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:278)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.moduleInvokeSafe(GatewayInterface.java:917)
	at com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory$DynamicRPCHandler.invoke(ModuleRPCFactory.java:53)
	... 26 more
Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.rti.dds.domain.DomainParticipantFactory
	at com.inductiveautomation.ignition.examples.scripting.common.HMItoSICCommand.dispose(HMItoSICCommand.java:43)
	at com.inductiveautomation.ignition.examples.scripting.common.HMItoSICCommand.startPublishing(HMItoSICCommand.java:91)
	at com.inductiveautomation.ignition.examples.scripting.gateway.GatewayScriptModule.startPublishing(GatewayScriptModule.java:16)
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(null)
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(null)
	at java.lang.reflect.Method.invoke(null)
	at com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ModuleInvoke.invoke(ModuleInvoke.java:167)
	at com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost(Gateway.java:434)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
	at com.inductiveautomation.ignition.gateway.bootstrap.MapServlet.service(MapServlet.java:86)
	at org.eclipse.jetty.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1410)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
	at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1665)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:527)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:131)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:578)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:223)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1570)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1383)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1543)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1305)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at com.inductiveautomation.catapult.handlers.RemoteHostNameLookupHandler.handle(RemoteHostNameLookupHandler.java:121)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:301)
	at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)
	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
	at org.eclipse.jetty.server.Server.handle(Server.java:563)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:505)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:762)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:497)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:416)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:385)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:272)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new$0(AdaptiveExecutionStrategy.java:140)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:934)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1078)
	at java.lang.Thread.run(null)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
	at com.sun.proxy.$Proxy58.startPublishing(Unknown Source)
	at com.inductiveautomation.ignition.examples.scripting.client.ClientScriptModule.startPublishingOnServer(ClientScriptModule.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException
>>> 

Based on this error I'm assuming you're using the RPC pattern because this looks to be executing in gateway scope.

Start by looking at the module.xml file that was generated and included with your module to make sure all of your expected dependencies are listed and have a "G" scope specifier.

1 Like

Or, if you want execution to actually happen in the local designer scope, package your dependency appropriately. What solution works best is up to you.

Per the module.xml file, it looks like the jar (nddsjava.jar) is included in the designer, gateway, and client scopes. See below:

<?xml version="1.0" encoding="UTF-8"?>
<modules>
	<module>
		<name>Ignition DDS Module</name>
		<id>com.inductiveautomation.ignition.examples.scripting.IgnitionDDSModule</id>
		<version>0.0.1-SNAPSHOT</version>
		<description>This module allows Ignition to publish and subscribe to DDS topics.</description>
		<requiredIgnitionVersion>8.1.11</requiredIgnitionVersion>
		<freeModule>false</freeModule>
		<hook scope="G">com.inductiveautomation.ignition.examples.scripting.gateway.GatewayHook</hook>
		<hook scope="C">com.inductiveautomation.ignition.examples.scripting.client.ClientHook</hook>
		<hook scope="D">com.inductiveautomation.ignition.examples.scripting.designer.DesignerHook</hook>
		<requiredFrameworkVersion>8</requiredFrameworkVersion>
		<jar scope="CD">client-0.0.1-SNAPSHOT.jar</jar>
		<jar scope="CDG">nddsjava.jar</jar>
		<jar scope="CDG">common-0.0.1-SNAPSHOT.jar</jar>
		<jar scope="D">designer-0.0.1-SNAPSHOT.jar</jar>
		<jar scope="G">gateway-0.0.1-SNAPSHOT.jar</jar>
	</module>
</modules>

Ok, make sure the class file referenced in the error actually exists in that JAR, and that you aren't missing any other dependencies.

edit: though I'd bet what is actually happening is that it does exist, and an Exception is being thrown during its static initializer.

3 Likes