|
| 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('"') || !value.endsWith('"')) { |
| 102 | + arguments = "${arguments.substring(0, valueStart)}"${value}"${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 = '"' |
| 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} "")) { |
| 214 | + throw new GradleException("Eclipse launch does not quote its --${flag} path: ${name}") |
| 215 | + } |
| 216 | + } |
| 217 | + if (!launch.contains('"-DlegacyClassPath.file=') |
| 218 | + || !launch.contains('minecraft_classpath.txt" -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