From fc9041a07183334d4d2655adef92123c2b96956e Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 22 Aug 2023 22:52:10 +0100 Subject: [PATCH 01/12] Start on Loom 1.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c7ff152af..523316666 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { group = 'net.fabricmc' archivesBaseName = project.name -def baseVersion = '1.3' +def baseVersion = '1.4' def ENV = System.getenv() if (ENV.BUILD_NUMBER) { From 3a090917ffe95cf531365a2c716221f983c68f8e Mon Sep 17 00:00:00 2001 From: modmuss Date: Thu, 24 Aug 2023 10:18:25 +0100 Subject: [PATCH 02/12] Update to Gradle 8.3, and update all other deps. (#946) * Update to Gradle 8.3, and update all other deps. * Fix tests * Lazily download decompilers, generate version constants to ensure they are synced between the build and Gradle. Each decompiler has a configuration, this allows the version to be changed at a later date if needed. * Fix typo :) * Oh so many versions --- .github/workflows/test-push.yml | 6 +- .gitignore | 4 +- bootstrap/build.gradle | 6 +- .../bootstrap/LoomGradlePluginBootstrap.java | 2 +- build.gradle | 73 +++++++-------- gradle/libs.versions.toml | 50 ++++++++++- gradle/runtime.libs.versions.toml | 23 +++++ gradle/test.libs.versions.toml | 23 +++++ gradle/versions.gradle | 85 ++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 8 +- settings.gradle | 11 +++ .../mixin/AnnotationProcessorInvoker.java | 3 +- .../configuration/LoomConfigurations.java | 9 +- .../LoomNativeSupportLibraryProcessor.java | 4 +- .../decompilers/DecompilerConfiguration.java | 26 ++++-- .../remapping/KotlinClassMetadataWrapper.java | 37 ++++++++ .../loom/task/service/JarManifestService.java | 3 +- .../net/fabricmc/loom/util/Constants.java | 28 ------ .../fabricmc/loom/util/SourceRemapper.java | 2 +- ...ClassMetadataRemappingAnnotationVisitor.kt | 19 ++-- .../kotlin/remapping/KotlinClassRemapper.kt | 12 +-- .../loom/test/LoomTestConstants.groovy | 2 +- .../test/integration/FabricAPITest.groovy | 8 +- .../loom/test/util/ServerRunner.groovy | 5 +- 26 files changed, 340 insertions(+), 112 deletions(-) create mode 100644 gradle/runtime.libs.versions.toml create mode 100644 gradle/test.libs.versions.toml create mode 100644 gradle/versions.gradle create mode 100644 src/main/java/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataWrapper.java diff --git a/.github/workflows/test-push.yml b/.github/workflows/test-push.yml index b9c6ee1f7..919565614 100644 --- a/.github/workflows/test-push.yml +++ b/.github/workflows/test-push.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - version: [8.1.0-jdk17] + version: [8.3.0-jdk17] runs-on: ubuntu-22.04 container: image: gradle:${{ matrix.version }} @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 container: - image: gradle:8.1.0-jdk17 + image: gradle:8.3.0-jdk17 options: --user root steps: @@ -46,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - version: [8.1.0-jdk17] + version: [8.3.0-jdk17] test: ${{ fromJson(needs.prepare_test_matrix.outputs.matrix) }} runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index 4354f60fd..664ed03e5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ !/Jenkinsfile !/checkstyle.xml !/codenarc.groovy -!/bootstrap \ No newline at end of file +!/bootstrap + +/src/**/generated \ No newline at end of file diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle index a92baf401..30b987a2e 100644 --- a/bootstrap/build.gradle +++ b/bootstrap/build.gradle @@ -3,8 +3,10 @@ plugins { id 'groovy' } -sourceCompatibility = 8 -targetCompatibility = 8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} tasks.withType(JavaCompile).configureEach { it.options.encoding = "UTF-8" diff --git a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java index c8a45de03..bd420dd00 100644 --- a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java +++ b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java @@ -14,7 +14,7 @@ */ @SuppressWarnings("unused") public class LoomGradlePluginBootstrap implements Plugin { - private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.1"; + private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.3"; private static final int MIN_SUPPORTED_MAJOR_JAVA_VERSION = 17; private static final int MIN_SUPPORTED_MAJOR_IDEA_VERSION = 2021; diff --git a/build.gradle b/build.gradle index 523316666..4c6034e41 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,8 @@ plugins { id 'jacoco' id 'codenarc' alias(libs.plugins.kotlin) - id "com.diffplug.spotless" version "6.18.0" - id "org.gradle.test-retry" version "1.5.2" + alias(libs.plugins.spotless) + alias(libs.plugins.retry) } tasks.withType(JavaCompile).configureEach { @@ -24,7 +24,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } group = 'net.fabricmc' -archivesBaseName = project.name def baseVersion = '1.4' def ENV = System.getenv() @@ -69,39 +68,35 @@ dependencies { bootstrap project(":bootstrap") // libraries - implementation ('commons-io:commons-io:2.11.0') - implementation ('com.google.code.gson:gson:2.10.1') - implementation ('com.fasterxml.jackson.core:jackson-databind:2.14.2') - implementation ('com.google.guava:guava:31.1-jre') - implementation ('org.ow2.asm:asm:9.5') - implementation ('org.ow2.asm:asm-analysis:9.5') - implementation ('org.ow2.asm:asm-commons:9.5') - implementation ('org.ow2.asm:asm-tree:9.5') - implementation ('org.ow2.asm:asm-util:9.5') + implementation libs.commons.io + implementation libs.gson + implementation libs.jackson + implementation libs.guava + implementation libs.bundles.asm // game handling utils - implementation ('net.fabricmc:stitch:0.6.2') { + implementation (libs.fabric.stitch) { exclude module: 'enigma' } // tinyfile management - implementation ('net.fabricmc:tiny-remapper:0.8.7') - implementation 'net.fabricmc:access-widener:2.1.0' - implementation 'net.fabricmc:mapping-io:0.2.1' + implementation libs.fabric.tiny.remapper + implementation libs.fabric.access.widener + implementation libs.fabric.mapping.io - implementation ('net.fabricmc:lorenz-tiny:4.0.2') { + implementation (libs.fabric.lorenz.tiny) { transitive = false } // decompilers - implementation ('net.fabricmc:fabric-fernflower:2.0.0') - implementation ('net.fabricmc:cfr:0.2.1') + compileOnly runtimeLibs.fernflower + compileOnly runtimeLibs.cfr // source code remapping - implementation ('net.fabricmc:mercury:0.3.0') + implementation libs.fabric.mercury // Kotlin - implementation('org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.6.2') { + implementation(libs.kotlin.metadata) { transitive = false } @@ -110,20 +105,21 @@ dependencies { // Testing testImplementation(gradleTestKit()) - testImplementation('org.spockframework:spock-core:2.3-groovy-3.0') { + testImplementation(testLibs.spock) { exclude module: 'groovy-all' } - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2' - testImplementation ('io.javalin:javalin:5.4.2') { + testImplementation testLibs.junit.jupiter.engine + testRuntimeOnly testLibs.junit.platform.launcher + testImplementation (testLibs.javalin) { exclude group: 'org.jetbrains.kotlin' } - testImplementation 'org.mockito:mockito-core:5.2.0' - testImplementation 'com.microsoft.java:com.microsoft.java.debug.core:0.46.0' + testImplementation testLibs.mockito + testImplementation testLibs.java.debug - compileOnly 'org.jetbrains:annotations:24.0.1' - testCompileOnly 'org.jetbrains:annotations:24.0.1' + compileOnly runtimeLibs.jetbrains.annotations + testCompileOnly runtimeLibs.jetbrains.annotations - testCompileOnly ('net.fabricmc:sponge-mixin:0.11.4+mixin.0.8.5') { + testCompileOnly (testLibs.mixin) { transitive = false } } @@ -136,8 +132,9 @@ jar { from configurations.bootstrap.collect { it.isDirectory() ? it : zipTree(it) } } -sourceCompatibility = JavaVersion.VERSION_17 -targetCompatibility = JavaVersion.VERSION_17 +base { + archivesName = project.name +} tasks.withType(JavaCompile).configureEach { it.options.release = 17 @@ -145,6 +142,8 @@ tasks.withType(JavaCompile).configureEach { java { withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } spotless { @@ -174,11 +173,11 @@ spotless { checkstyle { configFile = file('checkstyle.xml') - toolVersion = '10.6.0' + toolVersion = libs.versions.checkstyle.get() } codenarc { - toolVersion = "3.2.0" + toolVersion = libs.versions.codenarc.get() configFile = file("codenarc.groovy") } @@ -192,7 +191,7 @@ gradlePlugin { } jacoco { - toolVersion = "0.8.8" + toolVersion = libs.versions.jacoco.get() } // Run to get test coverage. @@ -201,7 +200,7 @@ jacocoTestReport { reports { xml.required = false csv.required = false - html.outputLocation = file("${buildDir}/jacocoHtml") + html.outputLocation = file("${layout.buildDirectory.get().asFile}/jacocoHtml") } } @@ -234,7 +233,7 @@ publishing { // Also publish a snapshot so people can use the latest version if they wish snapshot(MavenPublication) { publication -> groupId project.group - artifactId project.archivesBaseName + artifactId project.base.archivesName.get() version baseVersion + '-SNAPSHOT' from components.java @@ -360,3 +359,5 @@ class PrintActionsTestName extends DefaultTask { new File(System.getenv().GITHUB_OUTPUT) << "\ntest=$sanitised" } } + +apply from: rootProject.file('gradle/versions.gradle') \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8f38ae50..08e91098d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,54 @@ [versions] -kotlin = "1.8.10" +kotlin = "1.9.0" +asm = "9.5" +commons-io = "2.13.0" +gson = "2.10.1" +jackson = "2.15.2" +guava = "32.1.2-jre" + +stitch = "0.6.2" +tiny-remapper = "0.8.9" +access-widener = "2.1.0" +mapping-io = "0.4.2" +lorenz-tiny = "4.0.2" +mercury = "0.4.0" +kotlinx-metadata = "0.7.0" + +# Plugins +spotless = "6.20.0" +test-retry = "1.5.4" +checkstyle = "10.12.2" +codenarc = "3.3.0" +jacoco = "0.8.10" [libraries] +# Loom compile libraries +asm = { module = "org.ow2.asm:asm", version.ref = "asm" } +asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" } +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } +asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } +asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } + +commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } + +fabric-stitch = { module = "net.fabricmc:stitch", version.ref = "stitch" } +fabric-tiny-remapper = { module = "net.fabricmc:tiny-remapper", version.ref = "tiny-remapper" } +fabric-access-widener = { module = "net.fabricmc:access-widener", version.ref = "access-widener" } +fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" } +fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" } +fabric-mercury = { module = "net.fabricmc:mercury", version.ref = "mercury" } + +# Misc kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-metadata = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version.ref = "kotlinx-metadata" } [plugins] -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +retry = { id = "org.gradle.test-retry", version.ref = "test-retry" } + +[bundles] +asm = ["asm", "asm-analysis", "asm-commons", "asm-tree", "asm-util"] diff --git a/gradle/runtime.libs.versions.toml b/gradle/runtime.libs.versions.toml new file mode 100644 index 000000000..d67b9fd11 --- /dev/null +++ b/gradle/runtime.libs.versions.toml @@ -0,0 +1,23 @@ +[versions] +# Decompilers +fernflower = "2.0.0" +cfr = "0.2.1" + +# Runtime depedencies +mixin-compile-extensions = "0.6.0" +dev-launch-injector = "0.2.1+build.8" +terminal-console-appender = "1.2.0" +jetbrains-annotations = "24.0.1" +native-support = "1.0.1" + +[libraries] +# Decompilers +fernflower = { module = "net.fabricmc:fabric-fernflower", version.ref = "fernflower" } +cfr = { module = "net.fabricmc:cfr", version.ref = "cfr" } + +# Runtime depedencies +mixin-compile-extensions = { module = "net.fabricmc:fabric-mixin-compile-extensions", version.ref = "mixin-compile-extensions" } +dev-launch-injector = { module = "net.fabricmc:dev-launch-injector", version.ref = "dev-launch-injector" } +terminal-console-appender = { module = "net.minecrell:terminalconsoleappender", version.ref = "terminal-console-appender" } +jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } +native-support = { module = "net.fabricmc:fabric-loom-native-support", version.ref = "native-support" } \ No newline at end of file diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml new file mode 100644 index 000000000..4c2b65738 --- /dev/null +++ b/gradle/test.libs.versions.toml @@ -0,0 +1,23 @@ +[versions] +spock = "2.3-groovy-3.0" +junit = "5.10.0" +javalin = "5.6.2" +mockito = "5.4.0" +java-debug = "0.48.0" +mixin = "0.11.4+mixin.0.8.5" + +gradle-nightly = "8.4-20230821223421+0000" +fabric-loader = "0.14.22" +fabric-installer = "0.11.1" + +[libraries] +spock = { module = "org.spockframework:spock-core", version.ref = "spock" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +javalin = { module = "io.javalin:javalin", version.ref = "javalin" } +mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } +java-debug = { module = "com.microsoft.java:com.microsoft.java.debug.core", version.ref = "java-debug" } +mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" } +gradle-nightly = { module = "org.gradle:dummy", version.ref = "gradle-nightly" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-installer = { module = "net.fabricmc:fabric-installer", version.ref = "fabric-installer" } \ No newline at end of file diff --git a/gradle/versions.gradle b/gradle/versions.gradle new file mode 100644 index 000000000..77f7890f5 --- /dev/null +++ b/gradle/versions.gradle @@ -0,0 +1,85 @@ +/** + * Generates a java source file containing all of the version from the Gradle version catalog. + */ +import java.nio.file.Files +import java.time.LocalDate + +generateVersionConstants(sourceSets.main, "runtimeLibs", "net/fabricmc/loom/util/LoomVersions") +generateVersionConstants(sourceSets.test, "testLibs", "net/fabricmc/loom/test/LoomTestVersions") + +def generateVersionConstants(def sourceSet, def catalogName, def sourcesName) { + def versionCatalog = extensions.getByType(VersionCatalogsExtension.class).named(catalogName) + + def task = tasks.register("${catalogName}GenerateConstants", GenerateVersions.class) { + versionCatalog.getLibraryAliases().forEach { + def lib = versionCatalog.findLibrary(it).get().get() + getVersions().put(it, lib.toString()) + } + + className = sourcesName + headerFile = file("HEADER") + outputDir = file("src/${sourceSet.name}/generated") + } + + sourceSet.java.srcDir task + spotlessGroovyGradle.dependsOn task // Not quite sure why this is needed, but it fixes a warning. + compileKotlin.dependsOn task + sourcesJar.dependsOn task +} + +abstract class GenerateVersions extends DefaultTask { + @Input + abstract MapProperty getVersions() + + @Input + abstract Property getClassName() + + @InputFile + abstract RegularFileProperty getHeaderFile() + + @OutputDirectory + abstract DirectoryProperty getOutputDir() + + @TaskAction + def run() { + def output = outputDir.get().asFile.toPath() + output.deleteDir() + + def className = getClassName().get() + def si = className.lastIndexOf("/") + def packageName = className.substring(0, si) + def packagePath = output.resolve(packageName) + def sourceName = className.substring(si + 1, className.length()) + def sourcePath = packagePath.resolve(sourceName + ".java") + Files.createDirectories(packagePath) + + def constants = getVersions().get().collect { entry -> + def split = entry.value.split(":") + if (split.length != 3) return "" + "\tpublic static final ${sourceName} ${toSnakeCase(entry.key)} = new ${sourceName}(\"${split[0]}\", \"${split[1]}\", \"${split[2]}\");" + }.findAll { !it.blank }.join("\n") + + def header = headerFile.get().getAsFile().text.replace("\$YEAR", "${LocalDate.now().year}").trim() + + sourcePath.write( +"""${header} + +package ${packageName.replace("/", ".")}; + +/** + * Auto generated class, do not edit. + */ +public record ${sourceName}(String group, String module, String version) { +${constants} + + public String mavenNotation() { + return "%s:%s:%s".formatted(group, module, version); + } +} +""") + } + + static def toSnakeCase(String input) { + return input.trim().replaceAll(/[^a-zA-Z0-9]+/, '_').toUpperCase() + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 37652 zcmZ6SQ*jNdnQBE-m!q1z)J^6!8liD~E|8k;d@!RKqW+P+c{{A_w4h-Fct^jI*3f}}> z2Q39vaxe&dYajQhot|R|okxP_$~ju*X0I0#4uyvp5Y5h!UbielGCB{+S&Y%+upGDb zq|BVDT9Ed2QC(eCsVrrfln`c3G!v|}sr1Y02i z%&LlPps4#Ty_mb$1n|@5Qfpv_+YV$Jdc936HIb{37?{S?l#NH+(Uw<@p6J%2p)un; z8fSGPL>@VtAl4yv;YO5e z$ce51CS;`NGd!WVoXeA9vfJC?1>OLi=8DCWBC=^_)V|)E5|B~`jRg01sgJZg#H@DN z(%3v>_-$+>k5p8l?YQWO0Xnm+Qg}U9W+}Al#c_RurG{H6IF}%vlMobp!nmIFL5{I# zoF z4ytIT@lBphb!xg@+~Hd9$f>Hh zUWt4fdi9Gtx|Z%Qfqw2|q5|Nnxh|mer1*VKpI}@@YPdN?TtU6jE;@uhxp8=l?#DTW z3?}F=_muS@5OK7^63G_i&I}DlJCSXGU*&Kq^(hgNE-=%%`BAo0 zBU#vb^C+2dcfe0`MDBTc%;9sY8a+%WNboJPY~n<&z)unXq5*0aZ&|aYVl1Am$Xp_c zU6TBDJ)I1Czr9Fusl92Pkm{EaI=QRi&nIo%&vvPM$PW7gOATu2+6A9&#{E|R8_vZD zo=}nNASfxDaaoMiy1+Z0+XD9hN4VaK<7I$rOt z5^|1qXwt%WJ5}+eQ#RFYSZ*(`YcT-098L^_8q29iO=XfmXO;Z9NHp+;FxUbI$Fg; zi510A`7H3>G6C##jBjc~Ixv7Rty}TthLu-u<1akLY7djP%xObB2KP!vAp?%YSbD^% zu=YcbKXUUhzgC;^%P&GvnnDJ&9=Xg%dauiSajot%RIn@(gf);fn@&Ru4)KS47(OdJ z$h)5lhgOh?n~P1R&)RcABS_Qia>NzjcvP`~C&VU6N2E8OL&X&1=1U2b&N`9o??Yn> zF<;;DseXn1&2-S!d-L&Z@p7C>>z>}0fA`19kNzf@X6+?iRv;E4ptwF7UwR@K58#?IR?)HVT8 zl~Dm+bfAIu3_Uc6J6a+zC+(~hEa^(RtRb#jVZn#5;_Fi`yR0K0?3LpaJTu+@7UsX& z#qUh`Nb;vJ0R=JB!leZl^YGMQ=p^l!6|^I_CMO(I)y+$u>K3zK#wVX08}j>x3CZwp zlk*ylL1!pfyq)Mh{n_|@TFPDddYx131Jmjk#j{Kh5*L*ig|AGXsfKOg#A9=C+CntSIZTb-d{G)j<>I+x8(cr40Xc1%<2LuzauvEDVt6i97SpA6 zsxGPO)MV;#UbwBSPiP{2*4l8o(o6o*tddwUFwx3;(g3LspjtuwUQvC*_4iMDCj+7uNe z>HNYl12vbCMsk!BRX&lF@neUQF46p|G{+&{RA1VANjF~C@9I6Br_$YAdX+rqwy7+| zPf=TFt(2f#W6Zb>-7(K%c~P$-E5B%z+?{oOh@b%O6VJEKH^@I;y!78V5vYfx#vL|J zte^#>+1NkFzOBEu6N-m!uO({kkWTY=oOtt5gF-!78Cb;LJH|+GW=czxXTyUDFBdbg zw&;1{SfPq|#+>6wJ;@YCj^E*1Z{Wtt;APe=!aZ&)_P~Wq$346{9sl6}#we1s$o+9H zH2@_Ct7gbH9Oqtdr=IDyUGFHc@}NPiXO$7%44}{^?+MTHPpFs}U1ktHWzj}Bmh7}} z0r`~t6xa4x#>EyC{l!C;zpw){$b=O||F?$c0b<;(<3p_FLE)z)5kvMz%M$s$!kQ_@ zn7YaOX%*Syd%2nV(t`wfW^U1#TSeTnz~P(CuN9rh$N(BdqHmQpSlbru>&Qzp$!Wk% z@i17nZv$pOU|V^^=Zs*wcArd+Ig@jr0zuo%Wd)iEO1x#u)m37$r7*KFW9)89oswQ# zSYKZ^R5ka^d-_*@na|Ow8zNyJ708zX4N6j&jykXV7%hZ|j*C~=m!BN;4KHywBL@+J zFMVY_D2@vrI@t{z&|1*KsUw>d1SRZ?V>}z7O@%r#Y@yFi4d#!`PKfi>SE6(y7$7?o zh^&V1d)~1F!w62_{X|LVW2E~`cd+u_koSGZOL**qSQj;OFHOrag&04h*(pJdFN6hx zh<`idoM?HedX~KoGce-)-;g^Xb;;7#SY~TY0~yH&G~!Kdm$7U4=b5|mk@Ktm{rke$ zRd_nDsKt3|h;WU(v78jFvhvoGaG=F!ZU7;=mve%3PVm+Zsz!^ELnE&b8=*|m;?b*BQe}|1AK&i+{?MLRhV+uBX*Du$tfT}EnHNpBthR}_xDzZ#PB_ElYd?REZ#@GIbt4a63@b<^e z0Roi}Zr-Q-sD~v`HAvj{K=fpGi}!iUTfwsL^W_7opUM5+Nom4Vf|-l>{5T=VEoa9` z$wdiRKM}u~6cGK4Hyv}17PNx+9%x+42m!jaas7pL9uM@LO#WpY_b#a??K_*O@u4As zNH0$up@AAflGq@Ck)t(XG>@nlrgzJuhUh>K8*K9?5DAIZZ53v-hlF|kK6vrENdAWw z<*oCApq8wFPL+lLQGuCv0r!I762os)Fb@WTS)7ZCeFb|Zct|UBAa<1<9M|wVu@TfO zAY@^rrg}Qu{e0z*!oHB!*>jZ}Zm^X;t)`1iOubj30>uC2dHBgCdTcn4*hIt&>mjgs z@chLwLzCM3Jk`)6J@77;ave;*g27yps*!8eRuZLmf z+~W>kS#<_W3dbNz0z1PI5<%@gMRiLvo9RlIcyf{gTTjZp>n zCA6CO0>+*AiqzO8qo3-eITXeI1N^_bvwWZ^K!gDU^FT|w=A=#{^cmmW%f^#;Yr)G(EHZ=8TYj> zSU%DrTk1YIp0WUqaalA-#p+mWV?;DN3=)M8r7Oej=b#Z}Xs{p~wrO27JcTDGW`H(0 z!qD_Xd^F$s$C;GWMER%{I%p#(W`>Mg=YV%ztG2Bf&VQByR5*<=W;(~&w450Sw- z&v)+bPcx|8L2x+5rc-uwKl**(w@A)E_^BHgze1&B1!a?Kcro8Vf7s-=ujFiEi}=4W zvQ80O;nlZ@sW?VZ$D}IQT1l~EunsL>ui8nrr5#Py;lRFQLppSXmNScPVcjw`_=j7P zC6G&zna5UjbOxVD{Q?%G!F`(<@txVX)Rb&Ci&WIc+boK)Vx(P@Y8^%#E9tp2FzsL7 zN|ujIll!%^2cqT#x#Uyw0QsvnjnYFmnVc&9Ld&rvD|uMh`9B(k0+h;9@|U*z83Zc| z^gDgyTIr>eE7P&o5`8o6Z-74$JA$Bv)q6&oCFFOj1RmC~f%)|`q|~|=VS@4ai}IRA zrk`paX)_$nXpBX5HkEt<+QYcJn>9!r{#OpG*?**E zF4DG7h+-+ilK6_$ewPrM*B&FEKdt7gB^xtmpUu&pu~YsM){ycr7!-yBp}ssn|2T*4%vhs9ZX;FE0WM5iEo7Jrgyj(au+Q_^8*7aN%nC2v9BpOz6E;@Ae z6`jsk$$MUJAA<`gSa8*9$LWW)G=q*z?}1lGb2_RIg8vFk4Kb@u0;H9#xQjVQLVD3rgP%9YxIfY>cZQp1Um8nZhx30;BqgqHI=dBJ- zdDdvni6NaU&Ju2^7K*hiXC33bnfox+8vbL>w;of20_c&+q)y&FWUtoFa-yRj_~F%* z=t;#(7UlA4%Fm}#R5c575CsnOc(YVYm$s!TAdo@;(UJrBnhU)PuuD)E^o@HJN32XF zYRqj+d$AM1tACioZZ8YvrXci@ELZr9ACNU$1_KXS?$MRCcwM*ZcE)&wi_#NLH;2%V268UW?OVFSIJ;C5d zKnqu91}(Z4e^!Ki`q{xJp?Jd2guS*fpuaD+t{iW;&|>9^MF4nuNuEk zeolrCT^Ek-YNOs`eZ&)69=31j{z1%<32I;=$`ub8Vi%T_1cDAB{f3dJi$)l~eK&Si z6kXy;&3=8NH(oC@C8nADzKW@aD|L^|q~s^QYooSr7bhXw! zuUyO%6(tOngxFePj>!*q@_o!6ypM;f-s^+xlK1=+ujdy244_Jo>v1f6(Pe6ez09HD z5S+aeYZ&4cxB^+feStV~!Wj9^s=zT|6sU-^I-Plyy5(MeJAz~QV0bHxP85Oi1^%Tx>axi;rp2a} z>Uy%3d(Zo0^Xv8fg4LQYpu`q5$rNQs;=XF?#5J!C7T|wJ4`yx zCf;EWH`O&&AAbQ8Z)h1_!=pZFDTPzM{C98nxWH6h4zf^Z@qOQRnH!=_=GxW=Z?srv7J=%JCXF*? zw;&5KD3-^6{WS3O+hyH5tzQ_ev{ zuOquYA(x%naj=Y8C+^9@Pn`mxO-Ws8gKa<|CKwHljJXoe146CN&DfGd+S&KK&6K1k zv?FDRELtxCRu~W?6;#dFMD2<~Oc=PWPC=v!(tOfriOePfkh^dga&#=mxYxmc4pXcf zfmFJ@7EZikj4xi{g@lHmj(N3P8#ol}n%^xUL&2GlG6z#o@BA5xgomE`-T4y}?6Cw| zx$OoWyAx{_EmPiM zEi%=fEgF+Zd2S7=j&s_l#rQZ6u%Fqo@*|xxH2irHz`i6nPt^V-Ou8_YYVQfeCAJ9K zAGqsa3u-)Hrr8K~wQJ7AQWZE%f%b%sR7l~T)YDpg%88Uq1Cc(OZ8i~ln};D7)*Ly< z9lUkgXPLAN=&w<1i5R73?8rUTPEdh#StrnUghGvJbbUq)?|p(cAAKe;QuPfd1ubD+ zl+)mVP!*K1J^Sl0khkO$JJ;ek*|!TE@7Ai@Uej%#@Ya-Nl$F0TDPz>u&S)#j$peaG zm(rIO;#Bz@Kqguv-Lbk_N)6?va8rmb0U6cZH*yUYaBK7}bbjf^^=Z15+ZO2p#3z0| zo%K((lY-D_&bNsp$;_h2W=6i{$k14a1 zu8Pj(iv4aKPJM26ZuvHk2i#{Bg+HsHj=r&)8LzZopotENKxdgup)@{UDN)?ydnAe^ zz`+DYsE8;BSSY(0793hBr*-soAl@H(kB9spa9UUr>`_qP?&q162GTWMKkmdc%~F?0OQvPBw%M3DjAH$mP_0 zn;RX&9lJ$sP|i!6&4StDdL>Oz8svAEg<5wtY-|z(uu#pLh&n?=w*%|EQ=aHVisIDh z3}DGGi|h6YYoJTe%1*Q?#aJOUF<<|(vPg&H)+|u~iu9vS9sg50!Jh21FtQ-Pz@-0q zwA}x1tYtZcPJ%x{1*NEO1C}H(zgAPp#c4)(B19LzlLYI?m}EoBSY?;O{hq6FwvrbW z)lHA7VJ(b2N-!(!IVHIH<{P-D%)mF9p z_v?`xOtzi+5CRLMJ^!E`ceH`wurLx)LoK<1?vNbHmJZX00c5H_f(EWqPZ}y~qOI(t zJxI~%HIt;jAwNf8r?TMW6-K7}r$h>HgwU2AF zYg%ruK{p0=fR@mW9RPFOJsCkllZXIzJ>`7cH&SG>sXL=!Wy(AU9z(NqV!IpoUa^)d zok2QH@BZ(1i8DFw6=)u*OH7j9ka*UR-LIEOI}w|z^Und?K;rb7{H;3HO15)S52HBj zse@>hT}GDaZn#Y2cHx1h(NJLFi+^t46z{2GOpo4}Cpx=4V76uK&CfJ`ly;RIQ_b zhK1n^bnX3=S1ZWRULjo^?^Ech$&!N^3VmQy?d(I{oRCK*{r}(mJ zPik|X+)CrZob_ZsN;}R=Tg{%3_|m&$wR0G;(5CCJZ$DAK_aF@U0mtHaS!*?8ifx64 z`H7aSSuvA*o+?b<;tSB*|K8ZkDZ1)Q-K3)yfg+*2`r?9&6MHexRSxdv&xv$Wq}UQO zHUx`7rPA=%i#!y`fADsSIb%$ngkI)zrE5Xzxm|Z zh|~QJ^;QB6S5Wgb_P{Xe#Xa0;ph&uC<9qQuVHBJAszfF%v9hT=2(u?G!i!Ht&=ieG zgDS!r#*!8Js!5pvrgN;5Uq1srr4>gEUjlkyZTY?*6RlBLSl;+)oseT%r4G{ch9L*} zU>TXDTA=^70wFFUESu9j=$7?02#dN0b+UbLbIq_@q>!{Y$u;rG{SrL-{(bRR0!<9V za2E#uYrGkqP@39Z#}Rpd6+WA5Izn^aD2GY7;b4bS?ig+2Qu1HO%iLlTaqu}hvjLiU zOy8q3(};?+|Gws4jkLa`FMd}DOkbQPH-SKKDA@ej_R6FW!JnW@1q@|WLEwACWn;1m zq?j^VRI}`q%CI78G$)k=BnD>CU#81a1_xl)_Q+|`3*=Xb7|H)Y7Z*ny$X}3FiyiDP zmb2Lz9hZ51KR^)aBTXD$##R)i9A--B7Q7+WNZiJi=?nRV6k_7x8<%3SfY652A z&V2*%x;wu?c^zj?ZN{}By_a0S@e&Q_n+4O7p*CBF#6u@UEcMFD+GkPgyxgJ+95>u+ zQgVKm9`_w)#ZuCFa$Z%t>|(ngMThCS_vhD52HNAY8FthjYZ4JdVsB?oN8q>O{kVV!IjZE)hnTcUc&~{Vyg!7tQ4nFp z;i?p@^=jOv?>~mT3FR4z&q}QJR+F+Uelw~!jt6@rsFY+vf_S|&ZB}hXL4fh(<+e+kGjS07#P=N zWJZg$-!MkOAGQy#eo1{&$D`X9SD${kCwI%Z9e&$Lry~;C;7_U@cP%0U2%useF8ovz z-%5Z$(;>zPH&<`m*Y=2 zmAK5EHz>RQ8Lt7_c*ZB`pTm3 zO?<8$R^ztmO9dtdOemZT_AH)su9yuW{WF|`s z`E$HVAoe3gCz`9|&hF1C(V*Dj%oUV7=2tit&}H5CNmSW9VZNn%g+e-7&J}w{2LJj3 zdxYxxSqPFkHOq>mQ9guwv-2-w8HY(Y7ERx`K6+)5@qwK3VIXTp=e|Tu+>zgklyW%a z^2{D*G$jO9SSjtn|A+9D6`a` zY_t#Jzv}gvVn%@cr{4B|kt>6IWBtj^V|&YoAD)LXR0b~)AIhWmt#*yVfgILzl6m*pC)sVEpC>2G zU@%r2Qbji8K{nWm_RIC=#$zHm@t$YW%wFPBD+FVZO&Ey!gEnhPSNkLF*OhUF*C3bD zWhCgqAJ~&iw-nYAWd>5?zNmDr>dfe9)c4mVuIghr#;12v8r(|cmc_&Kz?^_<-W($V zY(P0bg*XU_>HRy$z!emZ&0g>QLq*+;k&aiU0D~Ev#;4o*x+5ne$NjqK!l00`W5$L@ zGia0dJg*}t+^PQK7u?FokiKmyA=DfT_QIYTs3%1n(INy?gZN-RFi#J*55ks2)-}o6 z`2;^C;D@&Jvv5tE9B;@|1hdlwPfE$h#YkDFqOh-J<8W(AenY;$K+1efw_psQ;AjBC z0EOkWMnBU%hzPQ&1=>~CqD^}p={B=fB;d@2RfRG!dyQ=6Ml)%d6wjm$&!i7obBE1S zaQh-Q?YQF)xHq*}?Q7RZ@daB^IJ@IN5&o-}Ypvn#BtD5?xE=yS1a60|Q<$bPiHdJX zs84+OG3a1mbaY@~RR2du&`J5yupnzA-IbKDSjMx7Ip!=3YBV!6?eI$vxPbIw?HnkU zVTFFu0d3gGPdj=I3i1hx(E8w?8?>?o@>*HgDm2Xu1JX`#Ean+1@aFldgU#mY8Emps za>k3`BB`%ezKIMQ@LZn-!0WE(Y?nE~Dd3#1*Wvm-447Qnr>E6W+4*gT7wDrd!i$jY zMiaw% zG?#L)sKISRO49P7*$AtIAZU~h{4jaz_IzK{%cfWL?zT}*35C_HFhVB7Y}^ck{a8)3 z6j#N}q!lx(JP}=-VY@(J)p6_9#HLxP>SnyGXUE14?PQ*zo&C*H^3=tR?`dT8m7MCz*5lBy6p zq>TO{HFsBK8q}x_)`4;J%UdG~z3*|*LyS>mS-&6_ehQ#-77MfZDU(>N1)I9_U`N9+ zH+f^gh4O8k`BXs_ftV57Lddg*W{>WEa#%=S90s)8kK@;R?7;nAg%35yGoYraMjAEI z`;}1>+j>fSRnp1pAepm}PKtvdahlK+xS-YDYYOrB3lo-GxnHD<7rn(hhM-Z%-2Z$g zpggDHiZbvcIsgnut}WH*rSX{FCUvEzuBukQ(a-ZS5=)k;9E9VT++U49x4BZ{Tm zHL|19Ab?t?vA>~a<}B~~I9MXPO3jmISbtQF?^V*j4+k~Kh!yLKj-oScKLWA;GWoN7 z=xGvqAU?clBP2(fD73gngTRVf*TA=)k}w=7W?ev;(d6>R)Wm^qUttviohjljZc3w- zP(QP1wC>Ku5Ar59M@9%1NtkIFV02d<+>&$Y^lB%byWzGBRa9BPT5*gDYUmG*m#6ml z4LLOMA|ULbd@B=Rt6V&x@#a#}87oil=M-MN+z!neF<1k-Q1~$y*L6fUC|O|NcG)dk z+^eYd8FqDY-UqB%g@Xf7Sv^uEX# zdD(a}u^AN$OnvT4nihKguQ1Wx*L-(B|6z2jXt+CD)E5 zlfr~j14MK+5hE?`3uzvuri!35s%A@U)oy{oUflp(^z$vHK%k=C&bGv-C8t~JImU%0HUKZse(qO>{99Bvsl zib(}khqWh+7ZGQbGABDko8dOM@<)OQY{P^PA-faqW^(h4dcP5gfL2U6D>u5tXVDw! z4Mbs4R*60r8vEPgID5etTc_M|88B0cJuXn~4LM7zoSKp6D`^Ap&w3lB&6$*ApI^5c zGfA?L%c4rxTmAu$dCxJs!B!LIQhFfZOOowN7hW8$EfWkx-pCHxtd4UPBhZ$h6(in| zROv`G-FMhB-{;zL*jHHTf_X+S@Ji*O2BF#>vxP!3ZqV3cUyU&Z^!-@BBoDGSm6qai zhJve-6jR!`c1~(RRohZKRgo=3Z=zr#O4XyvilFJqv7EprbvjB;(FSzrkHtbybpR=P_7j|qGl{n5`~^i;e$_m}tZm)Hi5Ev+;t!0nAcuGY zxHvBZ`6_K67+`~ubaYA$J+tvv8MtO6sxEqrL}BVyaWe4=H)CJ{RSN5%?>0l57NBa& zV&ZZVbvN}gb&C|J14!Gln%Hh%OS~QzOx>yydwkN((`r5Hx)WSg(l$~V8J%PQ=p?h* ze5l%M2G{s0$crU z#!eygiTwrF*K|bMArB@?oO+F*nkO0lWAV@KPusDnKx5Fs1LJdEP0H=X zBJJ-uH@onSH20f&74iUiE_NL zQnlb>Bx9k4EXiWVg_N>0SW+AP)=lZ{=j{!hO#MtEEAPS6ZW;7 zSf;k9&Ilhol+gZTemQv^)H)jQ9^rYe z#tYKj@&l`HdyGwthiYX2ztuvHy`V;9YB zDwd^XE48}(sIlFwD@RtoO0iYxX?(npiDcZMf45rpD@q;t4D^ctz4a{3oofz9)c)I= ztNxP)8hCK@JH~_E%G(JtE_XH>JFn6?5QGp-T5MsbzrE znukDnlPT``K~uzJew$MRJxj6_&&SiGBu^%bBGu@A4{0*HbrfAmqkM$*%(x@iX-9o> zT6lo5;@gX%mUB)FVx@bJ$!52Qpox0xgM9*Z2+G%K%xfZ~st+X3NLtu2pCPyj+9C~~ z|6z3goCto*p|3WSz{IkoPYiQ_cXd$WzP1wZgkxZsRPn3T$b)CP+$&g)A~}OYUw&Yn z-|h7cD)Tk1x--q?+dxOt)ly4pF(WPxpR?4Ys)eVVcHG^DdNez~&QgFQbP zT{fIjOL%rOszhK21=6f{PT2 zyd5R4m~vOvSb=FB?7WrRKaI%|%8wlE0Gp&=Punl6yX#@uJ{VA&2xr zYo`-aamROVpiD^_p72LBu9@(!;v!M~XlB;lhG{4MNZBblPloOD*vaSE%x-s7zs4um z)Ff3aKS_{CCI5*cI&RfyI#9ly+*wlrdA%3BFn+qcc3C%Z#_*S853{*|*dKltn zC7y9@#b#L~m4Q|2fw@IJ`EId0^7Q_(9jC7biWYI%4J3HQJUo{$5apf@O%xp8i1QgR z(DG(2ZzTvKkdZNG4qcYtjw|TaZ1<`C#HCs%b*wZ9*rPEkwt=00>Fz<03# zU_#wZ)q+fj^xJfa_v-5qs4x4aiyu0qeE>M4YMws1Owp7B8tBnWkjFyL^BwxQhG)(o z8U*Qm&F0X#o7)+;h~I)Ca+XQfffjt?OPyPADv^&Jg0!8tb4CXWn2BEK6+p5+f~2!Z zRYMAdh)MyQO`$nIxrqWaNjmM^;Yc0+?zDJ)b1NBg;f|VW0&z?=J*CBvibxL|92s@~ z(#eZ^_X0Z@c%Pjk_X>CijiF<=tI2NApn!Q}q<;E@{;mAwl%csrBnJlBO!D|$=f$1b z^R1@4sgPTOs~g6B7i-6l9?XOaeXbgZ=LTzYeV&>JS|U=q++1PWyhq#^tn_dM<(L#6 zoT?Xhv~N~Mjnxv=t9v%p<~G%){f5z!^~Byza0XN(bq(NsqU1ti7(!t&hgPW|VXFjX ztCR-V$nOLtxTL%oS;fT0+CkxV!zGKc<$4k6ThZ+Tk;tBb*K-A`exdY7oOUT~&M_Zw zn@6g8%wbMJJ|S60xDFG_aFr&1;Sh@qh(Ex79NiN~mubW`KEsBdvIb>p&oa0Q%_31(B_(a3FgQFW(=#Ordovk@Ytc1s3W z&^6x@RiSs9Yj8{}|NH2S*G!NcrmEJ3{pzn$=XZ8UH*;iIV>Rt>L3CJbDen8z+haeN z&LWQC9?-1}nU$RgFWF;2_LR5RK3+~(zU`R{1rLHjnQ@}RgIOo{&jOvaL0+Zxu8e-A z4a-w<9^f$Ths7v42{^okK0Ii(hlt{F0bCHwcpe#w1-!le#pE`wbH>r6OS}6gvC;s; zV?eMm?|MuIlIpVwwsTvghd@`r4X-8h@70tNf6pJk7qGX}6*n0{<$x4x7d5mGbZAf2 zM|A949+S$H^bpJ<(qyFu8d@{f5C&2T+}LCRLj#dXnH5>1u8R4x!ABOVm+p;z>mRd) z_1n0+?E34#x0fOz$AOJ^CuGe6cutu=w&QD!z(E?GGzccc+_|l|djQraM_yHay-~&e z!M z-nTV`a>sFX40^~%{r32*EcMK-O&N!(_68aDs-9ys$H=I=Irk%Q>H`&l_Byybc^^n{d=(;1`NqW8|Ai8KXWjSUZ zrH6lPKR5MASwyP!=Ki;v6#YAnHNpzW-tqxydW#_6mYpdun|Fed@XEPE_4{`}HS<1EZ9>#pBf;OFNP5dJP~Ec4ZWjzHuP0V_1~N&z zsE65DUkRqM(KxDXezH-Oc3o&eaZO%;#!FuacDF$yv&?{(Zb*w=IEa+azX4QyfgQuk zLp&LZVV51-S~K<9 zsu!8uk8U3Dv-&!X-))yJXyg=@mDR5r_!BfI<8|69)pBNVstm5Wx5q$JxH`K**2nM+ zH$tDTN_D*HRmg|dx{)BNUSBbvcTI-=K4a3a@lR0pV4I3YSl`(9WxSF54^b7-XQ9QC z+O&tiAQ6QYlo4OeH@uRwzvCL(J{)?ItkeBAyx&9#0wk*bCVKId&5jMfkKJCwb)zf- zC(&U_S5t}8({#`1Tw}IFW=cY8&(s}|?ykgmk1s|kk)Q&^-a0OxjfV_48l_a7mXfpE zyyt!dS(w+PGBsbx%|m)G>75*GIID8g5vVM>L~v$pzly(0yZBL2+f>EZ=J0 zlAT@L<7dg;CJCi-*kI7hrY|2#CfklOObCNCzf(vm4S*4Wa54J)-)Z38IM^wuksl9! zfNt_4k~#xx0NHHLR~S84@a&7TR@`5*HFCdy?9XYZyLcILG_r#d-OTa&C!@RnD(Gim zpW^jv&aZ}`qCl@Xv;*=+h6Cl_QT?!Ie6JNm&k`+L+6ip~oNhoI6NdA%Pk>cFG|G57 zjV3@(vSt^}Chq2j-Ju=-x`Bjq)`o*I%jU!rAT5G^-QoD1rd6}CC-QP7Ss?wA)2^+d zXEi10(yosD^UgdPcA{41rncq)CR00O7nc+@T}=XY%&$;L3s_NR)dna!39kUTO*}7Q*@EVDm6}po zuAe31`e9C)+3su@bJ_j^uLpS~p#C(WauizGw707`K*tKz zYs0@_PEfmM^Knyn(T9@Rc28oa{JRXOj zg^@{fL*plU8ET4l{cQ34b1X|uB^lQq4w?2XeWE?gmLm9n7#x5dKSM5p$|7?L;{szWu!Z1$zyJm z0{~5BsM?DI**zFYscpUNQJ&gIfA5u5#O=nEI~mC%3#OgAVr-egpgDp(msqkjCBddk zU8tQS9M^dN>msPe60~p$yJGzQ?984+J7=(x%!z+ri}@%@|=37bX~rU2q4#DI8EGXi=o=idpUdfX$FX z$+2cH^!&pziAMg(f7R{npVYUfhEOz%TVTUcRF&o^%opw9>vE9%uL7R$X>p2_ST;~XaIINz`a%7AW$T} ztPKCdeobpS26iR~l-w@tbJOfi?A|~8d_SR$kQ4#q#ycXcVIWBCXsu?a-BTFe;@kP~ z#E`}i%Fu!n73t4FQf<05JQV_ARhH=0Vszb{q0sQ1`%uMPAI6(@!;=IK_qmM4_r{r< zYHTsaGOXKD=Iq$iUh)*|goECD(gS0f!nDR3@(mIOCH{myv~u!);eZt5$qW275nK(~ z76`v#qP(iqLlAnY&PuH$^sMb!lud^%T|rLHCHFAruWp6Jzga<~O_Cd%!ufa-wQP$5 zzl5pp#J+cse0S%37IL_&2fl1onJNaCs%#FjZ8&6Gd*EXKb-sxtwM^f+qG3c4*Kegv zsHMlUB35Oa*2|?sDQUtguZg{`3v0AFgtmiz2SkmwnSc(_=s^BE6?Q!3xUMUsrq!$h zpSy0X(fZN%_J=<`I0iGO zQciT|1_PP4OY=nujM7e0fF$6h7e`zu+#^UjIslQ&!00^ko-VmvQOkOT1YT|4f^xIz z>@q^52#?f=hQMzchjbxK7*s5HZQ8?_4$8+2rOsJ9kXP~C5KkCTQPp^jD#5!Y*BkBE z-su-^24H^wAEoQ7U##c^2Wuj7i`$1BnF=~{{AL$(ygx3(gQ ziHcSP2U@LYCvMhXHb!M3Jvg2QDf*s83Gw>gmavnlSw6^HzDe@tdcy@MfR~xFbv*yh z^`3q9J<0BQf6Lqb0=p6FT}kL4V?6C|#-PVKOH@c};I}3^zCG$V47pZz56&mh39+@! zL=SyVf0l^2`x#g*PRocx8in^-TZAX;hXuZgU#Wc}P5u!G^25~=i$)cBy$$SGQOd^D z1LX{IMP?Imeje6L5018e|XOA#>q(-A?493IPjgl*{AqOpD~In*jRq&xyG zk%@j-CcK9&pM2wue&1>L4?e8ObLE2D*0? z0%@1U?62gC^aI+?!5g_j>7VExQEzq{TIGT()jVvka^%V>mJKV42#L$%loz1eRkEl1 zL;8NI03$y6J9JOtwYEYEzT;-|h0iUix{x~0m4}mmHaayFd2Gd21&{t%1*4+}=qi>2 z)_Q?_D3CT&WP>9woR|(%423oeJEi6%I@>tjVF)su8FN^CZ2l1kM_$zB=L6D=aN~1f z+^FAMo5DN%OvD4RmX{q)z{3kua&u$Up6nUtPg80&e<(CFI-UOol|X90SO`(3p@W49 z5A>7%7{ai;ZW9uh$(2A3(3*O)f%g+a^aX!r23wx}fcEq+Q2vIV9_$S6L8bB8b3|w} z5D)zdZB>~6LQG6!WPF8i2!fR&S@lCBRuM#46baUj9u~(4OJbaLVw!bHc4^W}XiauA zxQvu!H-k~K2IOi?o*SpN3MCQiply1-8kAo*DCc8(dSGY|Eiv8Rm{ODKb6g^3!K8os zBl-mAq`D8CXvaogp*4WjbW)`(zChcI`a2?P-Rd5qf4-F9Q<#R)kZ}QFlF>^^?L#l? z$0QrT6uU?ghLB|!Fvo_al&eH8O5`(CMip6luTA1TQ5fW#^72v?lPe)gk)py-rfzF6 zT1gk(5Di^Rq)K=vVijfR>A+Jrfwnxy-|wS+AMu}?r4NZ{?D8q4zS=-b;6sTPAZ5by zBV3ekUb=ixB!&9FP)h>@6aWAS2mk;8K>!wxRf3+A>U%+d`)?CR5dQXTa`t6Sj2lQ( z8c2%^wv*Tnr4JHb!6}s1d5~906DXVW$~k(ybI<37{6qbjR^YTns`!aY{Z}d>`arEz z33c}3M79$-G;(%lcE6dO`DS+S*Ox#24B#wE299AgO2b(LeRx-?=c0HI?$sug6NWB--Kr+@ z39iO@!}Ur{dzR}koJysO_ry0M=SV-dKZrcUD$4K9wn`$fv4vC4&HJ9^ zlnE3eknftV%@7Uni&aVS$L4)uemNy7L9RMJWw_j#zm6G>2J~w8^J*AnIC%h?!I*bz zo++A1zQjL#YR+B3ge zv+R=eI99Mqhh=wD=eVs5?{Iv9yA1JmLx#iIHeNyb98e7ofi)Ga$#DuvhV1|A2Zm$2 zC$w!0bYzktlv32kshj5H*ELxsqlL|iBDGC_Pc=7H%OS}YBo!z5DmaEivvV`ImKjdJ zs^6w4iR#63Lb@zOCr>SBsPN`~?6cN|#aAxhEH2oHbjV0p1cMI!( z!kh3su}Ke8D!o#mrr#%=l|p(6gY*vf(Ob>padnGG3PDqsiaPmC($0~l(QIUf9zn}& zA@m(-8U|?WA`I{wPSD5$*}zG>O>6*fKc3%U|VrXM4*JUmjzYg_1jK*1h; z5G166JxyN};2DMZoIW7G(>Lf3oX4M7r2y~Z1x);n3jPg}$xy(n=*2r^6(aN1-3tbgWHIPQzZ>PQ#Dv1 zjUXFTAs1NY@fMW#5LIrB>@*6O{^Ah|uMg8#`u_t^O9KQH000OG0000%0MY{>(K-|W z05mKB03nlMcOHK(V{Bn_bIn=_d{oudKPQ>Ydzrj!14IS^M+FR7l`2bu5fXw4BmpxC zG@#N)@{){9X3|+$Y}ML|cC%uoi(0p~mM+p_D-#ea+C^Kt*?qT*TbHla?yJrBKli=a zk}=Zn`+mQEKxK!pYEXq&zIhU5<12UH9kXTX3Lp=Y0i}9ERE0hP!%td z!D4Ba2!nHUu9jU(b*|C4);emm4_Db`Lc3>&dcR< zh0Ls!W|e<5P0}=LyjtT6J=Dmvb#9T*i=UQ5bbhtzVd<&D&84g>~wvZW%Suv(F)>*@5A{1X2*%J;$%%RQE$Vk+R#kzvA zxCKHcFQ)eHTbqcFTH$zb(2PegS>E5Xv1ilPo*i4-djp-DdO+57g}K{o44L7P#y~t8 z439K3m9|B~vA7wIZ!tp&OXq`3i`KQTU)z7*)wiRky>IKL-iRA_H;?6>%b1IlhTKm_pZ|~g^=-k$hscK>>+uXb9;@2#Dk&6ZgU(&#ev{R*o-Hl5a5E`)z#B2IDMuCXOxAl z_?}2~S6^_>`)+wT$HW&%-hH(SaEPYOOmN7F6%}b|wpa?LI z?!#xh{W&L>Vv(8#oo77j_^SM;!}I_F!bd1_jI(b%WuWGK=V$wR)6Ofb!FcoZ8S!;# zAZ`xs!ajAH#_!Vj-5S4#sr%FvK4nl{J)?vEok%zpj#IoMzWv~TP=Hgkl8AqK&41EP zDhTfV|8FQI=X?a~aBu{vZfXTl8Mm-nh{|JDc&NiNhkC8oCaf3&snP*9l3ZhdZ>OD- za1^%8&#ZLBgxN|*b1>4_xv72cpf&ES6(*uVWX{~9Q4M0|u+<+8 zOhKHp<6>M+C(c#2cuO(uX#3OMt)MbT7 z;-gt7SwpEQ-T-^2>RekSAuO2Y<|v+H&@(ejfym%4EACXB9K)&#RF!{LWK$wOo`?ep zmN|yyf?znuDdEhb#?>0xdW0{*7T~En5tk@$gNcwCxBAnTI4i%Oa@AIr3#%)aK8{0ib%8eCoZ}R} znPyk#J;5V$TaYN^>RDnBoO@ekW+^@Aj>PO6UU4LrJ-IeII4XbFzQIA@f6;m8p3Bsb zHR|B4_@f5j$87Ln>3y6(flKcU zY8Z4I-EPqP=zxDgchCV`NoF8k^a>9G2+T(ex|8lQ=!107phxIYU}_ZkxM5uKytvcg z`}vbdHZmK_OvBzYai0Fr5N4m!_yL2Da?+q*&@T<1;9~|K=LZoyFJBE%GCJDVt~2-q z!}hqUY@zDtJ=;$tc{TNA;M ziku4J=8xJn%pZ^V4gMT|UYf^{Vg17-=#$@%pQgaK~bb{tE_wk)JU5OF#>Ki=ISzOl@yf1;QH2&czTTyVhhc$!TAf<|_t& zRm|}N;W0U`46%GC))9Ga)n$ z{>>rFR4@t0f&iF5k!BcZK*|wzk!bKrr>MDYAq@I0y=d?+`Bw)2ngRI=(Yis>M?Qi_$yF>_XHRv4g&&Fvz_2O8w z`nNQzYw%zBZ^#9C5^d+Y^p$nNOnLY`q$E_i(wyQ0?K9&}xWN7*Xm-9wXl{gdrG|e_ z>Pc;ya#@5W^WVj?^I|xQJPSH~qtVD7`?)Je=IiQ9 zgMg*pC)re(YR)m0qS1qC16Adarwk`gk5Mz$W9^Nrm(Vs8tgss7UV+lLJ2zzAXaVDT zJRNpA=A1V{;kewwS5{BoI(;VZ`5S-#&mNU>b1obaD=f()PG060&3p@+X>rkcieUyi zQ@*J5#H_e;qe0wcT~~AH)EPzbhyrUxbKiSBdQo>-3j_u4?uA)|`@q1T!K$GH+Q0xK2PyEE#`>aP_D3 zfN}0R%~R;}_;o7%d`L9Ia?Q-@rej-atYX%T!gF>iNjod^hIWtb8VW{ZnQs!ZU*f*Z zT+T~X*2YX5(Ya0K*Hw~YX5Xd>1&inHaESySF_8#bsQ*b_zW0(>BK zrvj4iW%B}n6pA1Z6%B?WF?nvm27$p*ODdO!en&*U&yn6{R8yyCifJTsU6Qb*W|yG5 zK5CAPsR!WrDM3Hax5NLlZK9tW;b(?oQ(`m)>ut8Ms+5S$vK^!*n{9uX4aNwDcSm-?f2;BsWBbfupHAmu zu-1KX^}TsM4drXA`PFSRpd*w~7KJRco@hn!Kchfzff4`#t z0LFMJqhF4>d+9@H4`H;O+~ktknp&=_KSl+|sBnT@_p41GM(e>R(fL$H7tlx0tFg)H zqx3QLTg!4K2CJS3QlNSwN}*zOpTlT`G?L$QR%S7ptNsjPoiQU$G2tj@PLq*+y_ zSyiT4RXVJsC;GW?%3=CA*1(htu_EGbK0)q*3DUZ1lB6G}Vy5o82x72rWV>r7f}zb zQS$r2eK9SePtbq;O2W#4(64{U5$n@RtcM-3p1`~&|J9&o zg1j}gM`>0~{ZX1-<8vLQIW@kbqr^2QsA{0LZh}rbN^@)GxQ~(##Pc$eFQHnC=1~`&L)}yl|C~pglvW)!x3pHv(poJ`Yqcz`)v~l!%N(twC#Z90>9;IL zzmxcRgdTr&g22LVp;=n<0I~P<<21hjD63SX1#0vdm7k!612sHBXB;DcMy)a>LNCpy zKB}fIN_?B)Qb+s=1a?t+%OB%S>TEoyT4T;9b= zT2fQ%Lm-}mQ8i4tG)b^GMDisG3rVVzroLrCC4GP4E~-004Fe~r5z%z6_q-%6!(p%T zo{!FgBwgTLj!u$ROwh`chiG+^Yi8;ubZkZ!c$@8=BFXBL_d^8_jZ+Mnz*fHnxFH$< zoVQ`+Qkq4V!K0TWqwb(OdJU43Nlmm9kv9n64`J^pb`K-ljvxgE(-@Y0pQF#iC<$5t zYd?Rk{CPNyfW!0!`XadNK;;wkB^c2IZ+;m*E>s4F8(yMujlROI8m(GMUsXnDx)MKM zqbD64_fw%A2hjJzB(-d<5yW1UNp-e2Ltrz8emI>jazpIvN)+jRgT9HK8D<6YWt+{c za4*!7|9r59d$_5{adVUV1g(MT*A9Sj>jZzb_4wRydy~s?wm7;;6PNq6B&|z1ygk)f zFHXO>si?A=9@3k18Fei86t5^LUQy~R^65$H99Ujla2H*6j5Z``PBf>sT9yC$gn zWL3$W;{E1|lB!bmSz1*(n|j8I58gormOT3p-bVA(oVB79?B>>DuBzlXZFW<=PcMI* zQ=Ftr4o%*UrCHwIBn5m$kCE;xN>X3_W3;_KN&SbYuSpYzDR6BCd_==nd0(9eRN4d$ zoNOx3f1oA@`pQpAk}iW)UqE!>lh1&)U*I#pTqS_p3}rq=_6 zS0VJTre?YZY4Z(8G}j_h--rTx9bkXCAEAFeAbA7rrZ zu@|Wk!SR%&M_!XcBzg`a(X$a*z%BF>`Y9Dc26pxqaWnmlehv$j@iG-eZWTIDQpqG( zm1)abg$3aT1|06BR3}v#TZ{yq1>^x1UMqn6pUE5^J;t z0sQAn| zT_C?{aPrV$I6?ATOAUAo_0%KV-S4$(AybluZzDqm#0UbS&O4flq@aJqPyGa4VaE=V zL#7BVRQ2+c;Pok(_W?+fq|?CNPsefpc`)mO*pg0TE$KAYLcak(3b1=6Abr5es4&() zsRW*#ownyH5d9VyRQBYMb62?$X4{pdPp?ycN^L4WG^|?EJF3v}7S2OQbc8P+B zI&O5A!1=uh`)lxN+o%SXA^1fH^ydQn4X7Tg0GCUkUN3@R6!y3V;d3nlNbGefEHD=o zzoXydga$gB{(znfGjr*W^e1?56gIZ!u0_fJGyMg>Kmj83C=&Wn&5K5M&@iQf4MSJ;YkJhMql_O_u#S0B zhU*O&j)F}^%rO!<&#VqW`9fC))gb2b9ClY&^}~4>1f)~Q>Kj0 zI(jxMo#Ci5LLEWj_R`(-7x$LU#qz|fMs;celUZ&h9<3-)2tfWlK=+2CEf+koW_w?kb4aA`UWR}|U1LV6HIg~Uk(L+jCnwm- zvGVXvuupM2=Oks|Q!%zKW}~E^vXZ9l8h=)LSbEcTO2vx;4qSl_bP7CzM+NpV2%}vf zg8c$r@JLOm6@eTcSFml_)i^b_l|Gp>%#?Hlu3=W-I&LVa?y_eDUShlpFAKban*y&g zc#UbV;|+l~@s_~bct^#%0`K8{fe-MZijM?7#wP-wGWTcrT*VgxU*anjw*3?tV zt-yDmH5 zr$Qzjse5vO=7xgaidVJrC0p6@)nUGO>(kO3)j5EmHB`b!^o(5Dm_adlOt5Y%rJyr> z@A177h4PbNoo5Fm1+C#qbE8ZVxqr6KaA{6b#%y9jd%Lx^Wf?Q6pSM)%6e@6SN`IQtlgr*5 zVsF;|{46~<#zER)beUm&ee(1x1EMxN3Dt@{cq&1!$8aqX`(%ISluntok~ zl2kYC&Z7#ow6;X{&qIlH%zvXQ(m9XnNONc&p-6MhJZd5fsQsCEs?bBQmL!1z`Y;2w z5{+bW5QhPO$2JuDr@2aJWI?%%8sFyKJ5Z-0zo9CRx;v+%py>j~tsVS%21 zqE_e8IEN$q^Vm3t9wI1A3=WzWv1zzt5u4{wN6VJmzhEn^+wypb_a5FDf&KTTha zr!j;W;y8l~7)AmkNaGy6XK~!341bSt{D=wsgh~8L9E-T*XD@;f$?d=q9QE^fw~)s$ ze!wxRp+eH#h126i-%X6_e{n&Ds-pLAZ1@MGv>{JGdK5g_*hg7^D#*HDU#?S4B#(!0 zS1g_g7z#$0)DZ0R`A?$XUk7l?KO3Y_57DlPXnPU-^%C_7X#WGV0i@Fc3N83v*!&p) z08TcO-liyj2Y6f66+Y)_JXwAjw&NtqR6(qlxlR6EN{#at(kdU-T>shv0Ie7b}9Ea=x&t9CV8A8k0viY&&^(L z;muxpl()#^Or2Z3G@09E(N>+e$(-#v@9TlFZJ+cLgc+3exH|Wtp3aM=@)!OKK+uf zl*d&be!p~I?cr-=?tTwno6pzr_409pJZ|*x2fXwjzDef~dZ|VDc#UtCo?E09m)3{8 zd@Fz0OA)?J#QKQralpg3>wJfFe$-1J<&Q~!=bawDOWt>T`5wO4!ylKCPY45_l!>46 z@Ifzsnm;2Oe^%%Fywt-R^*NdNfQKK{`H;?sJ^XnOe?dmS=%qe>NFGC8p3cKM zACdRNUK-#p={(}4ePVz|st9kr1KO_8q zJnP}F$@@9kUy|1SGW**)zpV3jy!0Vi za0`D|R(($dc*V<$MQ!`>K^xt6M$A}UI2ezcaVB4V!-m>z zO z2TTwDkV$XbSbNUW6)VwdZ2*ymHYRR#AY2?w?r^lH$BZ$}Y>LKus(WI=uCQ6XHx}&g zH)GXJY7k^SUD3Ufa5UJ(G$+@@#(H~PSm+NXdTYUdUq@Id&(F1BOXeIbnqlsL>kJRX zLwn2(p|Dxo*=fe(&A~`e@m8ISLc>uPfSh|xC=yDnWjed`7;+t3l6Pl&(RLuTVeRfv&p<4g2t^|`i!6_S2t}(!Ct`}u%yFhg$4v?nbz%EhsAE9Bx5dIt6D{%) zGf};*wGmSa!XjdQ#yp*Wgzl!X-Av2hRhtXOt-=nvFi{_hr8ZB?W~j|~h5F?iI)gu$ z{jv=DE$B8AoxRx{Y%k5GkS)xZvE$Y_JYYh^G`r&UsQ}?!_%p@GiYB&y4_99p>aPZ? zDIURpai)ITdV`42wt+slZg&tYfQ}wBF)k=Dp)C>YJij^Eue?a-AM5-Rgv>Z0GpL+# z{9cnS`l4K@;!X7Rr!?(`b9PBsPEV~|KhWK6#>}o(H6p@gP{|b9=*n`IpMvy21jpsxY?k(AT!G4+@nkm}~ib!0PzM^zI8}G^{%$ygG4!|uGs^**f`pwRS*`>^w z7wk+71jDMW_gQL(M#B~if-!$?J7;>WODu`0lXhoM)!Bm$+Cn{%U}7K!vWwq^);O<5 z%*V|{!#?;$LIPon8S4wh;}+;@;uG$8qANO(NI8e1wILdR>kB3l3K^VXBuY%~?*M{j zXlF|-Dk(ha67b1>rlRo^YEr?cdK)7k8yo0{{xacUf@QwCXkTA20*5v*DH^lgSm#%v zhfsV+D1x#EOgl;!0khrFcuP>soo8W*N;~7w0~7W5fGRg&m(E_W8#CcIMZ3qFT4z73 zp#TnVGm?mZ4W>{tD=o-~s~1v&gho}DO6RGn3h4eAu`ZsrZTxh z?e7%gSa4wy$ES^F#7?bCbCX(Ael*qv?|!B8ui(aR;A8ulJ+Dfp8Envx4EhRv)u1=%O@kif-+=xJ72mSxw+4NV9x&)2 zecGVU&}R+0kM1}4cl>*u{~+%_8vG~zv%!DiKch}MhDnwPxxX6xH~u?#%@oDpfABv6 zAxB?-Z1BH$hC#2=uMGMjH`5l8tp*xM#6pcY8cNvC4AyZ+0RwtH-i67K7Lvw(F<`feb<*3$>uZ8HDNum7!5Emm@Wnq;F=FcpF{iqZJ{*rh}BX@U|Zdv}CEdE`cy?s#>VvbcSuw}r)t{OvIqn&DKYsH0qIjRI3v$S>EX)?byi_cV5 z3D^)FNKoKJGw0aFp{~^#TD{g_Xd49i%F;lD$`;DpE>FMv{iPLIZ`A}A4c z?Q}!is5R=^CPODqQf+o3fY+D_5`tg%49IjcyVo{40cL!!HO(c&(Hm+(?U+pW!HT6mnL5UT0qumlN8 z&7~)Pmy^uib{Uj=_gs~KPgZi;+8c}RwNBs@vyUptWT!eB6H>88B33Sy zL+u!`Gp<#pl;*rhafjlT@Dmcz+P1pJ#w5Da@E!nt2bB#1c7cqyB9%_W?MZ5%tP;j+8sOt^0$coNUta%`H9F(NKB0 zxz9pe=uQ&P)+kdxcvry{>BJUGj&9GR-rhOI5CTuT*DnHp61xZbyMn^5jt&d4++8+8 zI!hPHEnx;EN={X3%}+!(rZ4x3OB-_s3Qq7nqF_KG_L@~%cPyvYL-B^b{=}f%f~)MV ze*PFocK7jwN=^Ehyi$(Ir{>hu@m~ix?%1U>2 z(Qp{u))il#Db}&`ZE23H$2_^H++bZl7QlDLijW_Q*C&q^&}Fa-emEH#tqVq?5mfzQ zD;lSk=D1B$DK#$o6UH;upS~K@_Xb0W4HCr@RhVRd~eY#=Y&2B1h&{m6Q+}oE7zp>u?!~ZUoK*| zwWWSa&KRgs0p1kdi{u*=s7~&YIVa~Hp5%!W@Se$6U2ibf2FEjjTgu6tVdY1~DL2Wc zo)Xge&*T>I5ue>6;nGA>Exrk=x$=V2VWZ9i|>zT ze3#<;6ZFZ{_ot{(Zq(2&luI@BzK`x#@6XYH19%r+pkLqR<58abP9M_O>-zfCs7T3 z0V8D=P5L4|M5J266RVbRrKy(i-$Qwd@`~~yGMdZ2Ncm_?XsH~ci2)~n zo|6JDbb5TQ5t`gy=5zU+73ITJFhqqD#azKoWOp28X@<}bs{uh3U5#`!bo z%fracKIafk3Ah|9-R_k-n4fxpJjL#R1Ef0-lGCx$Q|vham6lfw)3mb6VVYhh3ydN1 zmHS-7G^4B>oinleAk_yv#k%_*D)70UAp`Ru>Fj{Zxzc^5K3c4Qj7}P%Iqf4fw|$uW zh4Y4JKDIllZ~+=aR5DB_KVIy$YUPE68JvX?#ioO9y z*Xf&>xtcuh&;*?#%=wPf_$@Mjc$DUnN2g+)igfyxdOoiv5bHGSEg6{g20Sy5h`l07@{(J3Yz5^Q--MmJ}RCG3y)AG z3{=%FrmY^P#R0d^Jw!_ay1bV9^g{uU)$%+ZaKf?klGa=fVwFc|g&1^yWpeLTnKMp7 zuei?Y)F>Zkg}TQV5wzj?^SQh0|M}IEA%gihhKrnxQd$SYOLOm_19s| zeyq5T60q-Hx`77iM!JJO0NdQ8EZqvL&ZF(hf=;YnMlY$@I2%ClZF(8j8briBOW#p2 zFp{$Vh#hOv5MGJkv9YeK_qXsSP_0 zjy`i(?URQ0yYRdlcD@IZd@pT4+G#|pNeVL$c=2O}jo=|A%qArQk|Vt0C-hTr{4?|# zsh*$P;!Py&ZHeE1U+DD9HLY@M8Zrb zwvLp%9rSD4cpWyL%}37pjlwgL(?h_f-Eh?m*zw3sww*VB?o?<;bVFg&5o&H4p_Xls)V2jHxw`lp<95=Jsm+k8)3Zw3MhpNmLYYnf*S4QiP??d0!aAi^LS@8J+sPFdxc?YP@q(9Ifp`KoV%Aeqf zZrTdkf5xb|{SEXNC|Tn0YWgev4T_yam(t(qAK-m|BU0BtvDSe-*U-P{-=HGKSVH|6`XhA}mjBlOh!ek4OIbKK3$xIe+(3^Jbje-SXqFcqD1lHB z#Ha&n=h8bWmebKHV?VdOcoI3@B0r*ahSE$C7LPL7;rbFb61b?Ve1@Ed5qupe)x?iF z56HJfTVa>36mJ_SJ>{NAz4Y{tj zXc8=+X?FRU(GFHjP%zOdNOC*rN60*cW_NSNGuFol^&t3qTPnn)kFAs{u-IMfx|imE z`JBb>rO5mG5QPqqQR&kkrt>t~aitqk_mj%BiL1aK(Q720nGc7X3}a1!DW*dfKZIHh zA!@X13Ylz|jm(Mjsi37C3Dx3z|<$KRC?NznY2rIIDMnFr4MI!ax6mcFWA4J?f*`&Q;XOQoig+Rw^JH4a1r*>y=(z}BFon+LsdPS1 zqs!PwSFxY2;hA(T&!U^qzJ+JgtvrYB;JI`U&!bQBeEKpkP`2!c)pt-8D8H>yksgxf)rNWyLxre~l zlS;Y=z}?-p)f>p;8O6S-`WgQsI`!#19hH}kLLY882YrHk&dfrtj`(&>^3et3hA zXV@5d1~!qXD=NJF2wm}cx^jrFYAP>${}5d*r%~&m=9MYD5Y>CBl76axwZ!JzfR<;f zFxKQJe4FqSkX4|qrd-9+QoOEdu6UXjIo8guK)#z-ru?pA_EI<=N}H9=V(0DTa@>EV z1F`l~N&ok!a7H02S74(`Fi}O5xf-Fim=^I87-1m^lZ@%ZcDvppuaT zY?kp{m{nM>NvXU>RY$CU)H{J3Z&RVp^LX~_Afn0t^k8Gklj@K|bo~hJZ$~z{R%)8- z#B(2}>m{j}(z-!Pxf=y3arTg)_`ngmNsbzB`S>6ZMPlM+c>i-FbPGb~L+w8IFx@&# z9}ehcl``ozpFT_Yay! z-sN~-PFJe8GkuigQz?(v(Il=VAFqeU*5feJKYV6ml))(CwD?mCie8VnwB+7%+6l!O=g# z8CwB72hxS8D!q9Jxp@~A@NSyLX9M@op#^+yMp`dPNnFBz%a96LwU$FOlGf*{%E^Hu zdm68Ri&~Nzq`gIMx}-Df2c)t9$r@f`>)|ZBR`P;mrJOai!#Sy1f_YO^y(y|*P_=Ffyl^$^rohW<)lESv zQDe__e3~sTM!?1%x725xdp`?m+^PNC_I?`NSaREXx>K3MfdgDItm#D(wf=h)4*VG9 z{TH*@z@7vNSE58q=)-)HFs5if@D0~UW6!b>5%9Kw%S|HmR; z5%9o_UXaU^s%aT&-nLX-6MrCOHBB)xW!W?pQ^4Vi^AnRZQ>#l0Q}e6SbGfP2g~j>o z>_q{Qnd|aRIaQXmQfh%5Xr*xhof%y-Em^ac<+7~^=(;>VcWElK*s$s<8FI0#ESZWi ztyfsXb))L3$JMezE_$kleqAY8ld3_ZZrm0SJg;i1bwR+f*lz9Jvwx9g0sf3$B(L2w zs;11^mAqms%K5Uwa5>jy*-&}zE&8o>m69Bq(T!5dMV7i{$knQ1q%O#)(sTNUM8h2I{)09if zq*_u;OTeJ3WGV&QP_5gk+|J*mAIRUfxNzI9rUeL;o)#E2b1sO`GOy$k6@-HP4El_m~V9blWH>yhw$Ef*C-!Y^3om-rPilalaj zo{i%-5`N3l?|<-`fHNPy^4Z6E5xD8=FRB=?b5fyl zO`MBT-9=S1YHK$%{gy+~J7nENK9}dNxoc^`t8ib8TjTHtJjCQ8HnO)$5A5lFX{Ydd zV=b$FuQI1hd2lGLC}8vhoqeywxRY7>b|xoUUI4osExQMadYOySo46Q`wBTTp=q&3p z0TWGmO@CQ3Q~^g@AL=F_(#|>c5KEs}$YitIIFJ0FCgnDetaDEm2;k}c>Daf+g}7C? zjm{q%;Z_&4t3}x&cY)Z|G?Nf4deMThth;hBmTkFR@m3wbxw5!!=(o5vI^1^9%YeWa zm5sSIcG&_u@zHMD`R)FCD3)y6U~o4 zJdCpt@G+XTAwlzx@0gDw!rhPL2sc3biu8|~42_S{Y=v}u^zDwKUEN8&(jB&U(_!n}(B1qSZK6E*nj2;}0) zI)8$*@zF#b;yM2oLM!~My^in}I#%kCXx3RnSEQSUK0ggL^wjadxxlt=WS8!NUAm5x zY#If((7VzX=nK|yaI=wHKY}z4Q(iGbJ%YnTYKAD>K+?%^+Qr<+@eU?2MH#izoAq%b zxs9xBTqMaywiVJpOFU&rJ4*}%$d80eB!2}-ldc($3zKHd>2RC?`tRXT4TtM^aJHF? z3xCvw--H{cFYpirJ?+4YyKWlrh8-w^BQa2h_aJ5*cx`-#cmQ4}JGLB)^xZ>$jshN; zO;WUhEex*s3DnU#j`f_ZA-b8{!q7_O1nt$y`;O=1^n^Z6{+jeXM&krM@YCp_)PIL4 z@(GH~_#P$-f*8OYE>rvtqUckYC)*PwFJRHhW~_mJ3`-9BWs-vs@*>4)<15-jeVr`1 zwQS16Jf z_dn>QCltRAytvPiCHw3ro?^KqZ-33H3xgCqxtSdFU#nrH8T}At49YP;`AL*v59Ji0 z9Gbh;-$2lhr|}HM2;d-Aonn&ca4{C2gQXq9e-ROJjp5K+#DnuZxnbhYCn5wW`3geu zH_^74h>SL7zD?dWubd)dR7OrsrM8d5L-+RpzDmKKqTo-{7Cl27cFh5N$R3T;0DK;W z#s(3Fu1@-2bc$2KN1gJdYmw4CgZBRcv$4z`1r0!7MVMs+003DD0Bv1oI9yv7X7oNr zi&3IS^ftOEi5k&`3?anmT^NZnL^t|TBHCagL`y^qA$lizZuAl@2$RGmL40$4x%WQ4 z=R4=eIcx2At-b&3=Q(@tv)-3L6kl}94E%|Mq7u_ROefU9y@wJh^`y?>nUn(&7*UeA zq9r17&|0BMTVa^=CN+bzNO+oru8?%7fbC`ihg0w}+5UBfF9PZVK0d*(gWk-aeGzZS zI{A6JdWAs0O^bR(Lb%hK*haIEV;x}`+r}gM%^|Z-Wbma%Xoi0Hkeig76eGgY36oCK zi>gbEc;5K+JK>Q7_STl40~dddmvl|&rL@EGfZBL(x}icnuArP zOjg1c{s(i@BO?#2Lbi`J2T$&qxz=ml#nH?PH|4zZwZ!d^qpWvn3)1^+hPkH=s?t%= zyDV!}`R>no^$Z-VH{$hBS6JwHeq9Igvdy6) zeca_&BmGo(5LDlVR0L;enh8sLltx!icr^#U7-w7dzxWvQvqs(T9Y6God>$5r#3Z+e zEoqD@4Jjps##{9EysCB|-ay|xR(kfV@^otWfS*LMkjjpD{P9*JoXHL@9?dJ)PT_rw`oLGs(}<4by9i0709tX6c-;@Z+{iK*}?UTaOH?D zPL095%bcO*@dglXNYbjbztzTz-k>K70bR;tn+Xk8Y!8VJq_h)0HDdgxXU15o7ZX`lAaz+QK z-)B<$?7^XZ9 z*a*+0KRAx8pIqHnLI{)^m|{Gc5EVAMUy6GIzOk-u#@$CG89NlAtThaPQyd7S#E4y1 z)$=LU%_Mc$=%f;#W`k4A2vA>*$RW$>>ycc^U0n2>4)m}e;FK=}4jSZ;HTBzgZ#S1Q zrvnYF8=Ufh;485J374FzA6Je>%2jq&x^c9vG@XgYZ~!^^sd_0+I#7(jI54F_BZWmi zgfO-v;_da}V=(w#lv)oL4tMVxsvLpCU>aqy z7O{;J$rgHo9pyLP!Zj#RNjhL}7E>GEmAcTk23_0y>^*FJ>C1@_=B3zJIbF+GUIgDm zIn=^XLGfE0=dZU>$VK7h%0RZkmic7l{*l4@eD7=I51c3GVrRkOPoIR|L&@V)<>Ro+ zmp|b`e+Bm?(|tRl|HXc|N}PNd@i7^f@YgXz=3@#o?it zBR_Z-D@D$}u7IkD9i(7Ir66;k{2K4FaW2C4z3!0+=jz7|zFI zp3K|_B}MsBl^NC14~sA`2Qzqcp?t?;2BPO%Bx(6M0Cp z15Jz$YWhi9EOvX>Cx~S_de~@XZ+cgX zz4P0E*qw&rEPL$$`LI)xq)csn{`zWdU4>)423OtTIe~kK@Qj5*Hz8mcQ+V2w7g8D0krlL8 zOj#!cyy$WK^tL6XpWJVvk0?p}G+?43GnV*$aOS&4*=ED_?cow-RyQ;rUJ=H;!JX+6 z8P{23C3aorq!+oxu@WqHJj?ytqyB*YZ4(vB zdfX%-Lqumh5BFRsqdqdvmKmnLrxFYvu`~W!?VRhCyTLtZpv$>qehE?B+d8NjU%sj* z;8R#U704Bp(~;h5=e4dgK`yz4nm~M%SaFHO{}n%EL>EgX`0;&o?2!bN90h zxj)zD5X`V>@3B~t{~#N7h4*o3!nN;%fwZI!r6_kdY9H3ccBE#oVGsT|G2z=0p-VzD zR4pd$HsU0OpKe8fRn@-kA>=e(;p%Fy2#&$=c5`;BZj{&?9Y?($LyrV2#7RQ;r|WPb z4eg>CXz0kC?I%h1C0nUgi=k2stxP4`aS@}T%5|S3SZHUg+~ARDsI~?Fvs7ja{i*r3 zQk2KQkqW0%yOqNUAqpG1H1>4MTJb=i$Fn1J%FOVv2@?eWmI)+B&t`(fq!3^@Zg;l29oI8aZrMJc{HXqJCze)>g=;?*so zjnMK-uH&_C*O0}-Dvq*EpZkP7MRgOE%J{_0Ox5*_eReH&-7o@<@2U8RD_WiXP`N=H zjMf!Y6HyIbi2JS7$EN1q+J1!euw7lzl(nZ%#obJ^82i>;vn;pJHGDI$qG#^@CNnKB z6=WMbU$JC#Z4C=M3e#^kmFd6VID&TW1aNpjzgBtMmx#B2hb2j+01SunTi z{!nNS34c7c`2%Yomu9Kq^kG}<7Yc(h>e5+|ozOcP43QfigjavrJ0Re09#<}QdcNW3 zmS0>nW&5+|O-V<7-E9SJD$_Fkmk^YmDX_YGf z*y)xke_>E?%eZozTm@`A@8*4y(@2gkuSK!2taMQ;x(W1`GqGZ)acl)%Dh=T_UYGx1<{ls8=W?^L6Ov? zFC^UPo30rQfNA5r{RTytV>r1Cn5S-(XLmD6TiP3g>Xe5tVrZu!%tDE1or?v$)@h~| zbE`QXROe1=G22C&(>MpQwwt&;Q>%rZc9_tRtyDl~vR2f%RLWMOMA1{yfzy(c2XN!& zo+P-mwud>h+xuKDWJDmb)2p7i4>S)d+F+kfC`G#9BJ~gt6|z1n28iQ1ObX;H4KDYDRlzVMr= z!qyT#G}US~H+o21s`kRAqWD$*j^nQSV9_&!KXC~yPT1~C&r$Bk?!u?5ZR%jjexFgj zS6PAA(iXJOzE}D3)I@9j+3!_wTo(na?_FzH#9669nqI#f{%G5AOeL56MmFn{>~rs8 zhH_#BvyM|t_jIerYcF%ZN-xq6d41d;dnF7c^VRqjI%C~-@4ks7Z&RBYhhQcLMh)_J zdu4Vr`gVvIC2Ub*1a7g0UdFxQl{bWIK%*i@rSX&@e>k~Ryh8XwPXkk@mM?7ykhzd? zGtKIV+UPWGgEx3_JKZwEH4W#gX)paoqQT({tTTh=V8HVFRiI#9 zfbD`f?cY)OCpLT;SX$R3K9{=`+h7KbDWAu9ZE&-n3tr+otH;+$NneN=xpoc`yG9Kl zSHbN6iV6}Cs9XT{tN#Z6B{K*H^f$pI=NfK+-6j*L>Bjkxb2hn&&xN|$Hkm=R+ULIG zO)>ThB3&1<>geG?Jb1k>Qov(N2*i39;IrPE5gg14j*qb^`)!f~`+Gd>{}zl85O7_HC5a~bd&&})BL{{a~b{e%Dj delta 36122 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 Ac-xW1# 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 Q>rd#Eb%7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a21c6ebe2..d11cdd907 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb4..0adc8e1a5 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/settings.gradle b/settings.gradle index 885070a6d..039a49da2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,14 @@ rootProject.name = name +dependencyResolutionManagement { + versionCatalogs { + testLibs { + from(files("gradle/test.libs.versions.toml")) + } + runtimeLibs { + from(files("gradle/runtime.libs.versions.toml")) + } + } +} + include "bootstrap" \ No newline at end of file diff --git a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java index 8cc57928f..f90f7e494 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java @@ -47,6 +47,7 @@ import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.task.PrepareJarRemapTask; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.LoomVersions; /** * Normally javac invokes annotation processors, but when the scala or kapt plugin are installed they will want to invoke @@ -147,7 +148,7 @@ public void configureMixin() { // Add Mixin and mixin extensions (fabric-mixin-compile-extensions pulls mixin itself too) project.getDependencies().add(processorConfig.getName(), - Constants.Dependencies.MIXIN_COMPILE_EXTENSIONS + Constants.Dependencies.Versions.MIXIN_COMPILE_EXTENSIONS); + LoomVersions.MIXIN_COMPILE_EXTENSIONS.mavenNotation()); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java index d647c77a9..9d7008f35 100644 --- a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java +++ b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java @@ -35,6 +35,7 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.LoomVersions; import net.fabricmc.loom.util.gradle.SourceSetHelper; public abstract class LoomConfigurations implements Runnable { @@ -104,10 +105,10 @@ public void run() { extendsFrom(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME, Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES); // Add the dev time dependencies - getDependencies().add(Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES, Constants.Dependencies.DEV_LAUNCH_INJECTOR + Constants.Dependencies.Versions.DEV_LAUNCH_INJECTOR); - getDependencies().add(Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES, Constants.Dependencies.TERMINAL_CONSOLE_APPENDER + Constants.Dependencies.Versions.TERMINAL_CONSOLE_APPENDER); - getDependencies().add(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS); - getDependencies().add(JavaPlugin.TEST_COMPILE_ONLY_CONFIGURATION_NAME, Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS); + getDependencies().add(Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES, LoomVersions.DEV_LAUNCH_INJECTOR.mavenNotation()); + getDependencies().add(Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES, LoomVersions.TERMINAL_CONSOLE_APPENDER.mavenNotation()); + getDependencies().add(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()); + getDependencies().add(JavaPlugin.TEST_COMPILE_ONLY_CONFIGURATION_NAME, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()); } private NamedDomainObjectProvider register(String name, Role role) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/library/processors/LoomNativeSupportLibraryProcessor.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/library/processors/LoomNativeSupportLibraryProcessor.java index 89f85f82b..4baf86a4e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/library/processors/LoomNativeSupportLibraryProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/library/processors/LoomNativeSupportLibraryProcessor.java @@ -30,7 +30,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.library.Library; import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryContext; import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessor; -import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.LoomVersions; import net.fabricmc.loom.util.Platform; public class LoomNativeSupportLibraryProcessor extends LibraryProcessor { @@ -56,7 +56,7 @@ public ApplicationResult getApplicationResult() { @Override public Predicate apply(Consumer dependencyConsumer) { - dependencyConsumer.accept(Library.fromMaven(Constants.Dependencies.NATIVE_SUPPORT + Constants.Dependencies.Versions.NATIVE_SUPPORT_VERSION, Library.Target.LOCAL_MOD)); + dependencyConsumer.accept(Library.fromMaven(LoomVersions.NATIVE_SUPPORT.mavenNotation(), Library.Target.LOCAL_MOD)); return ALLOW_ALL; } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java index adf8650d4..70cecf6b1 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java +++ b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2018-2020 FabricMC + * Copyright (c) 2018-2023 FabricMC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,12 +26,15 @@ import javax.inject.Inject; +import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.decompilers.cfr.LoomCFRDecompiler; import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler; +import net.fabricmc.loom.util.LoomVersions; public abstract class DecompilerConfiguration implements Runnable { @Inject @@ -39,11 +42,24 @@ public abstract class DecompilerConfiguration implements Runnable { @Override public void run() { - registerDecompiler(getProject(), "fernFlower", FabricFernFlowerDecompiler.class); - registerDecompiler(getProject(), "cfr", LoomCFRDecompiler.class); + var fernflowerConfiguration = createConfiguration("fernflower", LoomVersions.FERNFLOWER); + var cfrConfiguration = createConfiguration("cfr", LoomVersions.CFR); + + registerDecompiler(getProject(), "fernFlower", FabricFernFlowerDecompiler.class, fernflowerConfiguration); + registerDecompiler(getProject(), "cfr", LoomCFRDecompiler.class, cfrConfiguration); + } + + private NamedDomainObjectProvider createConfiguration(String name, LoomVersions version) { + final String configurationName = name + "DecompilerClasspath"; + NamedDomainObjectProvider configuration = getProject().getConfigurations().register(configurationName); + getProject().getDependencies().add(configurationName, version.mavenNotation()); + return configuration; } - private void registerDecompiler(Project project, String name, Class decompilerClass) { - LoomGradleExtension.get(project).getDecompilerOptions().register(name, options -> options.getDecompilerClassName().set(decompilerClass.getName())); + private void registerDecompiler(Project project, String name, Class decompilerClass, NamedDomainObjectProvider configuration) { + LoomGradleExtension.get(project).getDecompilerOptions().register(name, options -> { + options.getDecompilerClassName().set(decompilerClass.getName()); + options.getClasspath().from(configuration); + }); } } diff --git a/src/main/java/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataWrapper.java b/src/main/java/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataWrapper.java new file mode 100644 index 000000000..2a6238a97 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataWrapper.java @@ -0,0 +1,37 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.kotlin.remapping; + +import kotlin.Metadata; +import kotlinx.metadata.jvm.KotlinClassMetadata; + +/** + * Similar story to JvmExtensionWrapper, lets abuse the fact that Java can call "internal" Kotlin APIs without reflection :). + */ +public record KotlinClassMetadataWrapper(KotlinClassMetadata metadata) { + public Metadata getAnnotationData() { + return metadata.getAnnotationData$kotlinx_metadata_jvm(); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java b/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java index f1c3d9b24..4ca2791c0 100644 --- a/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java +++ b/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java @@ -42,6 +42,7 @@ import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.configuration.InstallerData; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.LoomVersions; import net.fabricmc.tinyremapper.TinyRemapper; public abstract class JarManifestService implements BuildService { @@ -63,7 +64,7 @@ public static synchronized Provider get(Project project) { params.getGradleVersion().set(GradleVersion.current().getVersion()); params.getLoomVersion().set(LoomGradlePlugin.LOOM_VERSION); - params.getMCEVersion().set(Constants.Dependencies.Versions.MIXIN_COMPILE_EXTENSIONS); + params.getMCEVersion().set(LoomVersions.MIXIN_COMPILE_EXTENSIONS.version()); params.getMinecraftVersion().set(project.provider(() -> extension.getMinecraftProvider().minecraftVersion())); params.getTinyRemapperVersion().set(tinyRemapperVersion.orElse("unknown")); params.getFabricLoaderVersion().set(project.provider(() -> Optional.ofNullable(extension.getInstallerData()).map(InstallerData::version).orElse("unknown"))); diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index eebfeee5b..42692aabc 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -84,34 +84,6 @@ private Configurations() { } } - /** - * Constants related to dependencies. - */ - public static final class Dependencies { - public static final String MIXIN_COMPILE_EXTENSIONS = "net.fabricmc:fabric-mixin-compile-extensions:"; - public static final String DEV_LAUNCH_INJECTOR = "net.fabricmc:dev-launch-injector:"; - public static final String TERMINAL_CONSOLE_APPENDER = "net.minecrell:terminalconsoleappender:"; - public static final String JETBRAINS_ANNOTATIONS = "org.jetbrains:annotations:"; - public static final String NATIVE_SUPPORT = "net.fabricmc:fabric-loom-native-support:"; - - private Dependencies() { - } - - /** - * Constants for versions of dependencies. - */ - public static final class Versions { - public static final String MIXIN_COMPILE_EXTENSIONS = "0.6.0"; - public static final String DEV_LAUNCH_INJECTOR = "0.2.1+build.8"; - public static final String TERMINAL_CONSOLE_APPENDER = "1.2.0"; - public static final String JETBRAINS_ANNOTATIONS = "24.0.1"; - public static final String NATIVE_SUPPORT_VERSION = "1.0.1"; - - private Versions() { - } - } - } - public static final class MixinArguments { public static final String IN_MAP_FILE_NAMED_INTERMEDIARY = "inMapFileNamedIntermediary"; public static final String OUT_MAP_FILE_NAMED_INTERMEDIARY = "outMapFileNamedIntermediary"; diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index 17fa8a52e..1f4fca35e 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -192,7 +192,7 @@ private Mercury getMercuryInstance() { } Set files = project.getConfigurations() - .detachedConfiguration(project.getDependencies().create(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS)) + .detachedConfiguration(project.getDependencies().create(LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation())) .resolve(); for (File file : files) { diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataRemappingAnnotationVisitor.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataRemappingAnnotationVisitor.kt index c20820fa3..56ce489e6 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataRemappingAnnotationVisitor.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassMetadataRemappingAnnotationVisitor.kt @@ -58,18 +58,18 @@ class KotlinClassMetadataRemappingAnnotationVisitor(private val remapper: Remapp when (val metadata = KotlinClassMetadata.read(header)) { is KotlinClassMetadata.Class -> { - var klass = metadata.toKmClass() + var klass = metadata.kmClass klass = KotlinClassRemapper(remapper).remap(klass) - val remapped = KotlinClassMetadata.writeClass(klass, header.metadataVersion, header.extraInt).annotationData + val remapped = KotlinClassMetadata.writeClass(klass, header.metadataVersion, header.extraInt) writeClassHeader(remapped) validateKotlinClassHeader(remapped, header) } is KotlinClassMetadata.SyntheticClass -> { - var klambda = metadata.toKmLambda() + var klambda = metadata.kmLambda if (klambda != null) { klambda = KotlinClassRemapper(remapper).remap(klambda) - val remapped = KotlinClassMetadata.writeLambda(klambda, header.metadataVersion, header.extraInt).annotationData + val remapped = KotlinClassMetadata.writeLambda(klambda, header.metadataVersion, header.extraInt) writeClassHeader(remapped) validateKotlinClassHeader(remapped, header) } else { @@ -77,20 +77,21 @@ class KotlinClassMetadataRemappingAnnotationVisitor(private val remapper: Remapp } } is KotlinClassMetadata.FileFacade -> { - var kpackage = metadata.toKmPackage() + var kpackage = metadata.kmPackage kpackage = KotlinClassRemapper(remapper).remap(kpackage) - val remapped = KotlinClassMetadata.writeFileFacade(kpackage, header.metadataVersion, header.extraInt).annotationData + val remapped = KotlinClassMetadata.writeFileFacade(kpackage, header.metadataVersion, header.extraInt) writeClassHeader(remapped) validateKotlinClassHeader(remapped, header) } is KotlinClassMetadata.MultiFileClassPart -> { - var kpackage = metadata.toKmPackage() + var kpackage = metadata.kmPackage kpackage = KotlinClassRemapper(remapper).remap(kpackage) - val remapped = KotlinClassMetadata.writeMultiFileClassPart(kpackage, metadata.facadeClassName, metadata.annotationData.metadataVersion, metadata.annotationData.extraInt).annotationData + val wrapper = KotlinClassMetadataWrapper(metadata) + val remapped = KotlinClassMetadata.writeMultiFileClassPart(kpackage, metadata.facadeClassName, wrapper.annotationData.metadataVersion, wrapper.annotationData.extraInt) writeClassHeader(remapped) validateKotlinClassHeader(remapped, header) } - is KotlinClassMetadata.MultiFileClassFacade, is KotlinClassMetadata.Unknown, null -> { + is KotlinClassMetadata.MultiFileClassFacade, is KotlinClassMetadata.Unknown -> { // do nothing accept(next) } diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt index 416a0bde1..e3d00bb61 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt @@ -49,10 +49,10 @@ import kotlinx.metadata.internal.extensions.KmTypeAliasExtension import kotlinx.metadata.internal.extensions.KmTypeExtension import kotlinx.metadata.internal.extensions.KmTypeParameterExtension import kotlinx.metadata.internal.extensions.KmValueParameterExtension -import kotlinx.metadata.isLocal +import kotlinx.metadata.isLocalClassName import kotlinx.metadata.jvm.JvmFieldSignature import kotlinx.metadata.jvm.JvmMethodSignature -import kotlinx.metadata.jvm.jvmInternalName +import kotlinx.metadata.jvm.toJvmInternalName import org.objectweb.asm.commons.Remapper @OptIn(ExperimentalContextReceivers::class) @@ -86,8 +86,8 @@ class KotlinClassRemapper(private val remapper: Remapper) { } private fun remap(name: ClassName): ClassName { - val local = name.isLocal - val remapped = remapper.map(name.jvmInternalName).replace('$', '.') + val local = name.isLocalClassName() + val remapped = remapper.map(name.toJvmInternalName()).replace('$', '.') if (local) { return ".$remapped" @@ -241,10 +241,10 @@ class KotlinClassRemapper(private val remapper: Remapper) { } private fun remap(signature: JvmMethodSignature): JvmMethodSignature { - return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.desc)) + return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor)) } private fun remap(signature: JvmFieldSignature): JvmFieldSignature { - return JvmFieldSignature(signature.name, remapper.mapDesc(signature.desc)) + return JvmFieldSignature(signature.name, remapper.mapDesc(signature.descriptor)) } } diff --git a/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy b/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy index 9aedb1bef..6e128956d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy @@ -27,7 +27,7 @@ package net.fabricmc.loom.test import org.gradle.util.GradleVersion class LoomTestConstants { - private final static String NIGHTLY_VERSION = "8.3-20230702222859+0000" + private final static String NIGHTLY_VERSION = LoomTestVersions.GRADLE_NIGHTLY.version() private final static boolean NIGHTLY_EXISTS = nightlyExists(NIGHTLY_VERSION) // Test against the version of Gradle being used to build loom diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy index 0b20f0a69..f46cc9900 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy @@ -45,7 +45,7 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { setup: def gradle = gradleProject( repo: "https://github.com/FabricMC/fabric.git", - commit: "1ac061308b9d70fa6aad5db3dcc5580cb6ac71cb", + commit: "f091af96c53963fadf9dbc391c67bb40e5678a96", version: version, patch: "fabric_api" ) @@ -55,7 +55,7 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { // Set the version to something constant gradle.buildGradle.text = gradle.buildGradle.text.replace('project.version + "+" + (ENV.GITHUB_RUN_NUMBER ? "" : "local-") + getBranch()', "\"$API_VERSION\"") - def server = ServerRunner.create(gradle.projectDir, "1.20.1") + def server = ServerRunner.create(gradle.projectDir, "23w33a") .withMod(gradle.getOutputFile("fabric-api-${API_VERSION}.jar")) when: def result = gradle.run(tasks: [ @@ -77,8 +77,8 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { result.task(":build").outcome == SUCCESS result.task(":prepareRemapJar").outcome == SUCCESS - new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/13.0.10/fabric-biome-api-v1-13.0.10.jar").exists() - new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/13.0.10/fabric-biome-api-v1-13.0.10-sources.jar").exists() + new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/13.0.11/fabric-biome-api-v1-13.0.11.jar").exists() + new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/13.0.11/fabric-biome-api-v1-13.0.11-sources.jar").exists() serverResult.successful() serverResult.output.contains("- fabric-api $API_VERSION") diff --git a/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy b/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy index 2cc9d6f58..805c2094d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy @@ -28,11 +28,12 @@ import java.util.concurrent.TimeUnit import groovy.transform.Immutable +import net.fabricmc.loom.test.LoomTestVersions import net.fabricmc.loom.util.download.Download class ServerRunner { - static final String LOADER_VERSION = "0.14.21" - static final String INSTALLER_VERSION = "0.11.1" + static final String LOADER_VERSION = LoomTestVersions.FABRIC_LOADER.version() + static final String INSTALLER_VERSION = LoomTestVersions.FABRIC_INSTALLER.version() static final Map FABRIC_API_URLS = [ "1.16.5": "https://github.com/FabricMC/fabric/releases/download/0.37.1%2B1.16/fabric-api-0.37.1+1.16.jar", "1.17.1": "https://github.com/FabricMC/fabric/releases/download/0.37.1%2B1.17/fabric-api-0.37.1+1.17.jar" From 777a05457c9849541e3f78a95932f32ab8cdf31b Mon Sep 17 00:00:00 2001 From: modmuss Date: Sat, 9 Sep 2023 13:18:14 +0100 Subject: [PATCH 03/12] Filter out the authentication error when starting in development (#948) --- src/main/resources/log4j2.fabric.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/log4j2.fabric.xml b/src/main/resources/log4j2.fabric.xml index 69c9c0da4..cb8c84e22 100644 --- a/src/main/resources/log4j2.fabric.xml +++ b/src/main/resources/log4j2.fabric.xml @@ -4,6 +4,8 @@ + + From 0dc2d673174ba0dd3e24461209dd8020fb12cc4f Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sat, 9 Sep 2023 06:21:14 -0600 Subject: [PATCH 04/12] Add extension option to configure known INVOKEDYNAMIC BSMs (#945) * Set up configuration for known indy bsms * Make ignoredIndyBsms apply to service remapper * Add groovy indy instruction to default known BSMs * Address feedback --------- Co-authored-by: modmuss --- .../fabricmc/loom/api/LoomGradleExtensionAPI.java | 3 +++ .../loom/configuration/mods/ModProcessor.java | 1 + .../loom/extension/LoomGradleExtensionApiImpl.java | 14 ++++++++++++++ .../loom/task/service/TinyRemapperService.java | 9 ++++++--- .../net/fabricmc/loom/util/TinyRemapperHelper.java | 1 + 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 45587d963..78584e110 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -35,6 +35,7 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; import org.gradle.api.publish.maven.MavenPublication; import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.ApiStatus; @@ -129,6 +130,8 @@ default void interfaceInjection(Action action) { Property getCustomMinecraftManifest(); + SetProperty getKnownIndyBsms(); + /** * Disables the deprecated POM generation for a publication. * This is useful if you want to suppress deprecation warnings when you're not using software components. diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java index f302600a4..3ed36eef9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -133,6 +133,7 @@ private void remapJars(List remapList) throws IOException { .stream().map(File::toPath).toArray(Path[]::new); TinyRemapper.Builder builder = TinyRemapper.newRemapper() + .withKnownIndyBsm(extension.getKnownIndyBsms().get()) .withMappings(TinyRemapperHelper.create(mappingConfiguration.getMappingsService(serviceManager).getMappingTree(), fromM, toM, false)) .renameInvalidLocals(false) .extraAnalyzeVisitor(AccessWidenerAnalyzeVisitorProvider.createFromMods(fromM, remapList)); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index 3060d0ef1..f8d492ed1 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -25,6 +25,7 @@ package net.fabricmc.loom.extension; import java.io.File; +import java.util.Set; import org.gradle.api.Action; import org.gradle.api.NamedDomainObjectContainer; @@ -35,6 +36,7 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; import org.gradle.api.publish.maven.MavenPublication; import org.gradle.api.tasks.SourceSet; @@ -71,6 +73,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final ConfigurableFileCollection log4jConfigs; protected final RegularFileProperty accessWidener; protected final Property customManifest; + protected final SetProperty knownIndyBsms; protected final Property transitiveAccessWideners; protected final Property modProvidedJavadoc; protected final Property intermediary; @@ -98,6 +101,12 @@ protected LoomGradleExtensionApiImpl(Project project, LoomFiles directories) { this.log4jConfigs = project.files(directories.getDefaultLog4jConfigFile()); this.accessWidener = project.getObjects().fileProperty(); this.customManifest = project.getObjects().property(String.class); + this.knownIndyBsms = project.getObjects().setProperty(String.class).convention(Set.of( + "java/lang/invoke/StringConcatFactory", + "java/lang/runtime/ObjectMethods", + "org/codehaus/groovy/vmplugin/v8/IndyInterface" + )); + this.knownIndyBsms.finalizeValueOnRead(); this.transitiveAccessWideners = project.getObjects().property(Boolean.class) .convention(true); this.transitiveAccessWideners.finalizeValueOnRead(); @@ -228,6 +237,11 @@ public Property getCustomMinecraftManifest() { return customManifest; } + @Override + public SetProperty getKnownIndyBsms() { + return knownIndyBsms; + } + @Override public String getModVersion() { return versionParser.getModVersion(); diff --git a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java index 38f7ad7d1..3e1eee877 100644 --- a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java +++ b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.StringJoiner; import org.gradle.api.Project; @@ -77,6 +78,8 @@ public static synchronized TinyRemapperService getOrCreate(SharedServiceManager joiner.add(project.getPath()); } + extension.getKnownIndyBsms().get().stream().sorted().forEach(joiner::add); + final String id = joiner.toString(); TinyRemapperService service = serviceManager.getOrCreateService(id, () -> { @@ -87,7 +90,7 @@ public static synchronized TinyRemapperService getOrCreate(SharedServiceManager mappings.add(gradleMixinMappingProvider(serviceManager, project.getGradle(), extension.getMappingConfiguration().mappingsIdentifier, from, to)); } - return new TinyRemapperService(mappings, !legacyMixin, kotlinClasspathService); + return new TinyRemapperService(mappings, !legacyMixin, kotlinClasspathService, extension.getKnownIndyBsms().get()); }); service.readClasspath(remapJarTask.getClasspath().getFiles().stream().map(File::toPath).filter(Files::exists).toList()); @@ -126,8 +129,8 @@ private static IMappingProvider gradleMixinMappingProvider(SharedServiceManager // Set to true once remapping has started, once set no inputs can be read. private boolean isRemapping = false; - public TinyRemapperService(List mappings, boolean useMixinExtension, @Nullable KotlinClasspath kotlinClasspath) { - TinyRemapper.Builder builder = TinyRemapper.newRemapper(); + public TinyRemapperService(List mappings, boolean useMixinExtension, @Nullable KotlinClasspath kotlinClasspath, Set knownIndyBsms) { + TinyRemapper.Builder builder = TinyRemapper.newRemapper().withKnownIndyBsm(knownIndyBsms); for (IMappingProvider provider : mappings) { builder.withMappings(provider); diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java index 0b862108e..648ba105a 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java @@ -82,6 +82,7 @@ public static TinyRemapper getTinyRemapper(Project project, SharedServiceManager .rebuildSourceFilenames(true) .invalidLvNamePattern(MC_LV_PATTERN) .inferNameFromSameLvIndex(true) + .withKnownIndyBsm(extension.getKnownIndyBsms().get()) .extraPreApplyVisitor((cls, next) -> { if (fixRecords && !cls.isRecord() && "java/lang/Record".equals(cls.getSuperName())) { return new RecordComponentFixVisitor(next, mappingTree, intermediaryNsId); From ae159dd1266c7e178b9be99a4f248175b1af99dd Mon Sep 17 00:00:00 2001 From: modmuss Date: Sat, 9 Sep 2023 13:21:43 +0100 Subject: [PATCH 05/12] Support Zip.getEntryCompression() setting (#936) * Support Zip.getEntryCompression() setting * Checkstyle * Set for both the entry and ZOS --- .../loom/task/AbstractRemapJarTask.java | 8 ++++-- .../loom/util/ZipReprocessorUtil.java | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index 3abc7c0a4..e5a0d9d07 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -53,6 +53,7 @@ import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.bundling.ZipEntryCompression; import org.gradle.build.event.BuildEventsListenerRegistry; import org.gradle.jvm.tasks.Jar; import org.gradle.workers.WorkAction; @@ -137,6 +138,7 @@ public final

void submitWork(Class clientOnlyEntries = new ArrayList<>(getClientOnlyEntries(getClientSourceSet())); @@ -160,6 +162,7 @@ public interface AbstractRemapParams extends WorkParameters { Property getArchivePreserveFileTimestamps(); Property getArchiveReproducibleFileOrder(); + Property getEntryCompression(); Property getJarManifestService(); MapProperty getManifestAttributes(); @@ -202,9 +205,10 @@ protected void modifyJarManifest() throws IOException { protected void rewriteJar() throws IOException { final boolean isReproducibleFileOrder = getParameters().getArchiveReproducibleFileOrder().get(); final boolean isPreserveFileTimestamps = getParameters().getArchivePreserveFileTimestamps().get(); + final ZipEntryCompression compression = getParameters().getEntryCompression().get(); - if (isReproducibleFileOrder || !isPreserveFileTimestamps) { - ZipReprocessorUtil.reprocessZip(outputFile.toFile(), isReproducibleFileOrder, isPreserveFileTimestamps); + if (isReproducibleFileOrder || !isPreserveFileTimestamps || compression != ZipEntryCompression.DEFLATED) { + ZipReprocessorUtil.reprocessZip(outputFile.toFile(), isReproducibleFileOrder, isPreserveFileTimestamps, compression); } } } diff --git a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java index f1c8c3645..a3b7797a6 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java @@ -38,6 +38,9 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; +import org.gradle.api.tasks.bundling.ZipEntryCompression; +import org.intellij.lang.annotations.MagicConstant; + public class ZipReprocessorUtil { /** * See {@link org.gradle.api.internal.file.archive.ZipCopyAction} about this. @@ -92,6 +95,10 @@ private static int specialOrdering(String name1, String name2) { } public static void reprocessZip(File file, boolean reproducibleFileOrder, boolean preserveFileTimestamps) throws IOException { + reprocessZip(file, reproducibleFileOrder, preserveFileTimestamps, ZipEntryCompression.DEFLATED); + } + + public static void reprocessZip(File file, boolean reproducibleFileOrder, boolean preserveFileTimestamps, ZipEntryCompression zipEntryCompression) throws IOException { if (!reproducibleFileOrder && preserveFileTimestamps) { return; } @@ -111,6 +118,8 @@ public static void reprocessZip(File file, boolean reproducibleFileOrder, boolea final var outZip = new ByteArrayOutputStream(entries.length); try (var zipOutputStream = new ZipOutputStream(outZip)) { + zipOutputStream.setMethod(zipOutputStreamCompressionMethod(zipEntryCompression)); + for (ZipEntry entry : entries) { ZipEntry newEntry = entry; @@ -119,6 +128,7 @@ public static void reprocessZip(File file, boolean reproducibleFileOrder, boolea setConstantFileTime(newEntry); } + newEntry.setMethod(zipEntryCompressionMethod(zipEntryCompression)); copyZipEntry(zipOutputStream, newEntry, zipFile.getInputStream(entry)); } } @@ -177,4 +187,20 @@ private static void setConstantFileTime(ZipEntry entry) { entry.setLastModifiedTime(FileTime.fromMillis(ZipReprocessorUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES)); entry.setLastAccessTime(FileTime.fromMillis(ZipReprocessorUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES)); } + + @MagicConstant(valuesFromClass = ZipOutputStream.class) + private static int zipOutputStreamCompressionMethod(ZipEntryCompression compression) { + return switch (compression) { + case STORED -> ZipOutputStream.STORED; + case DEFLATED -> ZipOutputStream.DEFLATED; + }; + } + + @MagicConstant(valuesFromClass = ZipEntry.class) + private static int zipEntryCompressionMethod(ZipEntryCompression compression) { + return switch (compression) { + case STORED -> ZipEntry.STORED; + case DEFLATED -> ZipEntry.DEFLATED; + }; + } } From 4caf4f831efae9df6607ca1f8d4cbf7d86a6e09f Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Sat, 9 Sep 2023 17:31:43 +0100 Subject: [PATCH 06/12] Sort client only entries, fixes #940 --- src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index e5a0d9d07..ca7da77e3 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -143,6 +144,7 @@ public final

void submitWork(Class clientOnlyEntries = new ArrayList<>(getClientOnlyEntries(getClientSourceSet())); clientOnlyEntries.addAll(getAdditionalClientOnlyEntries().get()); + Collections.sort(clientOnlyEntries); applyClientOnlyManifestAttributes(params, clientOnlyEntries); params.getClientOnlyEntries().set(clientOnlyEntries.stream().filter(s -> s.endsWith(".class")).toList()); } From e924faf44e066390146df0bc998fa5c041e609c8 Mon Sep 17 00:00:00 2001 From: modmuss Date: Sat, 9 Sep 2023 22:36:51 +0100 Subject: [PATCH 07/12] Support reading "development" dependencies from the installer json. (#949) * Support reading "development" dependencies from the installer json. * Fix checkstyle and comment. --- .../loom/configuration/InstallerData.java | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/InstallerData.java b/src/main/java/net/fabricmc/loom/configuration/InstallerData.java index 9496320b9..ce8da78d0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/InstallerData.java +++ b/src/main/java/net/fabricmc/loom/configuration/InstallerData.java @@ -24,11 +24,16 @@ package net.fabricmc.loom.configuration; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ExternalModuleDependency; import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.plugins.JavaPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomRepositoryPlugin; @@ -36,6 +41,8 @@ import net.fabricmc.loom.util.Constants; public record InstallerData(String version, JsonObject installerJson) { + private static final Logger LOGGER = LoggerFactory.getLogger(InstallerData.class); + public void applyToProject(Project project) { LoomGradleExtension extension = LoomGradleExtension.get(project); @@ -45,35 +52,61 @@ public void applyToProject(Project project) { extension.setInstallerData(this); - JsonObject libraries = installerJson.get("libraries").getAsJsonObject(); + final JsonObject libraries = installerJson.get("libraries").getAsJsonObject(); + + applyDependendencies(libraries.get("common").getAsJsonArray(), project); + + // Apply development dependencies if they exist. + if (libraries.has("development")) { + applyDependendencies(libraries.get("development").getAsJsonArray(), project); + } + } + + private void applyDependendencies(JsonArray jsonArray, Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); Configuration loaderDepsConfig = project.getConfigurations().getByName(Constants.Configurations.LOADER_DEPENDENCIES); - Configuration apDepsConfig = project.getConfigurations().getByName("annotationProcessor"); + Configuration annotationProcessor = project.getConfigurations().getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME); - libraries.get("common").getAsJsonArray().forEach(jsonElement -> { - String name = jsonElement.getAsJsonObject().get("name").getAsString(); - project.getLogger().debug("Adding dependency ({}) from installer JSON", name); + for (JsonElement jsonElement : jsonArray) { + final JsonObject jsonObject = jsonElement.getAsJsonObject(); + final String name = jsonObject.get("name").getAsString(); + + LOGGER.debug("Adding dependency ({}) from installer JSON", name); ExternalModuleDependency modDep = (ExternalModuleDependency) project.getDependencies().create(name); - modDep.setTransitive(false); + modDep.setTransitive(false); // Match the launcher in not being transitive loaderDepsConfig.getDependencies().add(modDep); - // TODO: work around until https://github.com/FabricMC/Mixin/pull/60 and https://github.com/FabricMC/fabric-mixin-compile-extensions/issues/14 is fixed. + // Work around https://github.com/FabricMC/Mixin/pull/60 and https://github.com/FabricMC/fabric-mixin-compile-extensions/issues/14. if (!IdeaUtils.isIdeaSync() && extension.getMixin().getUseLegacyMixinAp().get()) { - apDepsConfig.getDependencies().add(modDep); + annotationProcessor.getDependencies().add(modDep); } // If user choose to use dependencyResolutionManagement, then they should declare // these repositories manually in the settings file. - if (jsonElement.getAsJsonObject().has("url") && !project.getGradle().getPlugins().hasPlugin(LoomRepositoryPlugin.class)) { - String url = jsonElement.getAsJsonObject().get("url").getAsString(); - long count = project.getRepositories().stream().filter(artifactRepository -> artifactRepository instanceof MavenArtifactRepository) - .map(artifactRepository -> (MavenArtifactRepository) artifactRepository) - .filter(mavenArtifactRepository -> mavenArtifactRepository.getUrl().toString().equalsIgnoreCase(url)).count(); - - if (count == 0) { - project.getRepositories().maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(jsonElement.getAsJsonObject().get("url").getAsString())); - } + if (project.getGradle().getPlugins().hasPlugin(LoomRepositoryPlugin.class)) { + continue; } - }); + + addRepository(jsonObject, project); + } + } + + private void addRepository(JsonObject jsonObject, Project project) { + if (!jsonObject.has("url")) { + return; + } + + final String url = jsonObject.get("url").getAsString(); + final boolean isPresent = project.getRepositories().stream() + .filter(artifactRepository -> artifactRepository instanceof MavenArtifactRepository) + .map(artifactRepository -> (MavenArtifactRepository) artifactRepository) + .anyMatch(mavenArtifactRepository -> mavenArtifactRepository.getUrl().toString().equalsIgnoreCase(url)); + + if (isPresent) { + return; + } + + project.getRepositories().maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(jsonObject.get("url").getAsString())); } } From 0a3779f41dc6eae20cc27fbcec3f7cddee6cb0a5 Mon Sep 17 00:00:00 2001 From: modmuss Date: Sat, 9 Sep 2023 22:37:02 +0100 Subject: [PATCH 08/12] Fix and test FabricApiExtension not supporting deprecated modules. (#950) --- .../net/fabricmc/loom/LoomGradlePlugin.java | 2 +- .../configuration/FabricApiExtension.java | 74 +++++++++++++------ .../fabricmc/loom/util/download/Download.java | 8 +- .../loom/util/download/DownloadBuilder.java | 5 ++ .../loom/util/download/DownloadException.java | 17 +++++ .../test/unit/FabricApiExtensionTest.groovy | 69 +++++++++++++++++ .../unit/download/DownloadFileTest.groovy | 23 +++++- .../unit/download/DownloadStringTest.groovy | 40 +++++++++- .../loom/test/util/GradleTestUtil.groovy | 25 ++++++- 9 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index 9c9c938e2..8231476d4 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -89,7 +89,7 @@ public void apply(Project project) { // Setup extensions project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project)); - project.getExtensions().create("fabricApi", FabricApiExtension.class, project); + project.getExtensions().create("fabricApi", FabricApiExtension.class); for (Class jobClass : SETUP_JOBS) { project.getObjects().newInstance(jobClass).run(); diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java index e043fef98..732bfa0f3 100644 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java @@ -26,9 +26,11 @@ import java.io.File; import java.io.UncheckedIOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import javax.inject.Inject; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -41,25 +43,29 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.util.download.DownloadException; -public class FabricApiExtension { - private final Project project; - - public FabricApiExtension(Project project) { - this.project = project; - } +public abstract class FabricApiExtension { + @Inject + public abstract Project getProject(); private static final HashMap> moduleVersionCache = new HashMap<>(); + private static final HashMap> deprecatedModuleVersionCache = new HashMap<>(); public Dependency module(String moduleName, String fabricApiVersion) { - return project.getDependencies() + return getProject().getDependencies() .create(getDependencyNotation(moduleName, fabricApiVersion)); } public String moduleVersion(String moduleName, String fabricApiVersion) { String moduleVersion = moduleVersionCache - .computeIfAbsent(fabricApiVersion, this::populateModuleVersionMap) + .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions) .get(moduleName); + if (moduleVersion == null) { + moduleVersion = deprecatedModuleVersionCache + .computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions) + .get(moduleName); + } + if (moduleVersion == null) { throw new RuntimeException("Failed to find module version for module: " + moduleName); } @@ -71,9 +77,24 @@ private String getDependencyNotation(String moduleName, String fabricApiVersion) return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); } - private Map populateModuleVersionMap(String fabricApiVersion) { - File pomFile = getApiMavenPom(fabricApiVersion); + private Map getApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion); + } + } + + private Map getDeprecatedApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + // Not all fabric-api versions have deprecated modules, return an empty map to cache this fact. + return Collections.emptyMap(); + } + } + private Map populateModuleVersionMap(File pomFile) { try { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); @@ -101,27 +122,36 @@ private Map populateModuleVersionMap(String fabricApiVersion) { } } - private File getApiMavenPom(String fabricApiVersion) { - LoomGradleExtension extension = LoomGradleExtension.get(project); - - File mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/" + fabricApiVersion + ".pom"); + private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api", fabricApiVersion); + } - if (project.getGradle().getStartParameter().isOffline()) { - if (!mavenPom.exists()) { - throw new RuntimeException("Cannot retrieve fabric-api pom due to being offline"); - } + private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api-deprecated", fabricApiVersion); + } - return mavenPom; - } + private File getPom(String name, String version) throws PomNotFoundException { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version)); try { - extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion)) + extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name)) .defaultCache() .downloadPath(mavenPom.toPath()); } catch (DownloadException e) { - throw new UncheckedIOException("Failed to download maven info for " + fabricApiVersion, e); + if (e.getStatusCode() == 404) { + throw new PomNotFoundException(e); + } + + throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e); } return mavenPom; } + + private static class PomNotFoundException extends Exception { + PomNotFoundException(Throwable cause) { + super(cause); + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/download/Download.java b/src/main/java/net/fabricmc/loom/util/download/Download.java index a21d4ce44..2ba074bfa 100644 --- a/src/main/java/net/fabricmc/loom/util/download/Download.java +++ b/src/main/java/net/fabricmc/loom/util/download/Download.java @@ -129,7 +129,7 @@ String downloadString() throws DownloadException { if (!successful) { progressListener.onEnd(); - throw error("HTTP request to (%s) returned unsuccessful status (%d)", url, statusCode); + throw statusError("HTTP request to (%s) returned unsuccessful status".formatted(url) + "(%d)", statusCode); } try (InputStream inputStream = decodeOutput(response)) { @@ -228,7 +228,7 @@ private void doDownload(Path output) throws DownloadException { } } } else { - throw error("HTTP request returned unsuccessful status (%d)", statusCode); + throw statusError("HTTP request returned unsuccessful status (%d)", statusCode); } if (useEtag) { @@ -430,6 +430,10 @@ private void createLock(Path output) throws DownloadException { } } + private DownloadException statusError(String message, int statusCode) { + return new DownloadException(String.format(Locale.ENGLISH, message, statusCode), statusCode); + } + private DownloadException error(String message, Object... args) { return new DownloadException(String.format(Locale.ENGLISH, message, args)); } diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java index c2fd6679a..ab787bfb1 100644 --- a/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java @@ -158,6 +158,11 @@ private T withRetries(DownloadFunction supplier) throws DownloadException return supplier.get(build(i)); } catch (DownloadException e) { + if (e.getStatusCode() == 404) { + // Don't retry on 404's + throw e; + } + if (i == maxRetries) { throw new DownloadException(String.format(Locale.ENGLISH, "Failed download after %d attempts", maxRetries), e); } diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadException.java b/src/main/java/net/fabricmc/loom/util/download/DownloadException.java index 993fa2573..36db54cc3 100644 --- a/src/main/java/net/fabricmc/loom/util/download/DownloadException.java +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadException.java @@ -27,15 +27,32 @@ import java.io.IOException; public class DownloadException extends IOException { + private final int statusCode; + public DownloadException(String message) { super(message); + statusCode = -1; + } + + public DownloadException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; } public DownloadException(String message, Throwable cause) { super(message, cause); + statusCode = cause instanceof DownloadException downloadException ? downloadException.getStatusCode() : -1; } public DownloadException(Throwable cause) { super(cause); + statusCode = cause instanceof DownloadException downloadException ? downloadException.getStatusCode() : -1; + } + + /** + * @return -1 when the status code is unknown. + */ + public int getStatusCode() { + return statusCode; } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy new file mode 100644 index 000000000..860e65ebd --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy @@ -0,0 +1,69 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit + +import org.gradle.api.Project +import spock.lang.Specification + +import net.fabricmc.loom.configuration.FabricApiExtension +import net.fabricmc.loom.test.util.GradleTestUtil + +class FabricApiExtensionTest extends Specification { + def "get module version"() { + when: + def fabricApi = new FabricApiExtension() { + Project project = GradleTestUtil.mockProject() + } + def version = fabricApi.moduleVersion(moduleName, apiVersion) + + then: + version == expectedVersion + + where: + moduleName | apiVersion | expectedVersion + "fabric-api-base" | "0.88.3+1.20.2" | "0.4.32+fce67b3299" // Normal module, new version + "fabric-api-base" | "0.13.1+build.257-1.14" | "0.1.2+28f8190f42" // Normal module, old version before deprecated modules. + "fabric-networking-v0" | "0.88.0+1.20.1" | "0.3.50+df3654b377" // Deprecated module, opt-out version + "fabric-networking-v0" | "0.85.0+1.20.1" | "0.3.48+df3654b377" // Deprecated module, opt-in version + } + + def "unknown module"() { + when: + def fabricApi = new FabricApiExtension() { + Project project = GradleTestUtil.mockProject() + } + fabricApi.moduleVersion("fabric-api-unknown", apiVersion) + + then: + def e = thrown RuntimeException + e.getMessage() == "Failed to find module version for module: fabric-api-unknown" + + where: + apiVersion | _ + "0.88.0+1.20.1" | _ // Deprecated opt-out + "0.85.0+1.20.1" | _ // Deprecated opt-int + "0.13.1+build.257-1.14" | _ // No deprecated modules + } +} \ No newline at end of file diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy index 820647f93..1d9772035 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy @@ -76,16 +76,33 @@ class DownloadFileTest extends DownloadTest { def "File: Not found"() { setup: server.get("/fileNotfound") { - it.status(404) + it.status(HttpStatus.NOT_FOUND) } def output = new File(File.createTempDir(), "file.txt").toPath() when: - def result = Download.create("$PATH/stringNotFound").downloadPath(output) + def result = Download.create("$PATH/fileNotfound").downloadPath(output) then: - thrown DownloadException + def e = thrown DownloadException + e.statusCode == 404 + } + + def "File: Server error"() { + setup: + server.get("/fileServerError") { + it.status(HttpStatus.INTERNAL_SERVER_ERROR) + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + + when: + def result = Download.create("$PATH/fileServerError").downloadPath(output) + + then: + def e = thrown DownloadException + e.statusCode == 500 } def "Cache: Sha1"() { diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy index f6397a63f..48ed5fce3 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy @@ -46,7 +46,7 @@ class DownloadStringTest extends DownloadTest { def "String: Not found"() { setup: server.get("/stringNotFound") { - it.status(404) + it.status(HttpStatus.NOT_FOUND) } when: @@ -55,7 +55,24 @@ class DownloadStringTest extends DownloadTest { .downloadString() then: - thrown DownloadException + def e = thrown DownloadException + e.statusCode == 404 + } + + def "String: Server error"() { + setup: + server.get("/stringNotFound") { + it.status(HttpStatus.INTERNAL_SERVER_ERROR) + } + + when: + def result = Download.create("$PATH/stringNotFound") + .maxRetries(3) // Ensure we still error as expected when retrying + .downloadString() + + then: + def e = thrown DownloadException + e.statusCode == 500 } def "String: Redirect"() { @@ -97,6 +114,25 @@ class DownloadStringTest extends DownloadTest { result == "Hello World 3" } + def "String: Retries 404"() { + setup: + int requests = 0 + server.get("/retryString") { + requests ++ + it.status(HttpStatus.NOT_FOUND) + } + + when: + def result = Download.create("$PATH/retryString") + .maxRetries(3) + .downloadString() + + then: + def e = thrown DownloadException + e.statusCode == 404 + requests == 1 + } + def "String: File cache"() { setup: server.get("/downloadString2") { diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy index 423201cfd..49448cf00 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy @@ -36,12 +36,16 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.util.PatternFilterable import org.jetbrains.annotations.Nullable +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer import net.fabricmc.loom.LoomGradleExtension +import net.fabricmc.loom.extension.LoomFiles +import net.fabricmc.loom.test.LoomTestConstants +import net.fabricmc.loom.util.download.Download import static org.mockito.ArgumentMatchers.any -import static org.mockito.Mockito.mock -import static org.mockito.Mockito.when +import static org.mockito.Mockito.* class GradleTestUtil { static Property mockProperty(T value) { @@ -73,7 +77,18 @@ class GradleTestUtil { static LoomGradleExtension mockLoomGradleExtension() { def mock = mock(LoomGradleExtension.class) + def loomFiles = mockLoomFiles() when(mock.refreshDeps()).thenReturn(false) + when(mock.getFiles()).thenReturn(loomFiles) + when(mock.download(any())).thenAnswer { + Download.create(it.getArgument(0)) + } + return mock + } + + static LoomFiles mockLoomFiles() { + def mock = mock(LoomFiles.class, new RequiresStubAnswer()) + doReturn(LoomTestConstants.TEST_DIR).when(mock).getUserCache() return mock } @@ -121,4 +136,10 @@ class GradleTestUtil { def mock = mock(RepositoryHandler.class) return mock } + + static class RequiresStubAnswer implements Answer { + Object answer(InvocationOnMock invocation) throws Throwable { + throw new RuntimeException("${invocation.getMethod().getName()} is not stubbed") + } + } } From 71b7bea8548df06cd4b580fcf77c28d363dd2587 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 11 Sep 2023 11:29:01 +0100 Subject: [PATCH 09/12] Support the Vineflower decompiler (#951) --- build.gradle | 76 ++++++- gradle/runtime.libs.versions.toml | 2 + gradle/test.libs.versions.toml | 2 +- .../cfr/CFRObfuscationMapping.java | 3 +- .../loom/decompilers/cfr/CFRSinkFactory.java | 16 +- .../decompilers/cfr/LoomCFRDecompiler.java | 23 ++- .../decompilers/LoomInternalDecompiler.java} | 43 ++-- .../FabricFernFlowerDecompiler.java | 86 ++++++++ .../fernflower/FernflowerLogger.java | 6 +- .../fernflower/ThreadSafeResultSaver.java | 0 .../fernflower/TinyJavadocProvider.java | 15 +- .../vineflower/ThreadSafeResultSaver.java | 172 ++++++++++++++++ .../vineflower/TinyJavadocProvider.java | 188 ++++++++++++++++++ .../vineflower/VineflowerDecompiler.java} | 26 +-- .../vineflower/VineflowerLogger.java | 87 ++++++++ .../decompilers/DecompilerConfiguration.java | 108 +++++++++- .../test/integration/DecompileTest.groovy | 1 + 17 files changed, 790 insertions(+), 64 deletions(-) rename src/{main/java => decompilers/cfr}/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java (98%) rename src/{main/java => decompilers/cfr}/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java (90%) rename src/{main/java => decompilers/cfr}/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java (87%) rename src/{main/java/net/fabricmc/loom/decompilers/fernflower/FernFlowerUtils.java => decompilers/common/net/fabricmc/loom/decompilers/LoomInternalDecompiler.java} (63%) create mode 100644 src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java rename src/{main/java => decompilers/fernflower}/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java (92%) rename src/{main/java => decompilers/fernflower}/net/fabricmc/loom/decompilers/fernflower/ThreadSafeResultSaver.java (100%) rename src/{main/java => decompilers/fernflower}/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java (93%) create mode 100644 src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/ThreadSafeResultSaver.java create mode 100644 src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/TinyJavadocProvider.java rename src/{main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java => decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerDecompiler.java} (73%) create mode 100644 src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerLogger.java diff --git a/build.gradle b/build.gradle index 4c6034e41..014f8da9a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,11 +8,37 @@ plugins { id 'checkstyle' id 'jacoco' id 'codenarc' - alias(libs.plugins.kotlin) + alias(libs.plugins.kotlin) apply false // Delay this so we can perform magic 🪄 first. alias(libs.plugins.spotless) alias(libs.plugins.retry) } +/** + * Haha this is fun :) The Kotlin gradle plugin triggers deprecation warnings for custom configurations (https://youtrack.jetbrains.com/issue/KT-60879) + * We need to make DefaultConfiguration.isSpecialCaseOfChangingUsage think that our configurstion is a special case and not deprecated. + * We do this by setting DefaultConfiguration.roleAtCreation to LEGACY, thus isInLegacyRole will now return true. + * + * Yeah I know we can just ignore the deprecation warning, but doing so wouldn't alert us to issues when testing against pre-release Gradle versions. Also this is more fun :) + */ +def brokenConfigurations = [ + "commonDecompilerRuntimeClasspath", + "fernflowerRuntimeClasspath", + "cfrRuntimeClasspath", + "vineflowerRuntimeClasspath" +] + +configurations.configureEach { + if (brokenConfigurations.contains(it.name)) { + // For some reason Gradle stops us from using Groovy magic to do this, so lets do it the boring way. + def field = org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.class.getDeclaredField("roleAtCreation") + field.setAccessible(true) + field.set(it, ConfigurationRoles.LEGACY) + } +} + +// Ensure we apply the Kotlin plugin after, to allow for the above configuration to take place first +apply plugin: libs.plugins.kotlin.get().pluginId + tasks.withType(JavaCompile).configureEach { it.options.encoding = "UTF-8" } @@ -62,6 +88,29 @@ configurations.all { } } +sourceSets { + commonDecompiler { + java { + srcDir("src/decompilers/common") + } + } + fernflower { + java { + srcDir("src/decompilers/fernflower") + } + } + cfr { + java { + srcDir("src/decompilers/cfr") + } + } + vineflower { + java { + srcDir("src/decompilers/vineflower") + } + } +} + dependencies { implementation gradleApi() @@ -89,8 +138,23 @@ dependencies { } // decompilers - compileOnly runtimeLibs.fernflower - compileOnly runtimeLibs.cfr + fernflowerCompileOnly runtimeLibs.fernflower + fernflowerCompileOnly libs.fabric.mapping.io + + cfrCompileOnly runtimeLibs.cfr + cfrCompileOnly libs.fabric.mapping.io + + vineflowerCompileOnly runtimeLibs.vineflower + vineflowerCompileOnly libs.fabric.mapping.io + + fernflowerApi sourceSets.commonDecompiler.output + cfrApi sourceSets.commonDecompiler.output + vineflowerApi sourceSets.commonDecompiler.output + + implementation sourceSets.commonDecompiler.output + implementation sourceSets.fernflower.output + implementation sourceSets.cfr.output + implementation sourceSets.vineflower.output // source code remapping implementation libs.fabric.mercury @@ -130,6 +194,10 @@ jar { } from configurations.bootstrap.collect { it.isDirectory() ? it : zipTree(it) } + from sourceSets.commonDecompiler.output.classesDirs + from sourceSets.cfr.output.classesDirs + from sourceSets.fernflower.output.classesDirs + from sourceSets.vineflower.output.classesDirs } base { @@ -222,6 +290,8 @@ test { } } + +import org.gradle.api.internal.artifacts.configurations.ConfigurationRoles import org.gradle.launcher.cli.KotlinDslVersion import org.gradle.util.GradleVersion import org.w3c.dom.Document diff --git a/gradle/runtime.libs.versions.toml b/gradle/runtime.libs.versions.toml index d67b9fd11..fc707f6c9 100644 --- a/gradle/runtime.libs.versions.toml +++ b/gradle/runtime.libs.versions.toml @@ -2,6 +2,7 @@ # Decompilers fernflower = "2.0.0" cfr = "0.2.1" +vineflower = "1.9.3" # Runtime depedencies mixin-compile-extensions = "0.6.0" @@ -14,6 +15,7 @@ native-support = "1.0.1" # Decompilers fernflower = { module = "net.fabricmc:fabric-fernflower", version.ref = "fernflower" } cfr = { module = "net.fabricmc:cfr", version.ref = "cfr" } +vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" } # Runtime depedencies mixin-compile-extensions = { module = "net.fabricmc:fabric-mixin-compile-extensions", version.ref = "mixin-compile-extensions" } diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 4c2b65738..1c637f779 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -6,7 +6,7 @@ mockito = "5.4.0" java-debug = "0.48.0" mixin = "0.11.4+mixin.0.8.5" -gradle-nightly = "8.4-20230821223421+0000" +gradle-nightly = "8.5-20230908221250+0000" fabric-loader = "0.14.22" fabric-installer = "0.11.1" diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java similarity index 98% rename from src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java rename to src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java index aae260196..34236a43f 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java +++ b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java @@ -45,7 +45,6 @@ import org.benf.cfr.reader.util.output.DelegatingDumper; import org.benf.cfr.reader.util.output.Dumper; -import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.tree.MappingTree; @@ -66,7 +65,7 @@ public Dumper wrap(Dumper d) { private static MappingTree readMappings(Path input) { try (BufferedReader reader = Files.newBufferedReader(input)) { MemoryMappingTree mappingTree = new MemoryMappingTree(); - MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString()); + MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named"); MappingReader.read(reader, nsSwitch); return mappingTree; diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java similarity index 90% rename from src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java rename to src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java index bdc5a28a4..67a89473c 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java +++ b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java @@ -25,6 +25,7 @@ package net.fabricmc.loom.decompilers.cfr; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -37,23 +38,18 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; -import com.google.common.base.Charsets; import org.benf.cfr.reader.api.OutputSinkFactory; import org.benf.cfr.reader.api.SinkReturns; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import net.fabricmc.loom.util.IOStringConsumer; +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; public class CFRSinkFactory implements OutputSinkFactory { - private static final Logger ERROR_LOGGER = LoggerFactory.getLogger(CFRSinkFactory.class); - private final JarOutputStream outputStream; - private final IOStringConsumer logger; + private final LoomInternalDecompiler.Logger logger; private final Set addedDirectories = new HashSet<>(); private final Map> lineMap = new TreeMap<>(); - public CFRSinkFactory(JarOutputStream outputStream, IOStringConsumer logger) { + public CFRSinkFactory(JarOutputStream outputStream, LoomInternalDecompiler.Logger logger) { this.outputStream = outputStream; this.logger = logger; } @@ -72,7 +68,7 @@ public Sink getSink(SinkType sinkType, SinkClass sinkClass) { return switch (sinkType) { case JAVA -> (Sink) decompiledSink(); case LINENUMBER -> (Sink) lineNumberMappingSink(); - case EXCEPTION -> (e) -> ERROR_LOGGER.error((String) e); + case EXCEPTION -> (e) -> logger.error((String) e); default -> null; }; } @@ -83,7 +79,7 @@ private Sink decompiledSink() { if (!filename.isEmpty()) filename += "/"; filename += sinkable.getClassName() + ".java"; - byte[] data = sinkable.getJava().getBytes(Charsets.UTF_8); + byte[] data = sinkable.getJava().getBytes(StandardCharsets.UTF_8); writeToJar(filename, data); }; diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java similarity index 87% rename from src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java rename to src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java index de0b74d88..72f7b2e5b 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java +++ b/src/decompilers/cfr/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java @@ -45,10 +45,9 @@ import org.benf.cfr.reader.util.getopt.OptionsImpl; import org.benf.cfr.reader.util.output.SinkDumperFactory; -import net.fabricmc.loom.api.decompilers.DecompilationMetadata; -import net.fabricmc.loom.api.decompilers.LoomDecompiler; +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; -public final class LoomCFRDecompiler implements LoomDecompiler { +public final class LoomCFRDecompiler implements LoomInternalDecompiler { private static final Map DECOMPILE_OPTIONS = Map.of( "renameillegalidents", "true", "trackbytecodeloc", "true", @@ -56,16 +55,18 @@ public final class LoomCFRDecompiler implements LoomDecompiler { ); @Override - public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { + public void decompile(LoomInternalDecompiler.Context context) { + Path compiledJar = context.compiledJar(); + final String path = compiledJar.toAbsolutePath().toString(); final Map allOptions = new HashMap<>(DECOMPILE_OPTIONS); - allOptions.putAll(metaData.options()); + allOptions.putAll(context.options()); final Options options = OptionsImpl.getFactory().create(allOptions); ClassFileSourceImpl classFileSource = new ClassFileSourceImpl(options); - for (Path library : metaData.libraries()) { + for (Path library : context.libraries()) { classFileSource.addJarContent(library.toAbsolutePath().toString(), AnalysisType.JAR); } @@ -73,8 +74,8 @@ public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDes DCCommonState state = new DCCommonState(options, classFileSource); - if (metaData.javaDocs() != null) { - state = new DCCommonState(state, new CFRObfuscationMapping(metaData.javaDocs())); + if (context.javaDocs() != null) { + state = new DCCommonState(state, new CFRObfuscationMapping(context.javaDocs())); } final Manifest manifest = new Manifest(); @@ -82,8 +83,8 @@ public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDes Map> lineMap; - try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(sourcesDestination), manifest)) { - CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, metaData.logger()); + try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(context.sourcesDestination()), manifest)) { + CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, context.logger()); SinkDumperFactory dumperFactory = new SinkDumperFactory(cfrSinkFactory, options); Driver.doJar(state, path, AnalysisType.JAR, dumperFactory); @@ -93,7 +94,7 @@ public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDes throw new UncheckedIOException("Failed to decompile", e); } - writeLineMap(linemapDestination, lineMap); + writeLineMap(context.linemapDestination(), lineMap); } private void writeLineMap(Path output, Map> lineMap) { diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernFlowerUtils.java b/src/decompilers/common/net/fabricmc/loom/decompilers/LoomInternalDecompiler.java similarity index 63% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/FernFlowerUtils.java rename to src/decompilers/common/net/fabricmc/loom/decompilers/LoomInternalDecompiler.java index ee07f045c..ad6870527 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernFlowerUtils.java +++ b/src/decompilers/common/net/fabricmc/loom/decompilers/LoomInternalDecompiler.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2016-2022 FabricMC + * Copyright (c) 2023 FabricMC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,23 +22,40 @@ * SOFTWARE. */ -package net.fabricmc.loom.decompilers.fernflower; +package net.fabricmc.loom.decompilers; -import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; -import org.jetbrains.java.decompiler.util.InterpreterUtil; +// This is an internal interface to loom, DO NOT USE this in your own plugins. +public interface LoomInternalDecompiler { + void decompile(Context context); -import net.fabricmc.loom.util.ZipUtils; + interface Context { + Path compiledJar(); -public class FernFlowerUtils { - public static byte[] getBytecode(String externalPath, String internalPath) throws IOException { - File file = new File(externalPath); + Path sourcesDestination(); - if (internalPath == null) { - return InterpreterUtil.getBytes(file); - } else { - return ZipUtils.unpack(file.toPath(), internalPath); - } + Path linemapDestination(); + + int numberOfThreads(); + + Path javaDocs(); + + Collection libraries(); + + Logger logger(); + + Map options(); + + byte[] unpackZip(Path zip, String path) throws IOException; + } + + interface Logger { + void accept(String data) throws IOException; + + void error(String msg); } } diff --git a/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java new file mode 100644 index 000000000..f0e33ee2c --- /dev/null +++ b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java @@ -0,0 +1,86 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2019-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.decompilers.fernflower; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.jetbrains.java.decompiler.main.Fernflower; +import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; +import org.jetbrains.java.decompiler.util.InterpreterUtil; + +import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; + +public final class FabricFernFlowerDecompiler implements LoomInternalDecompiler { + @Override + public void decompile(LoomInternalDecompiler.Context context) { + Path sourcesDestination = context.sourcesDestination(); + Path linemapDestination = context.linemapDestination(); + + final Map options = new HashMap<>( + Map.of( + IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1", + IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1", + IFernflowerPreferences.REMOVE_SYNTHETIC, "1", + IFernflowerPreferences.LOG_LEVEL, "trace", + IFernflowerPreferences.THREADS, String.valueOf(context.numberOfThreads()), + IFernflowerPreferences.INDENT_STRING, "\t", + IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(context.javaDocs().toFile()) + ) + ); + + options.putAll(context.options()); + + IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile); + Fernflower ff = new Fernflower((externalPath, internalPath) -> FabricFernFlowerDecompiler.this.getBytecode(externalPath, internalPath, context), saver, options, new FernflowerLogger(context.logger())); + + for (Path library : context.libraries()) { + ff.addLibrary(library.toFile()); + } + + ff.addSource(context.compiledJar().toFile()); + + try { + ff.decompileContext(); + } finally { + ff.clearContext(); + } + } + + private byte[] getBytecode(String externalPath, String internalPath, LoomInternalDecompiler.Context context) throws IOException { + File file = new File(externalPath); + + if (internalPath == null) { + return InterpreterUtil.getBytes(file); + } else { + return context.unpackZip(file.toPath(), internalPath); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java similarity index 92% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java rename to src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java index 3699e3a6e..e43e0ecd3 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java +++ b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java @@ -28,12 +28,12 @@ import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; -import net.fabricmc.loom.util.IOStringConsumer; +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; public class FernflowerLogger extends IFernflowerLogger { - private final IOStringConsumer logger; + private final LoomInternalDecompiler.Logger logger; - public FernflowerLogger(IOStringConsumer logger) { + public FernflowerLogger(LoomInternalDecompiler.Logger logger) { this.logger = logger; } diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadSafeResultSaver.java b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/ThreadSafeResultSaver.java similarity index 100% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadSafeResultSaver.java rename to src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/ThreadSafeResultSaver.java diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java similarity index 93% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java rename to src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java index ced8161d1..c80608341 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java +++ b/src/decompilers/fernflower/net/fabricmc/loom/decompilers/fernflower/TinyJavadocProvider.java @@ -35,16 +35,17 @@ import org.jetbrains.java.decompiler.struct.StructField; import org.jetbrains.java.decompiler.struct.StructMethod; import org.jetbrains.java.decompiler.struct.StructRecordComponent; -import org.objectweb.asm.Opcodes; import net.fabricmc.fernflower.api.IFabricJavadocProvider; -import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.tree.MappingTree; import net.fabricmc.mappingio.tree.MemoryMappingTree; public class TinyJavadocProvider implements IFabricJavadocProvider { + private static final int ACC_STATIC = 0x0008; + private static final int ACC_RECORD = 0x10000; + private final MappingTree mappingTree; public TinyJavadocProvider(File tinyFile) { @@ -93,7 +94,7 @@ public String getClassDoc(StructClass structClass) { addedParam = true; } - parts.add(String.format("@param %s %s", fieldMapping.getName(MappingsNamespace.NAMED.toString()), comment)); + parts.add(String.format("@param %s %s", fieldMapping.getName("named"), comment)); } } @@ -151,7 +152,7 @@ public String getMethodDoc(StructClass structClass, StructMethod structMethod) { addedParam = true; } - parts.add(String.format("@param %s %s", argMapping.getName(MappingsNamespace.NAMED.toString()), comment)); + parts.add(String.format("@param %s %s", argMapping.getName("named"), comment)); } } @@ -168,7 +169,7 @@ public String getMethodDoc(StructClass structClass, StructMethod structMethod) { private static MappingTree readMappings(File input) { try (BufferedReader reader = Files.newBufferedReader(input.toPath())) { MemoryMappingTree mappingTree = new MemoryMappingTree(); - MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString()); + MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named"); MappingReader.read(reader, nsSwitch); return mappingTree; @@ -178,10 +179,10 @@ private static MappingTree readMappings(File input) { } public static boolean isRecord(StructClass structClass) { - return (structClass.getAccessFlags() & Opcodes.ACC_RECORD) != 0; + return (structClass.getAccessFlags() & ACC_RECORD) != 0; } public static boolean isStatic(StructField structField) { - return (structField.getAccessFlags() & Opcodes.ACC_STATIC) != 0; + return (structField.getAccessFlags() & ACC_STATIC) != 0; } } diff --git a/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/ThreadSafeResultSaver.java b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/ThreadSafeResultSaver.java new file mode 100644 index 000000000..bb77aa16b --- /dev/null +++ b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/ThreadSafeResultSaver.java @@ -0,0 +1,172 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2019-2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.decompilers.vineflower; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.java.decompiler.main.DecompilerContext; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; + +public class ThreadSafeResultSaver implements IResultSaver { + private final Supplier output; + private final Supplier lineMapFile; + + public Map outputStreams = new HashMap<>(); + public Map saveExecutors = new HashMap<>(); + public PrintWriter lineMapWriter; + + public ThreadSafeResultSaver(Supplier output, Supplier lineMapFile) { + this.output = output; + this.lineMapFile = lineMapFile; + } + + @Override + public void createArchive(String path, String archiveName, Manifest manifest) { + String key = path + "/" + archiveName; + File file = output.get(); + + try { + FileOutputStream fos = new FileOutputStream(file); + ZipOutputStream zos = manifest == null ? new ZipOutputStream(fos) : new JarOutputStream(fos, manifest); + outputStreams.put(key, zos); + saveExecutors.put(key, Executors.newSingleThreadExecutor()); + } catch (IOException e) { + throw new RuntimeException("Unable to create archive: " + file, e); + } + + if (lineMapFile.get() != null) { + try { + lineMapWriter = new PrintWriter(new FileWriter(lineMapFile.get())); + } catch (IOException e) { + throw new RuntimeException("Unable to create line mapping file: " + lineMapFile.get(), e); + } + } + } + + @Override + public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { + this.saveClassEntry(path, archiveName, qualifiedName, entryName, content, null); + } + + @Override + public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content, int[] mapping) { + String key = path + "/" + archiveName; + ExecutorService executor = saveExecutors.get(key); + executor.submit(() -> { + ZipOutputStream zos = outputStreams.get(key); + + try { + zos.putNextEntry(new ZipEntry(entryName)); + + if (content != null) { + zos.write(content.getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + DecompilerContext.getLogger().writeMessage("Cannot write entry " + entryName, e); + } + + if (mapping != null && lineMapWriter != null) { + int maxLine = 0; + int maxLineDest = 0; + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < mapping.length; i += 2) { + maxLine = Math.max(maxLine, mapping[i]); + maxLineDest = Math.max(maxLineDest, mapping[i + 1]); + builder.append("\t").append(mapping[i]).append("\t").append(mapping[i + 1]).append("\n"); + } + + lineMapWriter.println(qualifiedName + "\t" + maxLine + "\t" + maxLineDest); + lineMapWriter.println(builder.toString()); + } + }); + } + + @Override + public void closeArchive(String path, String archiveName) { + String key = path + "/" + archiveName; + ExecutorService executor = saveExecutors.get(key); + Future closeFuture = executor.submit(() -> { + ZipOutputStream zos = outputStreams.get(key); + + try { + zos.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to close zip. " + key, e); + } + }); + executor.shutdown(); + + try { + closeFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + outputStreams.remove(key); + saveExecutors.remove(key); + + if (lineMapWriter != null) { + lineMapWriter.flush(); + lineMapWriter.close(); + } + } + + @Override + public void saveFolder(String path) { + } + + @Override + public void copyFile(String source, String path, String entryName) { + } + + @Override + public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { + } + + @Override + public void saveDirEntry(String path, String archiveName, String entryName) { + } + + @Override + public void copyEntry(String source, String path, String archiveName, String entry) { + } +} diff --git a/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/TinyJavadocProvider.java b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/TinyJavadocProvider.java new file mode 100644 index 000000000..ba8ada9fc --- /dev/null +++ b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/TinyJavadocProvider.java @@ -0,0 +1,188 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2019-2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.decompilers.vineflower; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.java.decompiler.struct.StructClass; +import org.jetbrains.java.decompiler.struct.StructField; +import org.jetbrains.java.decompiler.struct.StructMethod; +import org.jetbrains.java.decompiler.struct.StructRecordComponent; + +import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class TinyJavadocProvider implements IFabricJavadocProvider { + private static final int ACC_STATIC = 0x0008; + private static final int ACC_RECORD = 0x10000; + + private final MappingTree mappingTree; + + public TinyJavadocProvider(File tinyFile) { + mappingTree = readMappings(tinyFile); + } + + @Override + public String getClassDoc(StructClass structClass) { + MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName); + + if (classMapping == null) { + return null; + } + + if (!isRecord(structClass)) { + return classMapping.getComment(); + } + + /** + * Handle the record component docs here. + * + * Record components are mapped via the field name, thus take the docs from the fields and display them on then class. + */ + List parts = new ArrayList<>(); + + if (classMapping.getComment() != null) { + parts.add(classMapping.getComment()); + } + + boolean addedParam = false; + + for (StructRecordComponent component : structClass.getRecordComponents()) { + // The component will always match the field name and descriptor + MappingTree.FieldMapping fieldMapping = classMapping.getField(component.getName(), component.getDescriptor()); + + if (fieldMapping == null) { + continue; + } + + String comment = fieldMapping.getComment(); + + if (comment != null) { + if (!addedParam && classMapping.getComment() != null) { + //Add a blank line before components when the class has a comment + parts.add(""); + addedParam = true; + } + + parts.add(String.format("@param %s %s", fieldMapping.getName("named"), comment)); + } + } + + if (parts.isEmpty()) { + return null; + } + + return String.join("\n", parts); + } + + @Override + public String getFieldDoc(StructClass structClass, StructField structField) { + // None static fields in records are handled in the class javadoc. + if (isRecord(structClass) && !isStatic(structField)) { + return null; + } + + MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName); + + if (classMapping == null) { + return null; + } + + MappingTree.FieldMapping fieldMapping = classMapping.getField(structField.getName(), structField.getDescriptor()); + + return fieldMapping != null ? fieldMapping.getComment() : null; + } + + @Override + public String getMethodDoc(StructClass structClass, StructMethod structMethod) { + MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName); + + if (classMapping == null) { + return null; + } + + MappingTree.MethodMapping methodMapping = classMapping.getMethod(structMethod.getName(), structMethod.getDescriptor()); + + if (methodMapping != null) { + List parts = new ArrayList<>(); + + if (methodMapping.getComment() != null) { + parts.add(methodMapping.getComment()); + } + + boolean addedParam = false; + + for (MappingTree.MethodArgMapping argMapping : methodMapping.getArgs()) { + String comment = argMapping.getComment(); + + if (comment != null) { + if (!addedParam && methodMapping.getComment() != null) { + //Add a blank line before params when the method has a comment + parts.add(""); + addedParam = true; + } + + parts.add(String.format("@param %s %s", argMapping.getName("named"), comment)); + } + } + + if (parts.isEmpty()) { + return null; + } + + return String.join("\n", parts); + } + + return null; + } + + private static MappingTree readMappings(File input) { + try (BufferedReader reader = Files.newBufferedReader(input.toPath())) { + MemoryMappingTree mappingTree = new MemoryMappingTree(); + MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named"); + MappingReader.read(reader, nsSwitch); + + return mappingTree; + } catch (IOException e) { + throw new RuntimeException("Failed to read mappings", e); + } + } + + public static boolean isRecord(StructClass structClass) { + return (structClass.getAccessFlags() & ACC_RECORD) != 0; + } + + public static boolean isStatic(StructField structField) { + return (structField.getAccessFlags() & ACC_STATIC) != 0; + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerDecompiler.java similarity index 73% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java rename to src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerDecompiler.java index 2d2041b6f..ad865d735 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java +++ b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerDecompiler.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2019-2021 FabricMC + * Copyright (c) 2019-2023 FabricMC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ * SOFTWARE. */ -package net.fabricmc.loom.decompilers.fernflower; +package net.fabricmc.loom.decompilers.vineflower; import java.nio.file.Path; import java.util.HashMap; @@ -33,34 +33,36 @@ import org.jetbrains.java.decompiler.main.extern.IResultSaver; import net.fabricmc.fernflower.api.IFabricJavadocProvider; -import net.fabricmc.loom.api.decompilers.DecompilationMetadata; -import net.fabricmc.loom.api.decompilers.LoomDecompiler; +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; -public final class FabricFernFlowerDecompiler implements LoomDecompiler { +public final class VineflowerDecompiler implements LoomInternalDecompiler { @Override - public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { + public void decompile(Context context) { + Path sourcesDestination = context.sourcesDestination(); + Path linemapDestination = context.linemapDestination(); + final Map options = new HashMap<>( Map.of( IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1", IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1", IFernflowerPreferences.REMOVE_SYNTHETIC, "1", IFernflowerPreferences.LOG_LEVEL, "trace", - IFernflowerPreferences.THREADS, String.valueOf(metaData.numberOfThreads()), + IFernflowerPreferences.THREADS, String.valueOf(context.numberOfThreads()), IFernflowerPreferences.INDENT_STRING, "\t", - IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(metaData.javaDocs().toFile()) + IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(context.javaDocs().toFile()) ) ); - options.putAll(metaData.options()); + options.putAll(context.options()); IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile); - Fernflower ff = new Fernflower(FernFlowerUtils::getBytecode, saver, options, new FernflowerLogger(metaData.logger())); + Fernflower ff = new Fernflower(saver, options, new VineflowerLogger(context.logger())); - for (Path library : metaData.libraries()) { + for (Path library : context.libraries()) { ff.addLibrary(library.toFile()); } - ff.addSource(compiledJar.toFile()); + ff.addSource(context.compiledJar().toFile()); try { ff.decompileContext(); diff --git a/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerLogger.java b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerLogger.java new file mode 100644 index 000000000..675636f47 --- /dev/null +++ b/src/decompilers/vineflower/net/fabricmc/loom/decompilers/vineflower/VineflowerLogger.java @@ -0,0 +1,87 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021-2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.decompilers.vineflower; + +import java.io.IOException; + +import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; + +import net.fabricmc.loom.decompilers.LoomInternalDecompiler; + +public class VineflowerLogger extends IFernflowerLogger { + private final LoomInternalDecompiler.Logger logger; + + public VineflowerLogger(LoomInternalDecompiler.Logger logger) { + this.logger = logger; + } + + @Override + public void writeMessage(String message, Severity severity) { + if (severity.ordinal() < Severity.ERROR.ordinal()) return; + + System.err.println(message); + } + + @Override + public void writeMessage(String message, Severity severity, Throwable t) { + if (severity.ordinal() < Severity.ERROR.ordinal()) return; + + writeMessage(message, severity); + t.printStackTrace(System.err); + } + + private void write(String data) { + try { + logger.accept(data); + } catch (IOException e) { + throw new RuntimeException("Failed to log", e); + } + } + + @Override + public void startReadingClass(String className) { + write("Decompiling " + className); + } + + @Override + public void startClass(String className) { + write("Decompiling " + className); + } + + @Override + public void startWriteClass(String className) { + // Nope + } + + @Override + public void startMethod(String methodName) { + // Nope + } + + @Override + public void endMethod() { + // Nope + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java index 70cecf6b1..618629555 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java +++ b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java @@ -24,17 +24,27 @@ package net.fabricmc.loom.decompilers; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; + import javax.inject.Inject; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.decompilers.cfr.LoomCFRDecompiler; import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler; +import net.fabricmc.loom.decompilers.vineflower.VineflowerDecompiler; import net.fabricmc.loom.util.LoomVersions; +import net.fabricmc.loom.util.ZipUtils; public abstract class DecompilerConfiguration implements Runnable { @Inject @@ -44,9 +54,11 @@ public abstract class DecompilerConfiguration implements Runnable { public void run() { var fernflowerConfiguration = createConfiguration("fernflower", LoomVersions.FERNFLOWER); var cfrConfiguration = createConfiguration("cfr", LoomVersions.CFR); + var vineflowerConfiguration = createConfiguration("vineflower", LoomVersions.VINEFLOWER); - registerDecompiler(getProject(), "fernFlower", FabricFernFlowerDecompiler.class, fernflowerConfiguration); - registerDecompiler(getProject(), "cfr", LoomCFRDecompiler.class, cfrConfiguration); + registerDecompiler(getProject(), "fernFlower", BuiltinFernflower.class, fernflowerConfiguration); + registerDecompiler(getProject(), "cfr", BuiltinCfr.class, cfrConfiguration); + registerDecompiler(getProject(), "vineflower", BuiltinVineflower.class, vineflowerConfiguration); } private NamedDomainObjectProvider createConfiguration(String name, LoomVersions version) { @@ -62,4 +74,96 @@ private void registerDecompiler(Project project, String name, Class libraries() { + return metaData.libraries(); + } + + @Override + public LoomInternalDecompiler.Logger logger() { + return logger; + } + + @Override + public Map options() { + return metaData.options(); + } + + @Override + public byte[] unpackZip(Path zip, String path) throws IOException { + return ZipUtils.unpack(zip, path); + } + }); + } + } + + public static final class BuiltinFernflower extends BuiltinDecompiler { + public BuiltinFernflower() { + super(new FabricFernFlowerDecompiler()); + } + } + + public static final class BuiltinCfr extends BuiltinDecompiler { + public BuiltinCfr() { + super(new LoomCFRDecompiler()); + } + } + + public static final class BuiltinVineflower extends BuiltinDecompiler { + public BuiltinVineflower() { + super(new VineflowerDecompiler()); + } + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy index 5b2f0e105..7431aae09 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy @@ -48,6 +48,7 @@ class DecompileTest extends Specification implements GradleProjectTestTrait { decompiler | task | version 'fernflower' | "genSourcesWithFernFlower" | PRE_RELEASE_GRADLE 'cfr' | "genSourcesWithCfr" | PRE_RELEASE_GRADLE + 'vineflower' | "genSourcesWithVineflower" | PRE_RELEASE_GRADLE } @Unroll From 1a4f76584faa45aa99f32cc2e26f5eee6e86970f Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 11 Sep 2023 11:31:10 +0100 Subject: [PATCH 10/12] Fix reproducable zips across timezones. (#952) * Create test for zip timezones * Fix :) * Update ReproducibleBuildTest * Update windows hashes --- .../fabricmc/loom/util/ZipReprocessorUtil.java | 11 ++--------- .../integration/ReproducibleBuildTest.groovy | 12 ++++++------ .../loom/test/unit/ZipUtilsTest.groovy | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java index a3b7797a6..d962be8c4 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java @@ -30,7 +30,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; -import java.nio.file.attribute.FileTime; import java.util.Calendar; import java.util.Comparator; import java.util.GregorianCalendar; @@ -42,11 +41,6 @@ import org.intellij.lang.annotations.MagicConstant; public class ZipReprocessorUtil { - /** - * See {@link org.gradle.api.internal.file.archive.ZipCopyAction} about this. - */ - private static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0).getTimeInMillis(); - private ZipReprocessorUtil() { } private static final String MANIFEST_LOCATION = "META-INF/MANIFEST.MF"; @@ -183,9 +177,8 @@ private static void copyZipEntry(ZipOutputStream zipOutputStream, ZipEntry entry } private static void setConstantFileTime(ZipEntry entry) { - entry.setTime(ZipReprocessorUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES); - entry.setLastModifiedTime(FileTime.fromMillis(ZipReprocessorUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES)); - entry.setLastAccessTime(FileTime.fromMillis(ZipReprocessorUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES)); + // See https://github.com/openjdk/jdk/blob/master/test/jdk/java/util/zip/ZipFile/ZipEntryTimeBounds.java + entry.setTime(new GregorianCalendar(1980, Calendar.JANUARY, 1, 0, 0, 0).getTimeInMillis()); } @MagicConstant(valuesFromClass = ZipOutputStream.class) diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy index f40a65e0d..0936f58f7 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy @@ -55,13 +55,13 @@ class ReproducibleBuildTest extends Specification implements GradleProjectTestTr where: version | modHash | sourceHash - DEFAULT_GRADLE | "174c9b52f4bc6d489548d11b42e853cf" | [ - "5e6e56df303b4fbaaef372d6f143dbfc", - "92b6fbffd0bd14bf3c626750eb86c264" + DEFAULT_GRADLE | "97240b42385adfaa1952e9c4ea942f71" | [ + "61438feb9bd548788bbc637637d202fc", + "185ad8396d89b726064682bf22572036" ] - PRE_RELEASE_GRADLE | "174c9b52f4bc6d489548d11b42e853cf" | [ - "5e6e56df303b4fbaaef372d6f143dbfc", - "92b6fbffd0bd14bf3c626750eb86c264" + PRE_RELEASE_GRADLE | "97240b42385adfaa1952e9c4ea942f71" | [ + "61438feb9bd548788bbc637637d202fc", + "185ad8396d89b726064682bf22572036" ] } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy index 95c469166..1b40e3f3d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy @@ -26,6 +26,7 @@ package net.fabricmc.loom.test.unit import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.time.ZoneId import spock.lang.Specification @@ -155,6 +156,9 @@ class ZipUtilsTest extends Specification { def "append zip entry"() { given: + def currentTimezone = TimeZone.getDefault() + TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of(timezone))) + // Create a reproducible input zip def dir = Files.createTempDirectory("loom-zip-test") def zip = Files.createTempFile("loom-zip-test", ".zip") @@ -167,9 +171,21 @@ class ZipUtilsTest extends Specification { // Add an entry to it ZipReprocessorUtil.appendZipEntry(zip.toFile(), "fabric.mod.json", "Some text".getBytes(StandardCharsets.UTF_8)) + // Reset the timezone back + TimeZone.setDefault(currentTimezone) + then: ZipUtils.unpack(zip, "text.txt") == "hello world".bytes ZipUtils.unpack(zip, "fabric.mod.json") == "Some text".bytes - Checksum.sha1Hex(zip) == "232ecda4c770bde8ba618e7a194a4f7b57928dc5" + Checksum.sha1Hex(zip) == "1b06cc0aaa65ab2b0d423fe33431ff5bd14bf9c8" + + where: + timezone | _ + "UTC" | _ + "US/Central" | _ + "Europe/London" | _ + "Australia/Sydney" | _ + "Etc/GMT-6" | _ + "Etc/GMT+9" | _ } } From 0b3612135732dab9bca586fb73deed026c68022a Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Mon, 11 Sep 2023 11:47:19 +0100 Subject: [PATCH 11/12] Add `Fabric-Jar-Type` to jar manifest. Either 'classes' or 'sources', the idea is mod distribution platforms can check that sources jars are not uploaded accidentally. Closes #939 --- .../net/fabricmc/loom/task/AbstractRemapJarTask.java | 11 +++++++++++ .../java/net/fabricmc/loom/task/RemapJarTask.java | 2 ++ .../net/fabricmc/loom/task/RemapSourcesJarTask.java | 1 + 3 files changed, 14 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index ca7da77e3..7da1ea1e5 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -75,6 +75,7 @@ public abstract class AbstractRemapJarTask extends Jar { public static final String MANIFEST_NAMESPACE_KEY = "Fabric-Mapping-Namespace"; public static final String MANIFEST_SPLIT_ENV_KEY = "Fabric-Loom-Split-Environment"; public static final String MANIFEST_CLIENT_ENTRIES_KEY = "Fabric-Loom-Client-Only-Entries"; + public static final String MANIFEST_JAR_TYPE_KEY = "Fabric-Jar-Type"; public static final Attributes.Name MANIFEST_SPLIT_ENV_NAME = new Attributes.Name(MANIFEST_SPLIT_ENV_KEY); public static final Attributes.Name MANIFEST_CLIENT_ENTRIES_NAME = new Attributes.Name(MANIFEST_CLIENT_ENTRIES_KEY); @@ -112,6 +113,11 @@ public abstract class AbstractRemapJarTask extends Jar { @Optional public abstract Property getClientOnlySourceSetName(); + @Input + @Optional + @ApiStatus.Internal + public abstract Property getJarType(); + private final Provider jarManifestServiceProvider; @Inject @@ -120,6 +126,7 @@ public AbstractRemapJarTask() { getTargetNamespace().convention(MappingsNamespace.INTERMEDIARY.toString()).finalizeValueOnRead(); getRemapperIsolation().convention(false).finalizeValueOnRead(); getIncludesClientOnlyClasses().convention(false).finalizeValueOnRead(); + getJarType().finalizeValueOnRead(); jarManifestServiceProvider = JarManifestService.get(getProject()); usesService(jarManifestServiceProvider); @@ -149,6 +156,10 @@ public final

void submitWork(Class s.endsWith(".class")).toList()); } + if (getJarType().isPresent()) { + params.getManifestAttributes().put(MANIFEST_JAR_TYPE_KEY, getJarType().get()); + } + action.execute(params); }); } diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index e680792c2..9639f3d18 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -109,6 +109,8 @@ public RemapJarTask() { // Make outputs reproducible by default setReproducibleFileOrder(true); setPreserveFileTimestamps(false); + + getJarType().set("classes"); } private void setupPreparationTask() { diff --git a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java index cca9638ed..2a9424acc 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java @@ -51,6 +51,7 @@ public RemapSourcesJarTask() { serviceManagerProvider = BuildSharedServiceManager.createForTask(this, getBuildEventsListenerRegistry()); getClasspath().from(getProject().getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); + getJarType().set("sources"); } @TaskAction From bd09af17830feb068188bce948df200aeb7fab62 Mon Sep 17 00:00:00 2001 From: modmuss Date: Fri, 22 Sep 2023 18:55:44 +0100 Subject: [PATCH 12/12] Downloader: Protect against partially downloaded files. (#954) * Downloader: Protect against partially downloaded files. * Cleanup * Add 1 minute timeout. * Checkstyle --- .../fabricmc/loom/util/download/Download.java | 124 ++++++++++++------ 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/util/download/Download.java b/src/main/java/net/fabricmc/loom/util/download/Download.java index 2ba074bfa..79c200cc0 100644 --- a/src/main/java/net/fabricmc/loom/util/download/Download.java +++ b/src/main/java/net/fabricmc/loom/util/download/Download.java @@ -62,9 +62,11 @@ public final class Download { private static final String E_TAG = "ETag"; private static final Logger LOGGER = LoggerFactory.getLogger(Download.class); + private static final Duration TIMEOUT = Duration.ofMinutes(1); private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) .proxy(ProxySelector.getDefault()) + .connectTimeout(TIMEOUT) .build(); public static DownloadBuilder create(String url) throws URISyntaxException { @@ -93,17 +95,20 @@ public static DownloadBuilder create(String url) throws URISyntaxException { this.downloadAttempt = downloadAttempt; } - private HttpRequest getRequest() { + private HttpRequest.Builder requestBuilder() { return HttpRequest.newBuilder(url) + .timeout(TIMEOUT) .version(httpVersion) - .GET() + .GET(); + } + + private HttpRequest getRequest() { + return requestBuilder() .build(); } private HttpRequest getETagRequest(String etag) { - return HttpRequest.newBuilder(url) - .version(httpVersion) - .GET() + return requestBuilder() .header("If-None-Match", etag) .build(); } @@ -190,47 +195,12 @@ private void doDownload(Path output) throws DownloadException { return; } - if (success) { - try { - Files.deleteIfExists(output); - } catch (IOException e) { - throw error(e, "Failed to delete existing file"); - } - - final long length = Long.parseLong(response.headers().firstValue("Content-Length").orElse("-1")); - AtomicLong totalBytes = new AtomicLong(0); - - try (OutputStream outputStream = Files.newOutputStream(output, StandardOpenOption.CREATE_NEW)) { - copyWithCallback(decodeOutput(response), outputStream, value -> { - if (length < 0) { - return; - } - - progressListener.onProgress(totalBytes.addAndGet(value), length); - }); - } catch (IOException e) { - throw error(e, "Failed to decode and write download output"); - } - - if (Files.notExists(output)) { - throw error("No file was downloaded"); - } - - if (length > 0) { - try { - final long actualLength = Files.size(output); - - if (actualLength != length) { - throw error("Unexpected file length of %d bytes, expected %d bytes".formatted(actualLength, length)); - } - } catch (IOException e) { - throw error(e); - } - } - } else { + if (!success) { throw statusError("HTTP request returned unsuccessful status (%d)", statusCode); } + downloadToPath(output, response); + if (useEtag) { final HttpHeaders headers = response.headers(); final String responseETag = headers.firstValue(E_TAG.toLowerCase(Locale.ROOT)).orElse(null); @@ -260,6 +230,58 @@ private void doDownload(Path output) throws DownloadException { } } + private void downloadToPath(Path output, HttpResponse response) throws DownloadException { + // Download the file initially to a .part file + final Path partFile = getPartFile(output); + + try { + Files.deleteIfExists(output); + Files.deleteIfExists(partFile); + } catch (IOException e) { + throw error(e, "Failed to delete existing file"); + } + + final long length = Long.parseLong(response.headers().firstValue("Content-Length").orElse("-1")); + AtomicLong totalBytes = new AtomicLong(0); + + try (OutputStream outputStream = Files.newOutputStream(partFile, StandardOpenOption.CREATE_NEW)) { + copyWithCallback(decodeOutput(response), outputStream, value -> { + if (length < 0) { + return; + } + + progressListener.onProgress(totalBytes.addAndGet(value), length); + }); + } catch (IOException e) { + throw error(e, "Failed to decode and write download output"); + } + + if (Files.notExists(partFile)) { + throw error("No file was downloaded"); + } + + if (length > 0) { + try { + final long actualLength = Files.size(partFile); + + if (actualLength != length) { + throw error("Unexpected file length of %d bytes, expected %d bytes".formatted(actualLength, length)); + } + } catch (IOException e) { + throw error(e); + } + } + + try { + // Once the file has been fully read, create a hard link to the destination file. + // And then remove the temporary file, this ensures that the output file only exists in fully populated state. + Files.createLink(output, partFile); + Files.delete(partFile); + } catch (IOException e) { + throw error(e, "Failed to complete download"); + } + } + private void copyWithCallback(InputStream is, OutputStream os, IntConsumer consumer) throws IOException { byte[] buffer = new byte[1024]; int length; @@ -389,6 +411,18 @@ private void tryCleanup(Path output) { } catch (IOException ignored) { // ignored } + + try { + Files.deleteIfExists(getLockFile(output)); + } catch (IOException ignored) { + // ignored + } + + try { + Files.deleteIfExists(getPartFile(output)); + } catch (IOException ignored) { + // ignored + } } // A faster exists check @@ -405,6 +439,10 @@ private Path getLockFile(Path output) { return output.resolveSibling(output.getFileName() + ".lock"); } + private Path getPartFile(Path output) { + return output.resolveSibling(output.getFileName() + ".part"); + } + private boolean getAndResetLock(Path output) throws DownloadException { final Path lock = getLockFile(output); final boolean exists = exists(lock);