Skip to content

Commit 1baaa82

Browse files
authored
IGN-6250: Support unsigned modules in gradle-module-plugin (#30)
* IGN-6250: Support unsigned modules in gradle-module-plugin This adds support to skip the module signing requirement and task for the gradle-module-plugin. This adds the `skipModlSigning` boolean property to the ModuleSetting extension. This property doesn't disrupt task ordering or execution, it merely alters the assignment of task input and outputs to assign the unsigned module file and skips the `signModule` task. It also adds some additional test cases to test that the `buildResult.json`'s filename points to the unsigned and that the signed module is not built if the prop is true.
1 parent 568467b commit 1baaa82

10 files changed

Lines changed: 189 additions & 22 deletions

File tree

generator/generator-core/src/main/resources/templates/buildscript/root.build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,9 @@ ignitionModule {
7878
// the path from the root documentation dir to the index file.
7979
// documentationIndex.set("index.html")
8080

81+
/*
82+
* Optional unsigned modl settings. If true, modl signing will be skipped. This is not for production and should
83+
* be used merely for development testing
84+
*/
85+
// skipModlSigning = false
8186
}

generator/generator-core/src/main/resources/templates/buildscript/root.build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,10 @@ ignitionModule {
6767
hooks = mapOf(
6868
<HOOK_CLASS_CONFIG>
6969
)
70+
71+
/*
72+
* Optional unsigned modl settings. If true, modl signing will be skipped. This is not for production and should
73+
* be used merely for development testing
74+
*/
75+
// skipModlSigning.set(false)
7076
}

gradle-module-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,4 @@ the result.
165165
* v0.1.0-SNAPSHOT-16 - changed modlImplementation configuration to not resolve transitive dependencies
166166
* v0.1.0-SNAPSHOT-17 - modlImplementation and modlApi configurations have been replaced with a single `modlDependency` configuration to simplify dependency marking and avoid confusing differences between compile-time and modl runtime environments,
167167
* v0.1.0-SNAPSHOT-18 - reverted to separate modlImplementation and modlApi configurations. However, modlImplementation retains the same resolution semantics as gradle's `implementation`, while also fully resolving `modlImplementation`'s transitive dependencies for inclusion into the modl. This change results in logical handling of dependencies in IDE/gradle environments, while also ensuring that needed dependencies are bundled in the module for use a runtime.
168+
* v0.1.1-SNAPSHOT-1 - added the `skipModlSigning` ModuleSetting property to support skipping the `signModl` task and populating the `build.json`'s filename prop with the unsigned module allowing development without sharing/needing the signing keys.

gradle-module-plugin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repositories {
1919
}
2020

2121
group = "io.ia.sdk"
22-
version = "0.1.0"
22+
version = "0.1.1-SNAPSHOT-1"
2323

2424
configurations {
2525
val functionalTestImplementation by registering {

gradle-module-plugin/src/functionalTest/kotlin/io/ia/sdk/gradle/modl/IgnitionModulePluginFunctionalTest.kt

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import org.gradle.testkit.runner.GradleRunner
1111
import java.io.File
1212
import java.nio.file.Files
1313
import java.nio.file.Path
14+
import kotlin.io.path.absolutePathString
1415
import kotlin.test.Test
16+
import kotlin.test.assertFalse
1517
import kotlin.test.assertTrue
1618

1719
/**
@@ -146,6 +148,77 @@ open class IgnitionModulePluginFunctionalTest : BaseTest() {
146148
assertTrue(expected.exists(), "Built and signed module exists")
147149
}
148150

151+
@Test
152+
fun `gateway scoped unsigned module passes config and builds with signing credentials`() {
153+
val rootDir = tempFolder.newFolder("gwScopedUnsignedConfigAndBuild").toPath()
154+
val moduleName = "Great Tests"
155+
val scopes = "G"
156+
val packageName = "le.examp"
157+
println(rootDir.absolutePathString())
158+
159+
val config: GeneratorConfig = GeneratorConfigBuilder()
160+
.moduleName(moduleName)
161+
.scopes(scopes)
162+
.packageName(packageName)
163+
.parentDir(rootDir)
164+
.useRootForSingleScopeProject(false)
165+
.build()
166+
167+
val projectDir = ModuleGenerator.generate(config)
168+
projectDir.resolve("build.gradle").toFile().let {
169+
it.writeText(
170+
it.readLines().map { line ->
171+
if (" // skipModlSigning = false" == line) {
172+
" skipModlSigning = true"
173+
} else {
174+
line
175+
}
176+
}.joinToString(System.lineSeparator())
177+
)
178+
}
179+
prepareSigningTestResources(rootDir.resolve(nameToDirName(moduleName)))
180+
181+
runTask(projectDir.toFile(), "build")
182+
val expected = expectedSignedModule(projectDir, moduleName)
183+
184+
assertFalse(expected.exists(), "Built and signed module exists when `skipModlSigning` was true")
185+
}
186+
187+
@Test
188+
fun `gateway scoped unsigned module passes config and builds without signing credentials`() {
189+
val rootDir = tempFolder.newFolder("gwScopedUnsignedNoConfigAndBuild").toPath()
190+
val moduleName = "Great Tests"
191+
val scopes = "G"
192+
val packageName = "le.examp"
193+
println(rootDir.absolutePathString())
194+
195+
val config: GeneratorConfig = GeneratorConfigBuilder()
196+
.moduleName(moduleName)
197+
.scopes(scopes)
198+
.packageName(packageName)
199+
.parentDir(rootDir)
200+
.useRootForSingleScopeProject(false)
201+
.build()
202+
203+
val projectDir = ModuleGenerator.generate(config)
204+
projectDir.resolve("build.gradle").toFile().let {
205+
it.writeText(
206+
it.readLines().map { line ->
207+
if (" // skipModlSigning = false" == line) {
208+
" skipModlSigning = true"
209+
} else {
210+
line
211+
}
212+
}.joinToString(System.lineSeparator())
213+
)
214+
}
215+
216+
runTask(projectDir.toFile(), "build")
217+
val expected = expectedSignedModule(projectDir, moduleName)
218+
219+
assertFalse(expected.exists(), "Built and signed module exists when `skipModlSigning` was true")
220+
}
221+
149222
private fun expectedSignedModule(rootModuleDir: Path, moduleName: String): File {
150223
val expectedLocation = rootModuleDir.resolve("build").toAbsolutePath()
151224
return File("$expectedLocation/${signedModuleName(moduleName)}")

gradle-module-plugin/src/functionalTest/kotlin/io/ia/sdk/gradle/modl/task/ModuleBuildReportTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,45 @@ class ModuleBuildReportTest : BaseTest() {
114114
assertEquals(1, report.metaInfo.size, "metainfo should be added to report if configured in plugin")
115115
assertEquals("some string value", report.metaInfo["test.key"], "build report should hold metainfo value")
116116
}
117+
118+
@Test
119+
fun `build report contains unsigned modl entry when skipModlSigning`() {
120+
val parentDir: File = tempFolder.newFolder("build_report_unsigned")
121+
val moduleName = "Foo"
122+
123+
val config = GeneratorConfigBuilder()
124+
.moduleName(moduleName)
125+
.scopes("GCD")
126+
.packageName("check.my.signage")
127+
.parentDir(parentDir.toPath())
128+
.debugPluginConfig(true)
129+
.rootPluginConfig(
130+
"""
131+
id("io.ia.sdk.modl")
132+
""".trimIndent()
133+
)
134+
.build()
135+
136+
val projectDir = ModuleGenerator.generate(config)
137+
projectDir.resolve("build.gradle").toFile().let {
138+
it.writeText(
139+
it.readLines().map { line ->
140+
if (" // skipModlSigning = false" == line) {
141+
" skipModlSigning = true"
142+
} else {
143+
line
144+
}
145+
}.joinToString(System.lineSeparator())
146+
)
147+
}
148+
149+
runTask(projectDir.toFile(), "modlReport")
150+
val buildDir = projectDir.resolve("build")
151+
val buildJson = buildDir.resolve("buildResult.json")
152+
153+
assertTrue(buildJson.toFile().exists(), "buildResult.json should exist in the build dir")
154+
val report = jsonToAssemblyManifest(buildJson.toFile().readText())
155+
assertNotNull(report.fileName, "filename entry was not present")
156+
assertEquals("Foo.unsigned.modl", report.fileName, "filename entry was not present")
157+
}
117158
}

gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/IgnitionModlPlugin.kt

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ class IgnitionModlPlugin : Plugin<Project> {
6767

6868
project.afterEvaluate {
6969
settings.moduleVersion.convention(project.provider { project.version.toString() })
70-
}
71-
72-
project.afterEvaluate {
7370
if (settings.applyInductiveArtifactRepo.get()) {
7471
addInductiveAutoRepos(project)
7572
}
@@ -196,17 +193,43 @@ class IgnitionModlPlugin : Plugin<Project> {
196193
// task that signs the module, using [http://github.com/inductiveautomation/module-signer] to do so
197194
val sign = root.tasks.register(SignModule.ID, SignModule::class.java) { signTask ->
198195
signTask.unsigned.set(zip.flatMap { it.unsignedModule })
196+
signTask.skipSigning.set(settings.skipModlSigning)
197+
signTask.onlyIf {
198+
settings.skipModlSigning.get().also { useUnsigned ->
199+
if (useUnsigned) {
200+
root.logger.warn(
201+
"useUnsignedModule specified in Module Settings. Module Signing will be skipped"
202+
)
203+
}
204+
}.not()
205+
}
199206
}
200207

201208
val checksum = root.tasks.register(Checksum.ID, Checksum::class.java) { checksum ->
202209
checksum.hashAlgorithm.set(settings.checksumAlgorithm)
203-
checksum.signedModl.set(sign.flatMap { it.signed })
204-
checksum.dependsOn(sign)
210+
checksum.modlFile.set(
211+
settings.skipModlSigning.flatMap { useUnsigned ->
212+
if (useUnsigned) {
213+
zip.flatMap { it.unsignedModule }
214+
} else {
215+
sign.flatMap { it.signed }
216+
}
217+
}
218+
)
219+
checksum.dependsOn(sign, zip)
205220
}
206221

207222
val buildReport = root.tasks.register(ModuleBuildReport.ID, ModuleBuildReport::class.java) { report ->
208223
report.metaInfo.putAll(settings.metaInfo)
209-
report.modlFile.set(sign.flatMap { it.signed })
224+
report.modlFile.set(
225+
settings.skipModlSigning.flatMap { useUnsigned ->
226+
if (useUnsigned) {
227+
zip.flatMap { it.unsignedModule }
228+
} else {
229+
sign.flatMap { it.signed }
230+
}
231+
}
232+
)
210233
report.moduleId.set(settings.id)
211234
report.moduleName.set(settings.name)
212235
report.moduleVersion.set(settings.moduleVersion)
@@ -244,7 +267,15 @@ class IgnitionModlPlugin : Plugin<Project> {
244267
}
245268

246269
root.tasks.register(Deploy.ID, Deploy::class.java) {
247-
it.module.convention(sign.flatMap { signTask -> signTask.signed })
270+
it.module.convention(
271+
settings.skipModlSigning.flatMap { useUnsigned ->
272+
if (useUnsigned) {
273+
zip.flatMap { it.unsignedModule }
274+
} else {
275+
sign.flatMap { it.signed }
276+
}
277+
}
278+
)
248279
}
249280

250281
// root project can be a module artifact contributor, so we'll apply the tasks to root as well (may opt out)

gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/extension/ModuleSettings.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import org.gradle.api.model.ObjectFactory
88
import org.gradle.api.provider.MapProperty
99
import org.gradle.api.provider.Property
1010

11-
public const val EXTENSION_NAME = "ignitionModule"
11+
const val EXTENSION_NAME = "ignitionModule"
1212

1313
/**
1414
* A data class representing the configuration of an Ignition module that is assembled by the gradle plugin.
@@ -19,8 +19,8 @@ public const val EXTENSION_NAME = "ignitionModule"
1919
* reflected in the module.xml file that is generated and placed in the root of your assembled module by the plugin
2020
* 2. It identifies the project or subprojects that are to be included by your
2121
*/
22-
@Suppress("UnstableApiUsage")
2322
open class ModuleSettings @javax.inject.Inject constructor(objects: ObjectFactory) {
23+
2424
/**
2525
* The 'name' of your module as is displayed in the Ignition Gateway configuration page when the module is installed.
2626
*/
@@ -99,7 +99,7 @@ open class ModuleSettings @javax.inject.Inject constructor(objects: ObjectFactor
9999

100100
/**
101101
* If the module is a 'free' (non-commercial) module, without licensing restrictions, set this
102-
* value to Boolean.TRUE. Default is [Boolean.FALSE]
102+
* value to `true`. Default is `false`
103103
*
104104
* Optional
105105
*/
@@ -157,9 +157,16 @@ open class ModuleSettings @javax.inject.Inject constructor(objects: ObjectFactor
157157
*/
158158
val documentationIndex: Property<String> = objects.property(String::class.java)
159159

160+
/**
161+
* If `true` the signing task is not executed on the module and the unsigned module is used for all downstream
162+
* tasks. The default for this is `false` and should be in most scenarios. The exception being if you would like to
163+
* build an unsigned module for a development gateway without the signing keyfile present on the filesystem.
164+
*/
165+
val skipModlSigning: Property<Boolean> = objects.property(Boolean::class.javaObjectType).convention(false)
166+
160167
init {
161168
license.convention("")
162-
freeModule.convention(java.lang.Boolean.FALSE)
169+
freeModule.convention(false)
163170
moduleDescription.convention("")
164171
requireFromPlatform.convention(emptyMap())
165172
requiredIgnitionVersion.convention("8.0.0")

gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/task/Checksum.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,22 @@ open class Checksum @Inject constructor(_objects: ObjectFactory, _layout: Projec
3030

3131
@get:InputFile
3232
@get:PathSensitive(PathSensitivity.RELATIVE)
33-
val signedModl: RegularFileProperty = _objects.fileProperty()
33+
val modlFile: RegularFileProperty = _objects.fileProperty()
3434

3535
@get:OutputFile
3636
val checksumJson: RegularFileProperty = _objects.fileProperty().convention(
3737
_layout.buildDirectory.file("checksum/checksum.json")
3838
)
3939

4040
/**
41-
* Hash function to use against the signed module file to get the file's checksum.
41+
* Hash function to use against the module file to get the file's checksum.
4242
*/
4343
@get:Input
4444
val hashAlgorithm: Property<HashAlgorithm> = _objects.property(HashAlgorithm::class.java)
4545

4646
@TaskAction
4747
fun execute() {
48-
val module = signedModl.get().asFile
48+
val module = modlFile.get().asFile
4949

5050
if (module.exists()) {
5151
val digest = Files.asByteSource(module).hash(hashImpl(hashAlgorithm.get()))
@@ -59,7 +59,7 @@ open class Checksum @Inject constructor(_objects: ObjectFactory, _layout: Projec
5959
init {
6060
this.group = PLUGIN_TASK_GROUP
6161
this.description = """
62-
|Executes a hash function (default SHA256) against the signed modl file, and emits a json file to the build
62+
|Executes a hash function (default SHA256) against the modl file, and emits a json file to the build
6363
| directory containing the digest.""".trimMargin()
6464
}
6565
}

gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/task/SignModule.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ import javax.inject.Inject
3333
/**
3434
* Signs the module file, using credentials provided by the task running.
3535
*/
36-
@Suppress("UnstableApiUsage")
3736
open class SignModule @Inject constructor(_providers: ProviderFactory, _objects: ObjectFactory) : DefaultTask() {
3837
companion object {
3938
const val ID = "signModule"
39+
private const val SKIP = "<SKIP_SIGNING_ENABLED>" // placeholder prop value for skipModuleSigning
4040
}
4141

4242
init {
@@ -48,6 +48,9 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
4848
@get:InputFile
4949
val unsigned: RegularFileProperty = _objects.fileProperty()
5050

51+
@get:Input
52+
val skipSigning: Property<Boolean> = _objects.property(Boolean::class.java).convention(false)
53+
5154
// the signed modl file
5255
@get:OutputFile
5356
val signed: Provider<RegularFile> = unsigned.map {
@@ -60,7 +63,7 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
6063
@get:Optional
6164
val keystorePath: Property<String> = _objects.property(String::class.java).convention(
6265
_providers.provider {
63-
propOrLogError(KEYSTORE_FILE_FLAG, "keystore file location")
66+
if (skipSigning.get()) SKIP else propOrLogError(KEYSTORE_FILE_FLAG, "keystore file location")
6467
}
6568
)
6669

@@ -96,7 +99,7 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
9699
@get:Input
97100
val keystorePw: Property<String> = _objects.property(String::class.java).convention(
98101
_providers.provider {
99-
propOrLogError(KEYSTORE_PW_FLAG, "keystore password")
102+
if (skipSigning.get()) SKIP else propOrLogError(KEYSTORE_PW_FLAG, "keystore password")
100103
}
101104
)
102105

@@ -109,7 +112,7 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
109112
@get:Input
110113
val certFilePath: Property<String> = _objects.property(String::class.java).convention(
111114
_providers.provider {
112-
propOrLogError(CERT_FILE_FLAG, "certificate file location")
115+
if (skipSigning.get()) SKIP else propOrLogError(CERT_FILE_FLAG, "certificate file location")
113116
}
114117
)
115118

@@ -140,7 +143,7 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
140143
@get:Input
141144
val alias: Property<String> = _objects.property(String::class.java).convention(
142145
_providers.provider {
143-
propOrLogError(ALIAS_FLAG, "certificate alias")
146+
if (skipSigning.get()) SKIP else propOrLogError(ALIAS_FLAG, "certificate alias")
144147
}
145148
)
146149

@@ -152,7 +155,7 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects:
152155
@get:Input
153156
val certPw: Property<String> = _objects.property(String::class.java).convention(
154157
_providers.provider {
155-
propOrLogError(CERT_PW_FLAG, "certificate password")
158+
if (skipSigning.get()) SKIP else propOrLogError(CERT_PW_FLAG, "certificate password")
156159
}
157160
)
158161

0 commit comments

Comments
 (0)