Skip to content

Commit 9e88cb2

Browse files
authored
Merge pull request #493 from SkyBlade1978/feature/1.18-build-script-cleanup
Tidy the 1.18 build scripts
2 parents 3f12111 + 53cb567 commit 9e88cb2

9 files changed

Lines changed: 1194 additions & 1213 deletions

File tree

‎.gitattributes‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Disable autocrlf on generated files, they always generate with LF
22
# Add any extra files or paths here to make git stop saying they
33
# are changed when only line endings change.
4+
.gitattributes text eol=lf
45
src/generated/**/.cache/cache text eol=lf
56
src/generated/**/*.json text eol=lf
7+
*.gradle text eol=lf
8+
*.md text eol=lf
69
/gradlew text eol=lf
710
/gradlew.bat text eol=lf

‎build.gradle‎

Lines changed: 14 additions & 1213 deletions
Large diffs are not rendered by default.

‎gradle/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Gradle build layout
2+
3+
The top-level `build.gradle` contains the shared project identity, Java
4+
toolchain, source sets, and Forge development runs. The applied scripts keep
5+
the remaining build concerns small enough to review independently.
6+
7+
- `dependencies.gradle` owns repositories and dependency scopes.
8+
- `release/artifacts.gradle` builds and audits deterministic release files.
9+
- `release/publishing.gradle` describes guarded Maven publication.
10+
- `verification/support.gradle` owns data, GameTest, and upgrade smoke gates.
11+
- `verification/packaged-runtime.gradle` runs the built mod in official
12+
packaged client and dedicated-server installations.
13+
- `ide/eclipse.gradle` normalizes ForgeGradle's Eclipse launches and checks
14+
that production runs cannot see validation-only code.
15+
16+
Applied-script order is intentional. Artifact setup publishes an immutable
17+
`baseMetalsRelease` map consumed by publishing and packaged-runtime checks.
18+
Keep machine-specific paths in Gradle properties, never in these scripts.

