Declared sandbox build targets

- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 03:41:18 -04:00
1 parent fc810221bb
commit 52f1a18a1b
7 files changed
+122 -229

No files matched your search

+16 -24
View File
@@ -248,34 +248,26 @@ which is what a board reader means.
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```sh
./gradlew buildAll
```
builds every platform artifact into the workspace-level `../PluginOuts/` directory: the Bukkit jar
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
(1.21.1, 26.1.2, 26.2 and 26.3):
This builds and verifies the Bukkit plugin, API, SPI and the Fabric, Forge and NeoForge jars
for every Minecraft version declared in `gradle/mod-targets.json`. Artifacts stay in the
producing module's `build/` directory. The packed Bukkit plugin is in `build/libs/`, the
API is in `build/api/`, the SPI is in `spi/build/libs/`, and mod jars are in
`adapters/<loader>/build/<minecraft>/libs/`.
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Use `./gradlew buildBukkit` for the plugin, or `verify<Loader><mc>` for one mod jar,
for example `verifyFabric26_2` or `verifyNeoForge1_21_1`. `buildFabric`, `buildForge`
and `buildNeoforge` select the `minecraftVersion` Gradle property. `./gradlew checkAll`
runs every check plus the complete mod matrix. Forge 1.21.1 compiles its tests because
Forge 52 cannot bootstrap Minecraft outside FML.
Per-platform tasks: `./gradlew buildBukkit`, and `build<Loader><mc>` for one mod jar, for example
`buildFabric26_2` or `buildNeoForge1_21_1`. `buildFabric`, `buildForge` and `buildNeoforge` export the
26.3 jars. `./gradlew checkAll` runs every check, then builds and verifies all twelve mod jars and runs their
unit tests; Forge 1.21.1 only compiles its tests because Forge 52 cannot bootstrap Minecraft outside
FML. The
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
changes only the consumer directory. Both tasks copy every verified platform jar into
`../PluginOuts/`.
Multiplexor reads `gradle/multiplexor.gradle` to discover targets and build each in a
separate source sandbox. Use its project build and deployment commands to export jars
or install them into managed test instances. Gradle builds do not copy artifacts to
external destinations.
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@"
./gradlew buildAll "$@"
+19 -188
View File
@@ -9,9 +9,6 @@ import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.api.file.CopySpec
import org.gradle.api.file.DeleteSpec
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.initialization.IncludedBuild
@@ -20,8 +17,6 @@ import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 Arcane Arts (Volmit Software)
@@ -59,13 +54,6 @@ apply plugin: 'art.arcane.volmit-packaging'
apply from: file('gradle/runtime-relocations.gradle')
apply from: file('gradle/volmlib-stamp.gradle')
interface IrisFileOperations {
@Inject
FileSystemOperations getFileSystemOperations()
}
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
group = 'art.arcane'
version = providers.gradleProperty('irisVersion').getOrElse('4.3.5')
@@ -99,41 +87,11 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
File pluginOutDirectory = file(providers.gradleProperty('pluginOutDirectory').getOrElse('../PluginOuts'))
File legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${bukkitArtifactName}").get().asFile
File fabricArtifact = layout.projectDirectory.file("adapters/fabric/build/${minecraftVersion}/libs/${fabricArtifactName}").asFile
File forgeArtifact = layout.projectDirectory.file("adapters/forge/build/${minecraftVersion}/libs/${forgeArtifactName}").asFile
File neoForgeArtifact = layout.projectDirectory.file("adapters/neoforge/build/${minecraftVersion}/libs/${neoForgeArtifactName}").asFile
File consumerDirectory = layout.buildDirectory.dir('consumer').get().asFile
FileTree staleConsumerBukkitJars = fileTree(consumerDirectory) {
include('Iris v* [CraftBukkit] *.jar')
}
FileTree staleBukkitOutJars = fileTree(pluginOutDirectory) {
include('Iris v* [CraftBukkit] *.jar')
}
String selectedModdedSuffix = minecraftVersion.replace('.', '_')
apply plugin: ApiGenerator
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
// Multiplexor workspace when it exists, with a repo-local fallback for fresh clones.
File multiplexorConsumerLocation = new File(rootDir, '../../[Minecraft Server]/consumers').canonicalFile
String defaultConsumerLocation = multiplexorConsumerLocation.isDirectory()
? multiplexorConsumerLocation.absolutePath
: layout.buildDirectory.dir('consumers').get().asFile.absolutePath
String consumerLocation = providers.gradleProperty('location')
.getOrElse(defaultConsumerLocation)
String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins"
String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods"
String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods"
String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods"
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
ext.nativeDevelopmentAdapters = nmsBindings
nmsBindings.each { String key ->
@@ -240,65 +198,14 @@ tasks.named('jar', Jar).configure {
exclude('com/fren_gor/**')
}
doFirst {
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(legacyBukkitJar) }
}
archiveFileName.set(bukkitArtifactName)
}
tasks.register('iris', Copy) {
group = 'iris'
dependsOn('packedJar')
from({ tasks.named('packedJar').get().outputJar })
rename { String ignored -> bukkitArtifactName }
into(layout.buildDirectory.dir('consumer'))
doFirst {
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) }
}
}
tasks.register('buildSwiftSwamp') {
group = 'iris'
description = 'Builds every platform jar and drops them in the local BUILDS output directory.'
outputs.upToDateWhen { false }
dependsOn('iris', "verifyFabric${selectedModdedSuffix}", "verifyForge${selectedModdedSuffix}",
"verifyNeoForge${selectedModdedSuffix}")
File swiftSwampOut = file('C://VolmitSoftware/BUILDS')
Map<String, File> swiftSwampSources = [
'Iris.jar' : consumerBukkitArtifact,
'Iris-Fabric.jar' : fabricArtifact,
'Iris-Forge.jar' : forgeArtifact,
'Iris-Neoforge.jar': neoForgeArtifact
]
doLast {
swiftSwampSources.each { String target, File source ->
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
spec.from(source)
spec.into(swiftSwampOut)
spec.rename { String ignored -> target }
}
}
println('')
println('=== Iris buildSwiftSwamp -> ' + swiftSwampOut + ' ===')
['Iris.jar', 'Iris-Fabric.jar', 'Iris-Forge.jar', 'Iris-Neoforge.jar'].each { String jar ->
File produced = new File(swiftSwampOut, jar)
println(String.format(' %-24s %8d KB', jar, (long) (produced.length() / 1024)))
}
}
}
tasks.register('buildBukkit', Copy) {
group = 'iris'
dependsOn('packedJar')
from({ tasks.named('packedJar').get().outputJar })
rename { String ignored -> bukkitArtifactName }
into(pluginOutDirectory)
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar")
doFirst {
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
spec.delete(staleBukkitOutJars, legacyOutJar)
}
}
tasks.register('buildBukkit') {
group = 'build'
description = 'Builds and verifies the packed Bukkit plugin.'
dependsOn('packedJar', 'check', ':core:check', ':spi:check', ':adapters:bukkit:plugin:check')
dependsOn(nmsBindings.collect { String binding -> ":adapters:bukkit:nms:${binding}:check".toString() })
}
// The three loader jars come out of nested standalone Gradle builds driven through Exec. The outer
@@ -417,9 +324,7 @@ TaskProvider<Task> verifyModdedMatrix = tasks.register('verifyModdedMatrix') {
group = 'verification'
description = 'Builds, tests and verifies the Fabric, Forge and NeoForge jars of every supported Minecraft mod target.'
}
List<TaskProvider<Copy>> moddedExports = []
Map<String, TaskProvider<IrisModdedBuild>> selectedModdedBuilds = [:]
Map<String, TaskProvider<Copy>> selectedModdedExports = [:]
int matrixParallelism = Math.min(gradle.startParameter.maxWorkerCount, Math.max(1,
providers.gradleProperty('moddedBuildParallelism').getOrElse(
Math.min(12, Math.max(1, Runtime.runtime.availableProcessors() - 4)).toString()).toInteger()))
@@ -428,6 +333,9 @@ Provider<IrisModdedBuildService> matrixWorkers = gradle.sharedServices.registerI
maxParallelUsages.set(matrixParallelism)
}
List<String> nestedBuildArguments = [gradle.startParameter.buildCacheEnabled ? '--build-cache' : '--no-build-cache']
if (providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()) {
nestedBuildArguments.add('--no-daemon')
}
String nestedJvmArguments = providers.gradleProperty('org.gradle.jvmargs').getOrElse('') +
' -XX:ActiveProcessorCount=2'
if (gradle.startParameter.rerunTasks) {
@@ -512,26 +420,8 @@ irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
dependsOn(buildTarget)
}
verifyModdedMatrix.configure { dependsOn(buildTarget) }
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${targetMinecraft}-${loader}.jar")
FileTree staleTargetJars = fileTree(pluginOutDirectory) {
include("Iris v* [${label}] ${targetMinecraft}+*.jar")
}
TaskProvider<Copy> export = tasks.register("build${label}${suffix}", Copy) {
group = 'iris'
description = "Exports the verified ${label} ${targetMinecraft} jar to the plugin output directory."
dependsOn(buildTarget)
from(artifact)
into(pluginOutDirectory)
doFirst {
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
spec.delete(staleTargetJars, legacyOutJar)
}
}
}
moddedExports.add(export)
if (selected) {
selectedModdedBuilds[loader] = buildTarget
selectedModdedExports[loader] = export
}
}
}
@@ -541,24 +431,20 @@ if (selectedModdedBuilds.isEmpty()) {
tasks.register('buildFabric') {
group = 'iris'
description = "Exports the verified Fabric ${minecraftVersion} jar."
dependsOn(selectedModdedExports.fabric)
description = "Builds and verifies the Fabric ${minecraftVersion} jar."
dependsOn(selectedModdedBuilds.fabric)
}
tasks.register('buildForge') {
group = 'iris'
description = "Exports the verified Forge ${minecraftVersion} jar."
dependsOn(selectedModdedExports.forge)
description = "Builds and verifies the Forge ${minecraftVersion} jar."
dependsOn(selectedModdedBuilds.forge)
}
tasks.register('buildNeoforge') {
group = 'iris'
description = "Exports the verified NeoForge ${minecraftVersion} jar."
dependsOn(selectedModdedExports.neoforge)
}
tasks.named('check').configure {
dependsOn(selectedModdedBuilds.fabric)
description = "Builds and verifies the NeoForge ${minecraftVersion} jar."
dependsOn(selectedModdedBuilds.neoforge)
}
tasks.register('checkAll') {
@@ -643,67 +529,10 @@ tasks.named('buildBukkit').configure {
dependsOn('verifyBukkitArtifact')
}
tasks.named('iris').configure {
dependsOn('verifyBukkitArtifact')
}
tasks.register('buildAllToOut') {
group = 'iris'
description = 'Builds and verifies the Bukkit jar and every mod target jar into the plugin output directory.'
dependsOn('buildBukkit', ':spi:jar')
dependsOn(moddedExports)
doLast {
File[] jars = pluginOutDirectory.listFiles((java.io.FileFilter) { File f -> f.name.startsWith('Iris v') && f.name.endsWith('.jar') })
println('')
println('=== Iris buildAllToOut -> ' + pluginOutDirectory + ' ===')
if (jars != null) {
jars.sort { it.name }.each { File f ->
println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024)))
}
}
}
}
tasks.register('buildAll') {
group = 'iris'
outputs.upToDateWhen { false }
dependsOn('iris', 'buildAllToOut')
Map<String, List<Object>> consumerDrops = [
(bukkitConsumerPath) : ['Iris.jar', consumerBukkitArtifact],
(fabricConsumerPath) : ['Iris-Fabric.jar', fabricArtifact],
(forgeConsumerPath) : ['Iris-Forge.jar', forgeArtifact],
(neoForgeConsumerPath): ['Iris-Neoforge.jar', neoForgeArtifact]
]
doLast {
consumerDrops.each { String consumerPath, List<Object> drop ->
String keep = (String) drop.get(0)
File source = (File) drop.get(1)
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
consumerDrops.values().each { List<Object> other ->
String name = (String) other.get(0)
if (name != keep) {
spec.delete(new File(consumerPath, name))
}
}
}
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
spec.from(source)
spec.into(consumerPath)
spec.rename { String ignored -> keep }
}
}
println('')
println("=== Iris buildAll -> ${consumerLocation} ===")
println(" ${bukkitConsumerPath}/Iris.jar")
println(" ${fabricConsumerPath}/Iris-Fabric.jar")
println(" ${forgeConsumerPath}/Iris-Forge.jar")
println(" ${neoForgeConsumerPath}/Iris-Neoforge.jar")
}
}
tasks.register('buildPsychoLT') {
group = 'development'
description = 'Builds all verified Iris platform jars and stages them in the local test-server dropins and PluginOuts.'
dependsOn('buildAll')
group = 'build'
description = 'Builds and verifies the Bukkit plugin, API, SPI and every supported mod target.'
dependsOn('buildBukkit', 'irisApi', ':spi:jar', verifyModdedMatrix)
}
tasks.register('irisDev', Copy) {
@@ -1074,3 +903,5 @@ tasks.withType(PackedJar).configureEach {
NativeRuntimeArtifacts.verifyPacked(task.outputJar.get().asFile, nativeRuntimeModules, embeddedNatives())
}
}
apply from: file('gradle/multiplexor.gradle')
+2 -15
View File
@@ -37,11 +37,6 @@ public class ApiGenerator implements Plugin<Project> {
throw new GradleException("Publishing extension not found");
}
publishing.getRepositories().maven(repository -> {
repository.setName("deployDir");
repository.setUrl(targetDirectory(target).toURI());
});
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
publication.setGroupId(target.getGroup().toString());
publication.setArtifactId("iris");
@@ -49,27 +44,19 @@ public class ApiGenerator implements Plugin<Project> {
publication.artifact(task);
});
}
public static File targetDirectory(Project project) {
String dir = System.getenv("DEPLOY_DIR");
if (dir == null) {
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
}
return new File(dir);
}
}
abstract class GenerateApiTask extends DefaultTask {
public GenerateApiTask() {
setGroup("iris");
dependsOn("jar");
finalizedBy("publishMavenPublicationToDeployDirRepository");
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().get().getAsFile().getAbsolutePath()));
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
getInputFile().convention(jarTask.flatMap(Jar::getArchiveFile));
getOutputFile().convention(getProject().getLayout().file(getInputFile().getLocationOnly().map(input ->
new File(ApiGenerator.targetDirectory(getProject()), input.getAsFile().getName()))));
new File(getProject().getLayout().getBuildDirectory().dir("api").get().getAsFile(),
input.getAsFile().getName()))));
}
@InputFile
+4 -1
View File
@@ -50,9 +50,12 @@ public class ApiGeneratorTest {
assertEquals(jar.getArchiveFile().get().getAsFile(), api.getInputFile().get().getAsFile());
assertEquals(jar.getArchiveFileName().get(), api.getOutputFile().get().getAsFile().getName());
assertEquals(ApiGenerator.targetDirectory(project), api.getOutputFile().get().getAsFile().getParentFile());
assertEquals(project.getLayout().getBuildDirectory().dir("api").get().getAsFile(),
api.getOutputFile().get().getAsFile().getParentFile());
assertTrue(api.getFinalizedBy().getDependencies(api).isEmpty());
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
assertTrue(publishing.getRepositories().isEmpty());
MavenPublication publication = (MavenPublication) publishing.getPublications().getByName("maven");
assertEquals("art.arcane", publication.getGroupId());
assertEquals("iris", publication.getArtifactId());
+7
View File
@@ -217,6 +217,13 @@ def generateTemplates = tasks.register('generateTemplates', Copy) {
return 'development'
},
commit: providers.provider {
String sourceHash = providers.environmentVariable('MULTIPLEXOR_SOURCE_HASH').orNull
if (sourceHash != null) {
if (!(sourceHash ==~ /[a-f0-9]{64}/)) {
throw new GradleException('MULTIPLEXOR_SOURCE_HASH must be a SHA-256 source fingerprint')
}
return "source-${sourceHash}"
}
String commitId = null
Exception failure = null
try {
+73
View File
@@ -0,0 +1,73 @@
import org.gradle.jvm.tasks.Jar
Closure<Integer> compareMinecraft = { String left, String right ->
List<Integer> leftParts = left.tokenize('.').collect { String part -> Integer.parseInt(part) }
List<Integer> rightParts = right.tokenize('.').collect { String part -> Integer.parseInt(part) }
for (int index = 0; index < Math.max(leftParts.size(), rightParts.size()); index++) {
int comparison = Integer.compare(index < leftParts.size() ? leftParts[index] : 0,
index < rightParts.size() ? rightParts[index] : 0)
if (comparison != 0) {
return comparison
}
}
return 0
}
List<String> bukkitRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.3').tokenize('-')
Map<String, Map<String, Object>> modTargets = (Map<String, Map<String, Object>>) project.ext.irisModTargets
List<String> bukkitVersions = modTargets.keySet().findAll { String version ->
compareMinecraft(version, bukkitRange.first()) >= 0 && compareMinecraft(version, bukkitRange.last()) <= 0
}.toList()
Map<String, String> loaderLabels = [fabric: 'Fabric', forge: 'Forge', neoforge: 'NeoForge']
Map<String, String> loaderPins = [fabric: 'fabricLoader', forge: 'forge', neoforge: 'neoForge']
Map<String, String> loaderProperties = [fabric: 'fabricLoaderVersion', forge: 'forgeVersion', neoforge: 'neoForgeVersion']
String selectedVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.3')
List<Map<String, Object>> buildTargets = []
project.ext.multiplexor = [
schema: 1,
id: 'iris',
name: 'Iris',
sandbox: 'per-target',
parallelTargets: true,
sourceDependencies: ['volmlib'],
targets: buildTargets
]
gradle.projectsEvaluated {
File bukkitSource = tasks.named('jar', Jar).get().archiveFile.get().asFile
File bukkitArtifact = new File(bukkitSource.parentFile, bukkitSource.name.replaceFirst(/\.jar$/, '-packed.jar'))
File apiArtifact = tasks.named('irisApi', GenerateApiTask).get().outputFile.locationOnly.get().asFile
File spiArtifact = project(':spi').tasks.named('jar', Jar).get().archiveFile.locationOnly.get().asFile
buildTargets.addAll([
[id: 'bukkit', tasks: [':buildBukkit'], arguments: [], java: 25,
memoryMiB: 6144, workers: 2, dependsOn: [], artifacts: [
[path: relativePath(bukkitArtifact), role: 'runtime', platforms: ['plugin'], minecraftVersions: bukkitVersions]]],
[id: 'api', tasks: [':irisApi'], arguments: [], java: 25,
memoryMiB: 6144, workers: 2, dependsOn: [], artifacts: [
[path: relativePath(apiArtifact), role: 'api', platforms: ['plugin'], minecraftVersions: bukkitVersions]]],
[id: 'spi', tasks: [':spi:build'], arguments: [], java: 25,
memoryMiB: 4096, workers: 2, dependsOn: [], artifacts: [
[path: relativePath(spiArtifact), role: 'library', platforms: [], minecraftVersions: []]]]
])
modTargets.each { String minecraft, Map<String, Object> target ->
loaderLabels.each { String loader, String label ->
String pin = minecraft == selectedVersion
? providers.gradleProperty(loaderProperties[loader]).getOrElse(target[loaderPins[loader]].toString())
: target[loaderPins[loader]].toString()
String displayedPin = pin.startsWith(minecraft + '-') ? pin.substring(minecraft.length() + 1) : pin
String artifactName = "Iris v${project.version} [${label}] ${minecraft}+${displayedPin}.jar"
buildTargets.add([
id: "${loader}-${minecraft}".toString(),
tasks: [":verify${label}${minecraft.replace('.', '_')}".toString()],
arguments: ['-PmoddedBuildParallelism=1', '-PmoddedTestForks=1',
'-PmultiplexorSandbox=true'],
java: 25,
memoryMiB: 8192,
workers: 2,
dependsOn: [],
artifacts: [[path: "adapters/${loader}/build/${minecraft}/libs/${artifactName}".toString(),
role: 'runtime', platforms: [loader], minecraftVersions: [minecraft]]]
])
}
}
}