Including Third-Party Dependencies in Ignition Module

Hi everyone,

I'm trying to include third-party dependencies in my Ignition module. I attempted this using the perspective-component from the Ignition SDK GitHub by adding the dependencies I need to the build.gradle.kts file in the common scope but it didn't import the dependencies correctly.

Could anyone provide examples or suggest convenient ways to do this? Thanks in advance for your help!
@GeraldB

Can you post what you’ve currently got in your build.gradle.kts?

What did you actually try, and what was the actual result?

Note that you need to use our explicit modlApi dependency marker to tell the module plugin to actually include your dependency in the final output module. implementation just tells Gradle to use it for compilation, assuming that you will have it already in your final destination.

1 Like

Thanks for the answers, loading the dependencies with implementation is exactly what i tried that's why it didn't include them in the .modl file. I now tried to use modlApi like this:

plugins {
    `java-library`
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

repositories {
    mavenCentral()
    maven {
        url = uri("https://company/com/maven2")
    }
    mavenLocal()
    flatDir {
        dirs("libs")
    }
}


dependencies {
    // compileOnly is the gradle equivalent to "provided" scope.  Here we resolve the dependencies via the
    // declarations in the gradle/libs.versions.toml file
    compileOnly(libs.ignition.common)
    compileOnly(libs.ignition.perspective.common)
    compileOnly(libs.google.guava)
    compileOnly(libs.ia.gson)

    // https://mvnrepository.com/artifact/io.netty/netty-transport-native-epoll
    modlApi("io.netty:netty-transport-native-epoll:4.2.0.Final")
    // https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-common
    modlApi("org.apache.hadoop:hadoop-common:3.4.1")
    // https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-client
    modlApi("org.apache.hadoop:hadoop-client:3.4.1")
    // https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-hdfs
    modlApi("org.apache.hadoop:hadoop-hdfs:3.4.1")
}

Now it correctly includes the hadoop jar files in the .modl.

when i install the module to my Gateway i get the following error:

ModuleManager	18Apr2025 11:30:35	Error starting module org.fakester.radcomponent
java.lang.NoClassDefFoundError: com/inductiveautomation/perspective/gateway/api/PerspectiveContext

at org.fakester.gateway.RadGatewayHook.startup(RadGatewayHook.java:39)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$LoadedModule.startup(ModuleManagerImpl.java:2446)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.startupModule(ModuleManagerImpl.java:1188)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$2.call(ModuleManagerImpl.java:734)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.executeModuleOperation(ModuleManagerImpl.java:913)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.installModuleInternal(ModuleManagerImpl.java:700)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$InstallCommand.doExecute(ModuleManagerImpl.java:1915)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$AbstractModuleCommand.execute(ModuleManagerImpl.java:1864)

at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl$Receiver.receiveCall(ModuleManagerImpl.java:1820)

at com.inductiveautomation.ignition.gateway.redundancy.QueueableMessageReceiver.receiveCall(QueueableMessageReceiver.java:47)

at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl.dispatchMessage(RedundancyManagerImpl.java:1044)

at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl$ExecuteTask.run(RedundancyManagerImpl.java:1112)

at com.inductiveautomation.ignition.common.execution.impl.BasicExecutionEngine$ThrowableCatchingRunnable.run(BasicExecutionEngine.java:550)

at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)

at java.base/java.util.concurrent.FutureTask.run(Unknown Source)

at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.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.ClassNotFoundException: com.inductiveautomation.perspective.gateway.api.PerspectiveContext

at java.base/java.net.URLClassLoader.findClass(Unknown Source)

at com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:37)

at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

at com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader.loadClass(ModuleClassLoader.java:85)

at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

... 19 common frames omitted

what i don't really understand because i didn't change anything else than the build.gradle.ks in my common scope as shown above

It looks like you're missing the configuration stating your module depends on the Perspective module.

I just checked it and it's there, as i mentioned i didn't change anything besides my build.gradle.kts in the common folder. Here the main build.gradle.kts of the project:

import java.util.concurrent.TimeUnit


plugins {
    base
    // the ignition module plugin: https://github.com/inductiveautomation/ignition-module-tools
    id("io.ia.sdk.modl") version("0.1.1")
    id("org.barfuin.gradle.taskinfo") version "1.3.0"
}

allprojects {
    version = "1.0.0"
    group = "org.fakester"
}

ignitionModule {
    // name of the .modl file to build
    fileName.set("HadoopDependencies")

    // module xml configuration
    name.set("HadoopDependencies")
    id.set("org.fakester.radcomponent")
    moduleVersion.set("${project.version}")
    moduleDescription.set("A module that adds Hadoop Dependencies.")
    requiredIgnitionVersion.set("8.1.8")
    license.set("license.html")

    // If we depend on other module being loaded/available, then we specify IDs of the module we depend on,
    // and specify the Ignition Scope that applies. "G" for gateway, "D" for designer, "C" for VISION client
    // (this module does not run in the scope of a Vision client, so we don't need a "C" entry here)
    moduleDependencies.put("com.inductiveautomation.perspective", "DG")

    // map of 'Gradle Project Path' to Ignition Scope in which the project is relevant.  This is is combined with
    // the dependency declarations within the subproject's build.gradle.kts in order to determine which
    // dependencies need to be bundled with the module and added to the module.xml.
    projectScopes.putAll(
        mapOf(
            ":gateway" to "G",
            ":web" to "G",
            ":designer" to "D",
            ":common" to "GD"
        )
    )

    // 'hook classes' are the things that Ignition loads and runs when your module is installed.  This map tells
    // Ignition which classes should be loaded in a given scope.
    hooks.putAll(
        mapOf(
            "org.fakester.gateway.RadGatewayHook" to "G",
            "org.fakester.designer.RadDesignerHook" to "D"
        )
    )
    skipModlSigning.set(true)
}


val deepClean by tasks.registering {
    dependsOn(allprojects.map { "${it.path}:clean" })
    description = "Executes clean tasks and remove node plugin caches."
    doLast {
        delete(file(".gradle"))
    }
}

Does the Gateway you're installing this on have the Perspective module installed and running? Can you share the module.xml file from your .modl file?

3 Likes

i missed that the perspective module wasn't running. It works now, thanks!

1 Like

Hi Kevin, a further question appeard. I installed the modl to the Gateway and tried to access my 3rd party dependencies through the scripting console like this:

and it correctly imports, and then executes the following code that i have in Project Library/hadoopTest:

def testFct():
	system.tag.writeBlocking('[default]test',"inFunc")
	try:
		import org.apache.hadoop.conf.Configuration as Configuration
		system.tag.writeBlocking('[default]test',"foundImport")
		conf = Configuration();
		if(conf):
			system.tag.writeBlocking('[default]test',"foundImport+conf")
		else:
			system.tag.writeBlocking('[default]test',"foundImport-conf")
	except Exception as e:
			system.tag.writeBlocking('[default]test',str(e))

image

But when i create a Gateway Event Tag Change and do exactly the same:

it cannot find the dependencies:
image

I checked it in the module.xml of my build and all dependencies have jar scope="DG"

Any idea what i did wrong?

Modules get loaded in their own isolated ClassLoader, lower in the hierarchy than the platform.

The classes brought in by your module and its dependencies are not available to Gateway-scoped scripting. It only works by chance in Client/Designer scope because there is no ClassLoader hierarchy there - it's entirely flat.

The supported approach is to add scripting functions system.whatever.foo() that in turn use the libraries your module includes.

1 Like

Thanks for the answer! i will try that and give a short update afterwards!