‎gradle/dependencies.gradle‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
ext.orespawnModule = "mmd-orespawn-${project.orespawn_curse_project_id}"
2+
def orespawnVerificationRepository = project.findProperty('orespawnVerificationRepository')
3+
repositories {
4+
minecraft.mavenizer(it)
5+
maven fg.forgeMaven
6+
maven fg.minecraftLibsMaven
7+
mavenCentral()
8+
9+
// CI stages and hashes the exact public OreSpawn jar in a temporary Maven
10+
// mirror. Normal development resolves the same coordinates from CurseMaven.
11+
exclusiveContent {
12+
forRepository {
13+
maven {
14+
name = orespawnVerificationRepository == null
15+
? 'CurseMaven'
16+
: 'OreSpawnReleaseVerificationMirror'
17+
url = orespawnVerificationRepository == null
18+
? uri('https://www.cursemaven.com')
19+
: uri(orespawnVerificationRepository)
20+
metadataSources { artifact() }
21+
}
22+
}
23+
filter {
24+
includeModule('curse.maven', orespawnModule)
25+
}
26+
}
27+
28+
flatDir {
29+
dir rootProject.file('../../benchmarks/external/EnderIO-1.18.2/build/libs')
30+
}
31+
maven { url = uri('https://modmaven.dev/') }
32+
maven { url = uri('https://dvs1.progwml6.com/files/maven/') }
33+
maven { url = uri('https://maven.covers1624.net/') }
34+
}
35+
36+
dependencies {
37+
// ForgeGradle supplies the patched Minecraft development environment.
38+
implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}")
39+
40+
// OreSpawn remains an ordinary hard mod dependency in metadata and is
41+
// never shaded into Base Metals. Use the published 1.18.2 release in
42+
// development so clean checkouts exercise the same artifact as players.
43+
runtimeOnly renamer.dependency("curse.maven:mmd-orespawn-${orespawn_curse_project_id}:${orespawn_curse_file_id}")
44+
orespawnRelease "curse.maven:mmd-orespawn-${orespawn_curse_project_id}:${orespawn_curse_file_id}"
45+
46+
// API only: Mekanism remains optional at runtime. Its public slurry
47+
// registry is required to provide the eight historical processing chains.
48+
compileOnly 'mekanism:Mekanism:1.18.2-10.2.5.465:api'
49+
50+
// Opt-in runtime distributions for compatibility smoke tests. These are
51+
// never packaged, required, or resolved by ordinary Base Metals builds.
52+
def compatibilitySmokes = providers.gradleProperty('compatSmoke').getOrElse('')
53+
.split(',').collect { it.trim().toLowerCase(Locale.ROOT) }.findAll { !it.isEmpty() }
54+
if (compatibilitySmokes.contains('mekanism') || compatibilitySmokes.contains('all')) {
55+
runtimeOnly renamer.dependency('mekanism:Mekanism:1.18.2-10.2.5.465')
56+
}
57+
if (compatibilitySmokes.contains('tconstruct') || compatibilitySmokes.contains('all')) {
58+
runtimeOnly renamer.dependency('slimeknights.mantle:Mantle:1.18.2-1.9.55')
59+
runtimeOnly renamer.dependency('slimeknights.tconstruct:TConstruct:1.18.2-3.7.2.169')
60+
}
61+
if (compatibilitySmokes.contains('thermal') || compatibilitySmokes.contains('all')) {
62+
runtimeOnly renamer.dependency('com.teamcofh:cofh_core:1.18.2-9.2.3.47')
63+
runtimeOnly renamer.dependency('com.teamcofh:thermal_foundation:1.18.2-9.2.2.59')
64+
runtimeOnly renamer.dependency('com.teamcofh:thermal_expansion:1.18.2-9.2.2.24')
65+
}
66+
if (compatibilitySmokes.contains('enderio') || compatibilitySmokes.contains('all')) {
67+
runtimeOnly renamer.dependency('validation:EnderIO-base-1.18.2:6.0.0-alpha-a94a2cb')
68+
runtimeOnly renamer.dependency('validation:EnderIO-machines-1.18.2:6.0.0-alpha-a94a2cb')
69+
}
70+
71+
testImplementation platform('org.junit:junit-bom:5.10.2')
72+
testImplementation 'org.junit.jupiter:junit-jupiter'
73+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
74+
75+
}
76+
77+
renamer.mappings.from minecraft.dependency.toSrgFile
78+
79+
configurations.named('testRuntimeClasspath') {
80+
exclude group: 'curse.maven', module: orespawnModule
81+
}

‎gradle/ide/eclipse.gradle‎

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import groovy.xml.XmlSlurper
2+
3+
def eclipseWorkspaceProjectName = projectDir.name
4+
5+
// ForgeGradle 7 Eclipse launches are normalized after generation: they use the
6+
// configured Gradle cache and Java 17, load only bin/main, and exclude all unit,
7+
// GameTest, integrated-smoke, and upgrade-smoke code from production launches.
8+
eclipse {
9+
classpath {
10+
downloadSources = true
11+
downloadJavadoc = true
12+
file.whenMerged { classpath ->
13+
classpath.entries.findAll { entry ->
14+
String path = entry.path?.replace('\\', '/') ?: ''
15+
path.startsWith('src/test/') || path.startsWith('src/gametest/')
16+
|| path.startsWith('src/integratedSmoke/') || path.startsWith('src/packagedClientSmoke/')
17+
|| path.startsWith('src/upgradeSmoke/')
18+
|| path.startsWith('src/packagedServerSmoke/')
19+
}.each { entry ->
20+
entry.entryAttributes['test'] = 'true'
21+
}
22+
}
23+
}
24+
synchronizationTasks 'isolateEclipseProductionRuns'
25+
}
26+
27+
idea {
28+
module {
29+
downloadSources = true
30+
downloadJavadoc = true
31+
}
32+
}
33+
34+
def syncForge40EclipseLaunches = tasks.register('syncForge40EclipseLaunches') {
35+
group = 'ide'
36+
description = 'Normalizes Forge 40 Eclipse launches to the production-only merged output.'
37+
dependsOn tasks.named('processResources')
38+
doLast {
39+
String mainOutput = new File(projectDir, 'bin/main').absolutePath
40+
String modClasses = "${mod_id}%%${mainOutput}"
41+
String eclipseGradleJavaHome = System.getenv('JAVA_HOME') ?: System.getProperty('java.home')
42+
def escapePreference = { String value -> value.replace('\\', '\\\\').replace(':', '\\:') }
43+
File buildshipPreferences = file('.settings/org.eclipse.buildship.core.prefs')
44+
if (!buildshipPreferences.parentFile.isDirectory() && !buildshipPreferences.parentFile.mkdirs()) {
45+
throw new GradleException("Could not create ${buildshipPreferences.parentFile}")
46+
}
47+
buildshipPreferences.setText([
48+
'arguments=',
49+
'auto.sync=false',
50+
'build.scans.enabled=false',
51+
'connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)',
52+
"connection.gradle.user.home=${escapePreference(gradle.gradleUserHomeDir.absolutePath)}",
53+
'connection.project.dir=',
54+
'eclipse.preferences.version=1',
55+
"gradle.user.home=${escapePreference(gradle.gradleUserHomeDir.absolutePath)}",
56+
"java.home=${escapePreference(eclipseGradleJavaHome)}",
57+
'jvm.arguments=',
58+
'offline.mode=false',
59+
'override.workspace.settings=true',
60+
'show.console.view=false',
61+
'show.executions.view=false',
62+
''
63+
].join('\r\n'), 'UTF-8')
64+
65+
File processedResources = layout.buildDirectory.dir('resources/main').get().asFile
66+
copy {
67+
from processedResources
68+
into mainOutput
69+
}
70+
['Client', 'Server'].each { String runName ->
71+
File launch = file("run${runName}.launch")
72+
if (!launch.isFile()) throw new GradleException("Missing generated Eclipse launch ${launch}")
73+
String text = launch.getText('UTF-8')
74+
if (text.contains('<mapAttribute key="org.eclipse.debug.core.environmentVariables"/>')) {
75+
text = text.replace(
76+
'<mapAttribute key="org.eclipse.debug.core.environmentVariables"/>',
77+
'<mapAttribute key="org.eclipse.debug.core.environmentVariables">\r\n' +
78+
" <mapEntry key=\"MOD_CLASSES\" value=\"${modClasses}\"/>\r\n" +
79+
'</mapAttribute>')
80+
}
81+
text = text.replaceFirst(
82+
/<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="[^"]*"\/>/,
83+
java.util.regex.Matcher.quoteReplacement(
84+
"<stringAttribute key=\"org.eclipse.jdt.launching.PROJECT_ATTR\" value=\"${eclipseWorkspaceProjectName}\"/>"))
85+
text = text.replaceFirst(/<mapEntry key="MOD_CLASSES" value="[^"]*"\/>/,
86+
java.util.regex.Matcher.quoteReplacement("<mapEntry key=\"MOD_CLASSES\" value=\"${modClasses}\"/>"))
87+
88+
def programArguments = text =~ /(<stringAttribute key="org\.eclipse\.jdt\.launching\.PROGRAM_ARGUMENTS" value=")([^"]*)("\/>)/
89+
if (programArguments.count != 1) {
90+
throw new GradleException("ForgeGradle generated malformed program arguments in ${launch.name}")
91+
}
92+
String arguments = programArguments[0][2]
93+
['cache', 'metadata', 'to-srg', 'to-obf'].each { flag ->
94+
String prefix = "--${flag} "
95+
int valueStart = arguments.indexOf(prefix)
96+
if (valueStart < 0) throw new GradleException("ForgeGradle omitted --${flag} from ${launch.name}")
97+
valueStart += prefix.length()
98+
int valueEnd = arguments.indexOf(' --', valueStart)
99+
if (valueEnd < 0) throw new GradleException("ForgeGradle generated an unterminated --${flag} path")
100+
String value = arguments.substring(valueStart, valueEnd)
101+
if (!value.startsWith('&quot;') || !value.endsWith('&quot;')) {
102+
arguments = "${arguments.substring(0, valueStart)}&quot;${value}&quot;${arguments.substring(valueEnd)}"
103+
}
104+
}
105+
text = text.replace(programArguments[0][0],
106+
programArguments[0][1] + arguments + programArguments[0][3])
107+
108+
String legacyPrefix = '-DlegacyClassPath.file='
109+
int legacyStart = text.indexOf(legacyPrefix)
110+
int legacyEnd = text.indexOf(' -DignoreList=', legacyStart)
111+
if (legacyStart < 0 || legacyEnd < 0) {
112+
throw new GradleException("ForgeGradle omitted -DlegacyClassPath.file from ${launch.name}")
113+
}
114+
String xmlQuote = '&quot;'
115+
boolean quoted = legacyStart >= xmlQuote.length()
116+
&& text.substring(legacyStart - xmlQuote.length(), legacyStart) == xmlQuote
117+
&& text.substring(legacyEnd - xmlQuote.length(), legacyEnd) == xmlQuote
118+
if (!quoted) {
119+
text = text.substring(0, legacyStart) + xmlQuote +
120+
text.substring(legacyStart, legacyEnd) + xmlQuote + text.substring(legacyEnd)
121+
}
122+
if (!text.contains('ATTR_EXCLUDE_TEST_CODE')) {
123+
text = text.replace('</launchConfiguration>',
124+
' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n' +
125+
'</launchConfiguration>')
126+
}
127+
launch.setText(text, 'UTF-8')
128+
}
129+
}
130+
}
131+
132+
def verifyEclipseProductionClasspath = tasks.register('verifyEclipseProductionClasspath') {
133+
group = 'verification'
134+
description = 'Verifies Eclipse client/server launches cannot see validation-only code.'
135+
dependsOn syncForge40EclipseLaunches
136+
doLast {
137+
File classpath = file('.classpath')
138+
if (!classpath.isFile()) throw new GradleException('Eclipse .classpath was not generated')
139+
File eclipseProject = file('.project')
140+
if (!eclipseProject.isFile()
141+
|| !eclipseProject.getText('UTF-8').contains("<name>${eclipseWorkspaceProjectName}</name>")) {
142+
throw new GradleException("Eclipse project is not named ${eclipseWorkspaceProjectName}")
143+
}
144+
File buildshipPreferences = file('.settings/org.eclipse.buildship.core.prefs')
145+
if (!buildshipPreferences.isFile()) {
146+
throw new GradleException('Missing Eclipse Buildship preferences')
147+
}
148+
Properties buildshipConfiguration = new Properties()
149+
buildshipPreferences.withInputStream { buildshipConfiguration.load(it) }
150+
String expectedGradleHome = gradle.gradleUserHomeDir.canonicalPath
151+
String expectedJavaHome = new File(
152+
System.getenv('JAVA_HOME') ?: System.getProperty('java.home')).canonicalPath
153+
if (buildshipConfiguration.getProperty('connection.gradle.user.home') != expectedGradleHome
154+
|| buildshipConfiguration.getProperty('gradle.user.home') != expectedGradleHome
155+
|| buildshipConfiguration.getProperty('java.home') != expectedJavaHome
156+
|| buildshipConfiguration.getProperty('override.workspace.settings') != 'true') {
157+
throw new GradleException(
158+
"Eclipse Buildship must use Gradle home ${expectedGradleHome} and Java ${expectedJavaHome}")
159+
}
160+
def classpathXml = new XmlSlurper(false, false).parse(classpath)
161+
classpathXml.classpathentry.each { entry ->
162+
String path = entry.@path.text().replace('\\', '/')
163+
boolean validationSource = path.startsWith('src/test/') || path.startsWith('src/gametest/')
164+
|| path.startsWith('src/integratedSmoke/') || path.startsWith('src/packagedClientSmoke/')
165+
|| path.startsWith('src/upgradeSmoke/')
166+
|| path.startsWith('src/packagedServerSmoke/')
167+
if (validationSource) {
168+
String testFlag = entry.attributes.attribute.find { it.@name.text() == 'test' }?.@value?.text()
169+
if (testFlag != 'true') {
170+
throw new GradleException("Eclipse validation source is not isolated: ${path}")
171+
}
172+
}
173+
}
174+
['runClient.launch', 'runServer.launch'].each { name ->
175+
File launchFile = file(name)
176+
String launch = launchFile.getText('UTF-8')
177+
def launchXml = new XmlSlurper(false, false).parse(launchFile)
178+
String mainType = launchXml.stringAttribute.find {
179+
it.@key.text() == 'org.eclipse.jdt.launching.MAIN_TYPE'
180+
}?.@value?.text()
181+
if (mainType != 'net.minecraftforge.launcher.Main') {
182+
throw new GradleException("Eclipse launch has the wrong ForgeGradle 7 main class: ${name}")
183+
}
184+
String mainClassEntry = mainType.replace('.', '/') + '.class'
185+
List<File> explicitArchives = launchXml.listAttribute.find {
186+
it.@key.text() == 'org.eclipse.jdt.launching.CLASSPATH'
187+
}?.listEntry?.collect { entry ->
188+
def runtimeEntry = new XmlSlurper(false, false).parseText(entry.@value.text())
189+
String archive = runtimeEntry.@externalArchive.text()
190+
archive ? file(archive) : null
191+
}?.findAll { it != null } ?: []
192+
boolean mainClassAvailable = explicitArchives.any { archive ->
193+
archive.isFile() && new java.util.zip.ZipFile(archive).withCloseable { zip ->
194+
zip.getEntry(mainClassEntry) != null
195+
}
196+
}
197+
if (!mainClassAvailable) {
198+
throw new GradleException(
199+
"Eclipse launch main class ${mainType} is absent from its explicit classpath: ${name}")
200+
}
201+
if (!launch.contains('ATTR_EXCLUDE_TEST_CODE')
202+
|| launch =~ /(?i)(junit|mockito|src[\\\/]test|src[\\\/]gametest|integratedsmoke|packagedclientsmoke|upgradesmoke|packagedserversmoke)/) {
203+
throw new GradleException("Eclipse launch is not production-only: ${name}")
204+
}
205+
if (!launch.contains("key=\"org.eclipse.jdt.launching.PROJECT_ATTR\" value=\"${eclipseWorkspaceProjectName}\"")) {
206+
throw new GradleException("Eclipse launch targets the wrong workspace project: ${name}")
207+
}
208+
String expectedModClasses = "${mod_id}%%${new File(projectDir, 'bin/main').absolutePath}"
209+
if (!launch.contains(expectedModClasses)) {
210+
throw new GradleException("Eclipse launch is not using Forge 40's merged production output: ${name}")
211+
}
212+
['cache', 'metadata', 'to-srg', 'to-obf'].each { flag ->
213+
if (!launch.contains("--${flag} &quot;")) {
214+
throw new GradleException("Eclipse launch does not quote its --${flag} path: ${name}")
215+
}
216+
}
217+
if (!launch.contains('&quot;-DlegacyClassPath.file=')
218+
|| !launch.contains('minecraft_classpath.txt&quot; -DignoreList=')) {
219+
throw new GradleException("Eclipse launch does not quote -DlegacyClassPath.file: ${name}")
220+
}
221+
}
222+
File processed = layout.buildDirectory.dir('resources/main').get().asFile
223+
['META-INF/mods.toml', 'data/basemetals/orespawn/provider.json',
224+
'data/basemetals/recipes/stone_crackhammer.json'].each { name ->
225+
File expected = new File(processed, name)
226+
File actual = new File(projectDir, "bin/main/${name}")
227+
if (!expected.isFile() || !actual.isFile()
228+
|| !java.util.Arrays.equals(actual.bytes, expected.bytes)) {
229+
throw new GradleException("Eclipse output contains stale production resource ${name}")
230+
}
231+
}
232+
String metadata = new File(projectDir, 'bin/main/META-INF/mods.toml').getText('UTF-8')
233+
if (metadata.contains('${') || !metadata.contains('version="3.0.1.118021"')) {
234+
throw new GradleException('Eclipse output contains unresolved or invalid Base Metals version metadata')
235+
}
236+
}
237+
}
238+
239+
tasks.matching { it.name == 'genEclipseRuns' }.configureEach {
240+
finalizedBy syncForge40EclipseLaunches
241+
}
242+
243+
tasks.register('isolateEclipseProductionRuns') {
244+
group = 'ide'
245+
dependsOn tasks.named('genEclipseRuns')
246+
dependsOn syncForge40EclipseLaunches
247+
}
248+
249+
tasks.matching { it.name == 'eclipse' }.configureEach {
250+
finalizedBy syncForge40EclipseLaunches
251+
}

0 commit comments

Comments
 (0)