From a25958b31e987501213a3b498b2591d264d3b8b2 Mon Sep 17 00:00:00 2001 From: Kyle Wood Date: Tue, 11 Aug 2020 23:01:37 -0700 Subject: [PATCH] Gradle 6.7, initial patch remap (not working) --- .gitattributes | 1 - .gitignore | 143 +---- build.gradle.kts | 28 +- gradle/wrapper/gradle-wrapper.jar | Bin 56172 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 53 +- gradlew.bat | 43 +- license/copyright.txt | 1 - src/main/kotlin/Paperweight.kt | 524 ++++++++++++------ src/main/kotlin/PaperweightException.kt | 1 - src/main/kotlin/ext/CraftBukkitExtension.kt | 40 +- src/main/kotlin/ext/PaperExtension.kt | 39 +- src/main/kotlin/ext/PaperweightExtension.kt | 30 +- src/main/kotlin/ext/SpigotExtension.kt | 15 +- src/main/kotlin/ext/extensions.kt | 15 +- .../tasks/AddMissingSpigotClassMappings.kt | 73 ++- src/main/kotlin/tasks/ApplyDiffPatches.kt | 58 +- src/main/kotlin/tasks/ApplyGitPatches.kt | 121 ++-- src/main/kotlin/tasks/ApplyMcpPatches.kt | 33 +- src/main/kotlin/tasks/ApplyPaperPatches.kt | 89 +++ src/main/kotlin/tasks/ApplySourceAt.kt | 81 +++ src/main/kotlin/tasks/BaseTask.kt | 48 ++ .../kotlin/tasks/ControllableOutputTask.kt | 51 ++ src/main/kotlin/tasks/DecompileVanillaJar.kt | 29 +- src/main/kotlin/tasks/DownloadServerJar.kt | 31 +- src/main/kotlin/tasks/Extract.kt | 72 --- src/main/kotlin/tasks/FilterExcludes.kt | 28 +- src/main/kotlin/tasks/GenerateSpigotSrgs.kt | 91 +-- src/main/kotlin/tasks/GenerateSrgs.kt | 155 ++++-- src/main/kotlin/tasks/GetRemoteJsons.kt | 96 ---- src/main/kotlin/tasks/InspectVanillaJar.kt | 109 ++++ ...illaJarSrg.kt => MergeAccessTransforms.kt} | 43 +- src/main/kotlin/tasks/PatchMcpCsv.kt | 31 +- ...ependencies.kt => RemapAccessTransform.kt} | 37 +- src/main/kotlin/tasks/RemapSources.kt | 267 --------- src/main/kotlin/tasks/RemapSpigotAt.kt | 135 +++++ src/main/kotlin/tasks/RemapSrgSources.kt | 15 +- .../kotlin/tasks/RemapVanillaJarSpigot.kt | 82 ++- src/main/kotlin/tasks/RunForgeFlower.kt | 61 +- src/main/kotlin/tasks/RunMcInjector.kt | 68 +-- src/main/kotlin/tasks/RunSpecialSource.kt | 84 +++ ...GatherBuildData.kt => SetupMcLibraries.kt} | 29 +- .../kotlin/tasks/SetupSpigotDependencies.kt | 117 ---- src/main/kotlin/tasks/WriteLibrariesFile.kt | 26 +- src/main/kotlin/tasks/ZippedTask.kt | 18 +- src/main/kotlin/tasks/download-task.kt | 317 +++++++++++ src/main/kotlin/tasks/extract-tasks.kt | 127 +++++ src/main/kotlin/tasks/jar-tasks.kt | 100 ++++ .../{ => patchremap}/ApplyAccessTransform.kt | 51 +- .../kotlin/tasks/patchremap/PatchApplier.kt | 91 +++ .../patchremap/PatchSourceRemapWorker.kt | 96 ++++ .../kotlin/tasks/patchremap/RemapPatches.kt | 187 +++++++ .../sourceremap/AbstractParameterVisitor.kt | 74 +++ .../sourceremap/PatchParameterRemapper.kt | 95 ++++ .../kotlin/tasks/sourceremap/RemapSources.kt | 114 ++++ .../tasks/sourceremap/SrgParameterRemapper.kt | 142 +++++ src/main/kotlin/tasks/sourceremap/remap.kt | 90 +++ src/main/kotlin/util/BuildDataInfo.kt | 3 +- src/main/kotlin/util/Constants.kt | 27 +- ...ArtifactDescriptor.kt => MavenArtifact.kt} | 68 ++- src/main/kotlin/util/McpConfig.kt | 9 +- src/main/kotlin/util/MinecraftManifest.kt | 3 +- src/main/kotlin/util/download.kt | 140 +++++ src/main/kotlin/util/etag-util.kt | 106 ---- src/main/kotlin/util/git.kt | 56 +- src/main/kotlin/util/jvm.kt | 27 +- src/main/kotlin/util/utils.kt | 101 ++-- src/main/kotlin/util/zip.kt | 19 +- 68 files changed, 3498 insertions(+), 1658 deletions(-) create mode 100644 src/main/kotlin/tasks/ApplyPaperPatches.kt create mode 100644 src/main/kotlin/tasks/ApplySourceAt.kt create mode 100644 src/main/kotlin/tasks/BaseTask.kt create mode 100644 src/main/kotlin/tasks/ControllableOutputTask.kt delete mode 100644 src/main/kotlin/tasks/Extract.kt delete mode 100644 src/main/kotlin/tasks/GetRemoteJsons.kt create mode 100644 src/main/kotlin/tasks/InspectVanillaJar.kt rename src/main/kotlin/tasks/{RemapVanillaJarSrg.kt => MergeAccessTransforms.kt} (52%) rename src/main/kotlin/tasks/{SetupMcpDependencies.kt => RemapAccessTransform.kt} (56%) delete mode 100644 src/main/kotlin/tasks/RemapSources.kt create mode 100644 src/main/kotlin/tasks/RemapSpigotAt.kt create mode 100644 src/main/kotlin/tasks/RunSpecialSource.kt rename src/main/kotlin/tasks/{GatherBuildData.kt => SetupMcLibraries.kt} (65%) delete mode 100644 src/main/kotlin/tasks/SetupSpigotDependencies.kt create mode 100644 src/main/kotlin/tasks/download-task.kt create mode 100644 src/main/kotlin/tasks/extract-tasks.kt create mode 100644 src/main/kotlin/tasks/jar-tasks.kt rename src/main/kotlin/tasks/{ => patchremap}/ApplyAccessTransform.kt (73%) create mode 100644 src/main/kotlin/tasks/patchremap/PatchApplier.kt create mode 100644 src/main/kotlin/tasks/patchremap/PatchSourceRemapWorker.kt create mode 100644 src/main/kotlin/tasks/patchremap/RemapPatches.kt create mode 100644 src/main/kotlin/tasks/sourceremap/AbstractParameterVisitor.kt create mode 100644 src/main/kotlin/tasks/sourceremap/PatchParameterRemapper.kt create mode 100644 src/main/kotlin/tasks/sourceremap/RemapSources.kt create mode 100644 src/main/kotlin/tasks/sourceremap/SrgParameterRemapper.kt create mode 100644 src/main/kotlin/tasks/sourceremap/remap.kt rename src/main/kotlin/util/{ArtifactDescriptor.kt => MavenArtifact.kt} (58%) create mode 100644 src/main/kotlin/util/download.kt delete mode 100644 src/main/kotlin/util/etag-util.kt diff --git a/.gitattributes b/.gitattributes index 1978225..2fb638f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,3 @@ gradlew text eol=lf *.bat text eol=crlf *.jar binary - diff --git a/.gitignore b/.gitignore index 527d4fc..7e80851 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,14 @@ -### Intellij ### +# IntelliJ .idea/ +out/ -# File-based project format +*.ipr *.iws *.iml -# IntelliJ -out/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### Eclipse ### - +# Eclipse .metadata +.project bin/ tmp/ *.tmp @@ -30,143 +19,27 @@ local.properties .settings/ .loadpath .recommenders - -# External tool builders .externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" *.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) .factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing .apt_generated/ -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -### Eclipse Patch ### -# Eclipse Core -.project - -# JDT-specific (Eclipse Java Development Tools) -.classpath - -# Annotation Processing -.apt_generated - -.sts4-cache/ - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General +# macOS .DS_Store -.AppleDouble -.LSOverride -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Windows ### +# Windows # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db - -# Dump file -*.stackdump - # Folder config file [Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts *.lnk -### Gradle ### +# Gradle .gradle /build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Cache of project .gradletasknamecache -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.ja diff --git a/build.gradle.kts b/build.gradle.kts index 5b61afd..0382fe4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,6 @@ plugins { idea eclipse maven - kotlin("jvm") version "1.3.70" `kotlin-dsl` id("net.minecrell.licenser") version "0.4.1" id("com.github.johnrengelman.shadow") version "6.0.0" @@ -16,43 +15,36 @@ version = "1.0.0-SNAPSHOT" repositories { mavenLocal() mavenCentral() + jcenter() maven("https://oss.sonatype.org/content/repositories/snapshots/") maven("https://files.minecraftforge.net/maven/") } -val mcInjector: Configuration by configurations.creating - dependencies { - implementation(kotlin("stdlib-jdk8")) - compileOnly(gradleApi()) - compileOnly(gradleKotlinDsl()) + implementation("org.apache.httpcomponents:httpclient:4.5.12") + // Utils implementation("net.sf.opencsv:opencsv:2.3") implementation("com.github.salomonbrys.kotson:kotson:2.5.0") + // ASM for inspection + implementation("org.ow2.asm:asm:8.0.1") + // Cadix implementation("org.cadixdev:lorenz:0.5.4-SNAPSHOT") implementation("org.cadixdev:lorenz-asm:0.5.3") - implementation("org.cadixdev:mercury:0.1.0-SNAPSHOT") implementation("org.cadixdev:atlas:0.2.0") + implementation("org.cadixdev:at:0.1.0-SNAPSHOT") + + implementation("org.cadixdev:mercury:0.1.0-SNAPSHOT") } tasks.withType { kotlinOptions.jvmTarget = "1.8" -} - -val mcinjectorJar by tasks.registering(com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class) { - configurations = listOf(mcInjector) - archiveBaseName.set("mcinjector-shadowed") - archiveVersion.set("") - archiveClassifier.set("") - manifest { - attributes(mapOf("Main-Class" to "de.oceanlabs.mcp.mcinjector.MCInjector")) - } + kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=enable") } tasks.jar { - from(mcinjectorJar) archiveBaseName.set("io.papermc.paperweight.gradle.plugin") } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 28861d273a5d270fd8f65dd74570c17c9c507736..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 50996 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 GfBy%h)2f;P delta 48251 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 N0C65%gJi+L{vUeeYV`mB diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 186b715..be52383 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/license/copyright.txt b/license/copyright.txt index db3a615..ccc3fbe 100644 --- a/license/copyright.txt +++ b/license/copyright.txt @@ -2,7 +2,6 @@ paperweight is a Gradle plugin for the PaperMC project. It uses some code and systems originally from ForgeGradle. Copyright (C) 2020 Kyle Wood -Copyright (C) 2018 Forge Development LLC This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/src/main/kotlin/Paperweight.kt b/src/main/kotlin/Paperweight.kt index 55dbb72..dc32563 100644 --- a/src/main/kotlin/Paperweight.kt +++ b/src/main/kotlin/Paperweight.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,149 +24,264 @@ package io.papermc.paperweight +import com.github.salomonbrys.kotson.array +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.string +import com.google.gson.JsonObject import io.papermc.paperweight.ext.PaperweightExtension import io.papermc.paperweight.tasks.AddMissingSpigotClassMappings -import io.papermc.paperweight.tasks.ApplyAccessTransform import io.papermc.paperweight.tasks.ApplyDiffPatches import io.papermc.paperweight.tasks.ApplyGitPatches +import io.papermc.paperweight.tasks.ApplyMcpPatches +import io.papermc.paperweight.tasks.ApplyPaperPatches +import io.papermc.paperweight.tasks.ApplySourceAt import io.papermc.paperweight.tasks.DecompileVanillaJar +import io.papermc.paperweight.tasks.DownloadMcLibraries +import io.papermc.paperweight.tasks.DownloadMcpFiles +import io.papermc.paperweight.tasks.DownloadMcpTools import io.papermc.paperweight.tasks.DownloadServerJar -import io.papermc.paperweight.tasks.ExtractMcpData -import io.papermc.paperweight.tasks.ExtractMcpMappings +import io.papermc.paperweight.tasks.DownloadSpigotDependencies +import io.papermc.paperweight.tasks.DownloadTask +import io.papermc.paperweight.tasks.ExtractMappings +import io.papermc.paperweight.tasks.ExtractMcp +import io.papermc.paperweight.tasks.Filter import io.papermc.paperweight.tasks.FilterExcludes -import io.papermc.paperweight.tasks.GatherBuildData import io.papermc.paperweight.tasks.GenerateSpigotSrgs import io.papermc.paperweight.tasks.GenerateSrgs -import io.papermc.paperweight.tasks.GetRemoteJsons +import io.papermc.paperweight.tasks.InspectVanillaJar +import io.papermc.paperweight.tasks.Merge +import io.papermc.paperweight.tasks.MergeAccessTransforms import io.papermc.paperweight.tasks.PatchMcpCsv -import io.papermc.paperweight.tasks.RemapSources -import io.papermc.paperweight.tasks.RemapVanillaJarSrg +import io.papermc.paperweight.tasks.RemapAccessTransform +import io.papermc.paperweight.tasks.RemapSpigotAt +import io.papermc.paperweight.tasks.RemapVanillaJarSpigot import io.papermc.paperweight.tasks.RunForgeFlower import io.papermc.paperweight.tasks.RunMcInjector -import io.papermc.paperweight.tasks.SetupMcpDependencies -import io.papermc.paperweight.tasks.SetupSpigotDependencies +import io.papermc.paperweight.tasks.RunSpecialSource +import io.papermc.paperweight.tasks.SetupMcLibraries import io.papermc.paperweight.tasks.WriteLibrariesFile +import io.papermc.paperweight.tasks.patchremap.ApplyAccessTransform +import io.papermc.paperweight.tasks.patchremap.RemapPatches +import io.papermc.paperweight.tasks.sourceremap.RemapSources +import io.papermc.paperweight.util.BuildDataInfo import io.papermc.paperweight.util.Constants import io.papermc.paperweight.util.Git -import io.papermc.paperweight.tasks.RemapSrgSources -import io.papermc.paperweight.tasks.RemapVanillaJarSpigot +import io.papermc.paperweight.util.MinecraftManifest import io.papermc.paperweight.util.cache +import io.papermc.paperweight.util.contents import io.papermc.paperweight.util.ext +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson +import io.papermc.paperweight.util.registering import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Delete import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.bundling.Zip import org.gradle.kotlin.dsl.maven import org.gradle.kotlin.dsl.register -import util.BuildDataInfo import java.io.File class Paperweight : Plugin { override fun apply(target: Project) { - target.extensions.create(Constants.EXTENSION, PaperweightExtension::class.java, target) + target.extensions.create(Constants.EXTENSION, PaperweightExtension::class.java, target.objects, target.layout) - createConfigurations(target) - setupMcpDeps(target) - - createTasks(target) - - target.tasks.register("cleanCache").configure { - destroyables.register(target.cache) - doLast { - target.delete(target.cache) - } + target.tasks.register("cleanCache") { + delete(target.layout.cache) } - } - private fun createConfigurations(project: Project) { - project.repositories.apply { + // Make sure the submodules are initialized + Git(target.projectDir)("submodule", "update", "--init").execute() + + target.repositories.apply { + mavenCentral() + // Both of these are needed for Spigot maven("https://oss.sonatype.org/content/repositories/snapshots/") maven("https://hub.spigotmc.org/nexus/content/groups/public/") - maven { - name = "forge" - url = project.uri(Constants.FORGE_MAVEN_URL) - metadataSources { - artifact() - } - } - mavenCentral() - maven { - name = "minecraft" - url = project.uri(Constants.MC_LIBRARY_URL) - } } - project.configurations.register(Constants.MCP_MAPPINGS_CONFIG) - project.configurations.register(Constants.MCP_DATA_CONFIG) - project.configurations.register(Constants.SPIGOT_DEP_CONFIG) - project.configurations.create(Constants.MINECRAFT_DEP_CONFIG) - project.configurations.register(Constants.FORGE_FLOWER_CONFIG) - project.configurations.create(Constants.MCINJECT_CONFIG) + target.createTasks() } - private fun setupMcpDeps(project: Project) { - project.dependencies.add(Constants.MCP_DATA_CONFIG, project.provider { - mapOf( - "group" to "de.oceanlabs.mcp", - "name" to "mcp_config", - "version" to "${project.ext.mcpMinecraftVersion.get()}-${project.ext.mcpVersion.get()}", - "ext" to "zip" - ) - }) + private fun Project.createTasks() { + val extension = ext - project.dependencies.add(Constants.MCP_MAPPINGS_CONFIG, project.provider { - mapOf( - "group" to "de.oceanlabs.mcp", - "name" to "mcp_${project.ext.mcpMappingsChannel.get()}", - "version" to project.ext.mcpMappingsVersion.get(), - "ext" to "zip" - ) - }) + val initialTasks = createInitialTasks() + val generalTasks = createGeneralTasks() + val mcpTasks = createMcpTasks(initialTasks, generalTasks) + val spigotTasks = createSpigotTasks(initialTasks, generalTasks, mcpTasks) + + createPatchRemapTasks(initialTasks, generalTasks, mcpTasks, spigotTasks) + + val applySourceAt by tasks.registering { + inputZip.set(mcpTasks.applyMcpPatches.flatMap { it.outputZip }) + vanillaJar.set(generalTasks.downloadServerJar.flatMap { it.outputJar }) + vanillaRemappedSrgJar.set(mcpTasks.remapVanillaJarSrg.flatMap { it.outputJar }) + atFile.set(spigotTasks.mergeGeneratedAts.flatMap { it.outputFile }) + } + + val mergeRemappedSources by tasks.registering { + inputJars.add(spigotTasks.remapSpigotSources.flatMap { it.outputZip }) + inputJars.add(applySourceAt.flatMap { it.outputZip }) + } + + val patchPaperApi by tasks.registering { + branch.set("HEAD") + upstreamBranch.set("upstream") + upstream.set(spigotTasks.patchSpigotApi.flatMap { it.outputDir }) + patchDir.set(extension.paper.spigotApiPatchDir) + printOutput.set(true) + + outputDir.set(extension.paper.paperApiDir) + } + + val patchPaperServer by tasks.registering { + patchDir.set(extension.paper.spigotServerPatchDir) + remappedSource.set(mergeRemappedSources.flatMap { it.outputJar }) + templateGitIgnore.set(layout.projectDirectory.file(".gitignore")) + + outputDir.set(extension.paper.paperServerDir) + } + + val patchPaper by tasks.registering { + dependsOn(patchPaperApi, patchPaperServer) + } + + /* + * Not bothering mapping away from SRG until everything is stable under SRG + * Moving off of SRG will make things a lot more fragile + val remapSrgSourcesSpigotVanilla by tasks.registering { + inputZips.add(ZipTarget.base(applyMcpPatches.flatMap { outputZip })) + methodsCsv.set(mcpRewrites.flatMap { methodsCsv }) + fieldsCsv.set(mcpRewrites.flatMap { fieldsCsv }) + paramsCsv.set(mcpRewrites.flatMap { paramsCsv }) + } + */ } - // Types are specified in this method to hopefully improve editor performance a little, though I don't think it helps - private fun createTasks(project: Project) { - val cache: File = project.cache - val extension: PaperweightExtension = project.ext + // Shared task containers + data class InitialTasks( + val setupMcLibraries: TaskProvider, + val extractMcp: Provider, + val mcpMappings: Provider, + val downloadMcpTools: TaskProvider + ) - val initGitSubmodules: TaskProvider = project.tasks.register("initGitSubmodules") { - outputs.upToDateWhen { false } - doLast { - Git(project.projectDir)("submodule", "update", "--init").execute() - } - } - val gatherBuildData: TaskProvider = project.tasks.register("gatherBuildData") { - dependsOn(initGitSubmodules) - buildDataInfoFile.set(extension.craftBukkit.buildDataInfo) - } - val buildDataInfo: Provider = gatherBuildData.flatMap { it.buildDataInfo } + data class GeneralTasks( + val buildDataInfo: Provider, + val downloadServerJar: TaskProvider, + val filterVanillaJar: TaskProvider + ) - val extractMcpData: TaskProvider = project.tasks.register("extractMcpData") { - config.set(Constants.MCP_DATA_CONFIG) + data class McpTasks( + val generateSrgs: TaskProvider, + val remapVanillaJarSrg: TaskProvider, + val applyMcpPatches: TaskProvider + ) + + data class SpigotTasks( + val generateSpigotSrgs: TaskProvider, + val decompileVanillaJarSpigot: TaskProvider, + val patchSpigotApi: TaskProvider, + val patchSpigotServer: TaskProvider, + val remapSpigotSources: TaskProvider, + val mergeGeneratedAts: TaskProvider + ) + + private fun Project.createInitialTasks(): InitialTasks { + val cache: File = layout.cache + val extension: PaperweightExtension = ext + + val downloadMcManifest by tasks.registering { + url.set(Constants.MC_MANIFEST_URL) + outputFile.set(cache.resolve(Constants.MC_MANIFEST)) + } + + val mcManifest = downloadMcManifest.flatMap { it.outputFile }.map { gson.fromJson(it) } + + val downloadMcVersionManifest by tasks.registering { + url.set(mcManifest.zip(extension.minecraftVersion) { manifest, version -> + manifest.versions.first { it.id == version }.url + }) + outputFile.set(cache.resolve(Constants.VERSION_JSON)) + } + + val versionManifest = downloadMcVersionManifest.flatMap { it.outputFile }.map { gson.fromJson(it) } + + val setupMcLibraries by tasks.registering { + dependencies.set(versionManifest.map { version -> + version["libraries"].array.map { library -> + library["name"].string + }.filter { !it.contains("lwjgl") } // we don't need these on the server + }) + outputFile.set(cache.resolve(Constants.MC_LIBRARIES)) + } + + val downloadMcpFiles by tasks.registering { + mcpMinecraftVersion.set(extension.mcpMinecraftVersion) + mcpConfigVersion.set(extension.mcpConfigVersion) + mcpMappingsChannel.set(extension.mcpMappingsChannel) + mcpMappingsVersion.set(extension.mcpMappingsVersion) + + configZip.set(cache.resolve(Constants.MCP_ZIPS_PATH).resolve("McpConfig.zip")) + mappingsZip.set(cache.resolve(Constants.MCP_ZIPS_PATH).resolve("McpMappings.zip")) + } + + val extractMcpConfig by tasks.registering { + inputFile.set(downloadMcpFiles.flatMap { it.configZip }) outputDir.set(cache.resolve(Constants.MCP_DATA_DIR)) } - - val setupMcpDependencies: TaskProvider = project.tasks.register("setupMcpDependencies") { - configFile.set(extractMcpData.flatMap { it.configJson }) - forgeFlowerConfig.set(Constants.FORGE_FLOWER_CONFIG) - mcInjectorConfig.set(Constants.MCINJECT_CONFIG) - } - - val extractMcpMappings: TaskProvider = project.tasks.register("extractMcpMappings") { - config.set(Constants.MCP_MAPPINGS_CONFIG) + val extractMcpMappings by tasks.registering { + inputFile.set(downloadMcpFiles.flatMap { it.mappingsZip }) outputDir.set(cache.resolve(Constants.MCP_MAPPINGS_DIR)) } - val getRemoteJsons: TaskProvider = project.tasks.register("getRemoteJsons") { - config.set(Constants.MINECRAFT_DEP_CONFIG) + val downloadMcpTools by tasks.registering { + configFile.set(extractMcpConfig.flatMap { it.configFile }) + + val toolsPath = cache.resolve(Constants.MCP_TOOLS_PATH) + forgeFlowerFile.set(toolsPath.resolve("ForgeFlower.jar")) + mcInjectorFile.set(toolsPath.resolve("McInjector.jar")) + specialSourceFile.set(toolsPath.resolve("SpecialSource.jar")) } - val mcpRewrites: TaskProvider = project.tasks.register("mcpRewrites") { - fieldsCsv.set(extractMcpMappings.flatMap { it.fieldsCsv }) - methodsCsv.set(extractMcpMappings.flatMap { it.methodsCsv }) - paramsCsv.set(extractMcpMappings.flatMap { it.paramsCsv }) + return InitialTasks( + setupMcLibraries, + extractMcpConfig, + extractMcpMappings, + downloadMcpTools + ) + } + + private fun Project.createGeneralTasks(): GeneralTasks { + val buildDataInfo: Provider = contents(ext.craftBukkit.buildDataInfo) { + gson.fromJson(it) + } + + val downloadServerJar by tasks.registering { + downloadUrl.set(buildDataInfo.map { it.serverUrl }) + hash.set(buildDataInfo.map { it.minecraftHash }) + } + + val filterVanillaJar by tasks.registering { + inputJar.set(downloadServerJar.flatMap { it.outputJar }) + includes.set(listOf("/*.class", "/net/minecraft/**")) + } + + return GeneralTasks(buildDataInfo, downloadServerJar, filterVanillaJar) + } + + private fun Project.createMcpTasks(initialTasks: InitialTasks, generalTasks: GeneralTasks): McpTasks { + val filterVanillaJar: TaskProvider = generalTasks.filterVanillaJar + val cache: File = layout.cache + val extension: PaperweightExtension = ext + + val mcpRewrites by tasks.registering { + fieldsCsv.set(initialTasks.mcpMappings.flatMap { it.fieldsCsv }) + methodsCsv.set(initialTasks.mcpMappings.flatMap { it.methodsCsv }) + paramsCsv.set(initialTasks.mcpMappings.flatMap { it.paramsCsv }) changesFile.set(extension.paper.mcpRewritesFile) paperFieldCsv.set(cache.resolve(Constants.PAPER_FIELDS_CSV)) @@ -175,8 +289,9 @@ class Paperweight : Plugin { paperParamCsv.set(cache.resolve(Constants.PAPER_PARAMS_CSV)) } - val generateSrgs: TaskProvider = project.tasks.register("generateSrgs") { - configFile.set(extractMcpData.flatMap { it.configJson }) + val generateSrgs by tasks.registering { + inSrg.set(initialTasks.extractMcp.flatMap { it.mappings }) + methodsCsv.set(mcpRewrites.flatMap { it.paperMethodCsv }) fieldsCsv.set(mcpRewrites.flatMap { it.paperFieldCsv }) extraNotchSrgMappings.set(extension.paper.extraNotchSrgMappings) @@ -189,20 +304,78 @@ class Paperweight : Plugin { mcpToSrg.set(cache.resolve(Constants.MCP_TO_SRG)) } - val addMissingSpigotClassMappings: TaskProvider = project.tasks.register("addMissingSpigotClassMappings") { + val remapVanillaJarSrg by tasks.registering { + inputJar.set(filterVanillaJar.flatMap { it.outputJar }) + mappings.set(generateSrgs.flatMap { it.notchToSrg }) + + executable.set(initialTasks.downloadMcpTools.flatMap { it.specialSourceFile }) + configFile.set(initialTasks.extractMcp.flatMap { it.configFile }) + } + + val injectVanillaJarSrg by tasks.registering { + executable.set(initialTasks.downloadMcpTools.flatMap { it.mcInjectorFile }) + configFile.set(initialTasks.extractMcp.flatMap { it.configFile }) + + exceptions.set(initialTasks.extractMcp.flatMap { it.exceptions }) + access.set(initialTasks.extractMcp.flatMap { it.access }) + constructors.set(initialTasks.extractMcp.flatMap { it.constructors }) + + inputJar.set(remapVanillaJarSrg.flatMap { it.outputJar }) + } + + val downloadMcLibraries by tasks.registering { + mcLibrariesFile.set(initialTasks.setupMcLibraries.flatMap { it.outputFile }) + mcRepo.set(Constants.MC_LIBRARY_URL) + outputDir.set(cache.resolve(Constants.MINECRAFT_JARS_PATH)) + } + + val writeLibrariesFile by tasks.registering { + libraries.set(downloadMcLibraries.flatMap { it.outputDir }) + } + + val decompileVanillaJarSrg by tasks.registering { + executable.set(initialTasks.downloadMcpTools.flatMap { it.forgeFlowerFile }) + configFile.set(initialTasks.extractMcp.flatMap { it.configFile }) + + inputJar.set(injectVanillaJarSrg.flatMap { it.outputJar }) + libraries.set(writeLibrariesFile.flatMap { it.outputFile }) + } + + val applyMcpPatches by tasks.registering { + inputZip.set(decompileVanillaJarSrg.flatMap { it.outputJar }) + serverPatchDir.set(initialTasks.extractMcp.flatMap { it.patchDir }) + configFile.set(cache.resolve(Constants.MCP_CONFIG_JSON)) + } + + return McpTasks(generateSrgs, remapVanillaJarSrg, applyMcpPatches) + } + + private fun Project.createSpigotTasks(initialTasks: InitialTasks, generalTasks: GeneralTasks, mcpTasks: McpTasks): SpigotTasks { + val cache: File = layout.cache + val extension: PaperweightExtension = ext + + val (buildDataInfo, downloadServerJar, filterVanillaJar) = generalTasks + val (generateSrgs, _, _) = mcpTasks + + val addMissingSpigotClassMappings by tasks.registering { classSrg.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.classMappings })) memberSrg.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.memberMappings })) missingClassEntriesSrg.set(extension.paper.missingClassEntriesSrgFile) missingMemberEntriesSrg.set(extension.paper.missingMemberEntriesSrgFile) } - val generateSpigotSrgs: TaskProvider = project.tasks.register("generateSpigotSrgs") { + val inspectVanillaJar by tasks.registering { + inputJar.set(downloadServerJar.flatMap { it.outputJar }) + } + + val generateSpigotSrgs by tasks.registering { notchToSrg.set(generateSrgs.flatMap { it.notchToSrg }) srgToMcp.set(generateSrgs.flatMap { it.srgToMcp }) classMappings.set(addMissingSpigotClassMappings.flatMap { it.outputClassSrg }) memberMappings.set(addMissingSpigotClassMappings.flatMap { it.outputMemberSrg }) packageMappings.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.packageMappings })) extraSpigotSrgMappings.set(extension.paper.extraSpigotSrgMappings) + loggerFields.set(inspectVanillaJar.flatMap { it.outputFile }) spigotToSrg.set(cache.resolve(Constants.SPIGOT_TO_SRG)) spigotToMcp.set(cache.resolve(Constants.SPIGOT_TO_MCP)) @@ -212,25 +385,8 @@ class Paperweight : Plugin { notchToSpigot.set(cache.resolve(Constants.NOTCH_TO_SPIGOT)) } - val downloadServerJar: TaskProvider = project.tasks.register("downloadServerJar") { - dependsOn(gatherBuildData) - downloadUrl.set(buildDataInfo.map { it.serverUrl }) - hash.set(buildDataInfo.map { it.minecraftHash }) - } - - val filterVanillaJar: TaskProvider = project.tasks.register("filterVanillaJar") { - dependsOn(downloadServerJar) // the from() block below doesn't set up this dependency - archiveFileName.set("filterVanillaJar.jar") - destinationDirectory.set(cache.resolve(Constants.TASK_CACHE)) - - from(project.zipTree(downloadServerJar.flatMap { it.outputJar })) { - include("/*.class") - include("/net/minecraft/**") - } - } - - val remapVanillaJar: TaskProvider = project.tasks.register("remapVanillaJar") { - inputJar.set(project.layout.file(filterVanillaJar.map { it.outputs.files.singleFile })) + val remapVanillaJarSpigot by tasks.registering { + inputJar.set(filterVanillaJar.flatMap { it.outputJar }) classMappings.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.classMappings })) memberMappings.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.memberMappings })) packageMappings.set(extension.craftBukkit.mappingsDir.file(buildDataInfo.map { it.packageMappings })) @@ -246,18 +402,18 @@ class Paperweight : Plugin { finalMapCommand.set(buildDataInfo.map { it.finalMapCommand }) } - val removeSpigotExcludes: TaskProvider = project.tasks.register("removeSpigotExcludes") { - inputZip.set(remapVanillaJar.flatMap { it.outputJar }) + val removeSpigotExcludes by tasks.registering { + inputZip.set(remapVanillaJarSpigot.flatMap { it.outputJar }) excludesFile.set(extension.craftBukkit.excludesFile) } - val decompileVanillaJarSpigot: TaskProvider = project.tasks.register("decompileVanillaJarSpigot") { + val decompileVanillaJarSpigot by tasks.registering { inputJar.set(removeSpigotExcludes.flatMap { it.outputZip }) fernFlowerJar.set(extension.craftBukkit.fernFlowerJar) decompileCommand.set(buildDataInfo.map { it.decompileCommand }) } - val patchCraftBukkit: TaskProvider = project.tasks.register("patchCraftBukkit") { + val patchCraftBukkit by tasks.registering { sourceJar.set(decompileVanillaJarSpigot.flatMap { it.outputJar }) sourceBasePath.set("net/minecraft/server") branch.set("patched") @@ -266,7 +422,7 @@ class Paperweight : Plugin { outputDir.set(extension.craftBukkit.craftBukkitDir) } - val patchSpigotApi: TaskProvider = project.tasks.register("patchSpigotApi") { + val patchSpigotApi by tasks.registering { branch.set("HEAD") upstreamBranch.set("upstream") upstream.set(extension.craftBukkit.bukkitDir) @@ -275,7 +431,7 @@ class Paperweight : Plugin { outputDir.set(extension.spigot.spigotApiDir) } - val patchSpigotServer: TaskProvider = project.tasks.register("patchSpigotServer") { + val patchSpigotServer by tasks.registering { branch.set(patchCraftBukkit.flatMap { it.branch }) upstreamBranch.set("upstream") upstream.set(patchCraftBukkit.flatMap { it.outputDir }) @@ -284,78 +440,98 @@ class Paperweight : Plugin { outputDir.set(extension.spigot.spigotServerDir) } - val patchSpigot: TaskProvider = project.tasks.register("patchSpigot") { + val patchSpigot by tasks.registering { dependsOn(patchSpigotApi, patchSpigotServer) } - val setupSpigotDependencies: TaskProvider = project.tasks.register("setupSpigotDependencies") { + val downloadSpigotDependencies by tasks.registering { dependsOn(patchSpigot) - spigotApi.set(patchSpigotApi.flatMap { it.outputDir }) - spigotServer.set(patchSpigotServer.flatMap { it.outputDir }) - configurationName.set(Constants.SPIGOT_DEP_CONFIG) + apiPom.set(patchSpigotApi.flatMap { it.outputDir.file("pom.xml") }) + serverPom.set(patchSpigotServer.flatMap { it.outputDir.file("pom.xml") }) + apiOutputDir.set(cache.resolve(Constants.SPIGOT_API_JARS_PATH)) + serverOutputDir.set(cache.resolve(Constants.SPIGOT_SERVER_JARS_PATH)) } - val remapSpigotJarSrg: TaskProvider = project.tasks.register("remapSpigotJarSrg") { - // Basing off of the Spigot jar lets us inherit the AT modifications Spigot does before decompile - inputJar.set(remapVanillaJar.flatMap { it.outputJar }) - mappings.set(generateSpigotSrgs.flatMap { it.spigotToSrg }) + val remapSpigotAt by tasks.registering { + inputJar.set(remapVanillaJarSpigot.flatMap { it.outputJar }) + mapping.set(generateSpigotSrgs.flatMap { it.spigotToSrg }) + spigotAt.set(extension.craftBukkit.atFile) } - val remapSpigotSources: TaskProvider = project.tasks.register("remapSpigotSources") { - dependsOn(setupSpigotDependencies) + val remapSpigotSources by tasks.registering { spigotServerDir.set(patchSpigotServer.flatMap { it.outputDir }) spigotApiDir.set(patchSpigotApi.flatMap { it.outputDir }) mappings.set(generateSpigotSrgs.flatMap { it.spigotToSrg }) vanillaJar.set(downloadServerJar.flatMap { it.outputJar }) vanillaRemappedSpigotJar.set(removeSpigotExcludes.flatMap { it.outputZip }) - vanillaRemappedSrgJar.set(remapSpigotJarSrg.flatMap { it.outputJar }) - configuration.set(setupSpigotDependencies.flatMap { it.configurationName }) - configFile.set(extractMcpData.flatMap { it.configJson }) + spigotApiDeps.set(downloadSpigotDependencies.flatMap { it.apiOutputDir }) + spigotServerDeps.set(downloadSpigotDependencies.flatMap { it.serverOutputDir }) + constructors.set(initialTasks.extractMcp.flatMap { it.constructors }) } - val fixVanillaJarAccess: TaskProvider = project.tasks.register("fixVanillaJarAccess") { - inputJar.set(remapSpigotJarSrg.flatMap { it.outputJar }) - atFile.set(remapSpigotSources.flatMap { it.generatedAt }) - mapping.set(generateSpigotSrgs.flatMap { it.spigotToSrg }) + val remapGeneratedAt by tasks.registering { + inputFile.set(remapSpigotSources.flatMap { it.generatedAt }) + mappings.set(generateSpigotSrgs.flatMap { it.spigotToSrg }) } - val remapSrgSourcesSpigot: TaskProvider = project.tasks.register("remapSrgSourcesSpigot") { - inputZip.set(remapSpigotSources.flatMap { it.outputZip }) - methodsCsv.set(mcpRewrites.flatMap { it.methodsCsv }) - fieldsCsv.set(mcpRewrites.flatMap { it.fieldsCsv }) - paramsCsv.set(mcpRewrites.flatMap { it.paramsCsv }) + val mergeGeneratedAts by tasks.registering { + inputFiles.add(remapGeneratedAt.flatMap { it.outputFile }) + inputFiles.add(remapSpigotAt.flatMap { it.outputFile }) } - val injectVanillaJarForge: TaskProvider = project.tasks.register("injectVanillaJarForge") { - dependsOn(setupMcpDependencies) - configuration.set(setupMcpDependencies.flatMap { it.mcInjectorConfig }) - inputJar.set(remapSpigotJarSrg.flatMap { it.outputJar }) - configFile.set(extractMcpData.flatMap { it.configJson }) + return SpigotTasks( + generateSpigotSrgs, + decompileVanillaJarSpigot, + patchSpigotApi, + patchSpigotServer, + remapSpigotSources, + mergeGeneratedAts + ) + } + + private fun Project.createPatchRemapTasks( + initialTasks: InitialTasks, + generalTasks: GeneralTasks, + mcpTasks: McpTasks, + spigotTasks: SpigotTasks + ) { + val extension: PaperweightExtension = ext + + /* + * I don't remember what this is supposed to be for tbh + val patchPaperServerForPatchRemap by tasks.registering { + patchDir.set(extension.paper.spigotServerPatchDir) + remappedSource.set(spigotTasks.remapSpigotSources.flatMap { it.outputZip }) + + outputDir.set(cache.resolve("patch-paper-server-for-remap")) + } + */ + + val applyVanillaSrgAt by tasks.registering { + inputJar.set(mcpTasks.remapVanillaJarSrg.flatMap { it.outputJar }) + atFile.set(spigotTasks.mergeGeneratedAts.flatMap { it.outputFile }) } - val writeLibrariesFile: TaskProvider = project.tasks.register("writeLibrariesFile") { - dependsOn(getRemoteJsons) - config.set(getRemoteJsons.flatMap { it.config }) - } + val remapPatches by tasks.registering { + inputPatchDir.set(extension.paper.unmappedSpigotServerPatchDir) + sourceJar.set(spigotTasks.remapSpigotSources.flatMap { it.outputZip }) + apiPatchDir.set(extension.paper.spigotApiPatchDir) - val decompileVanillaJarForge: TaskProvider = project.tasks.register("decompileVanillaJarForge") { - dependsOn(setupMcpDependencies) - configuration.set(setupMcpDependencies.flatMap { it.forgeFlowerConfig }) - inputJar.set(injectVanillaJarForge.flatMap { it.outputJar }) - libraries.set(writeLibrariesFile.flatMap { it.outputFile }) - configFile.set(extractMcpData.flatMap { it.configJson }) - } + mappingsFile.set(spigotTasks.generateSpigotSrgs.flatMap { it.spigotToSrg }) -// val applyMcpPatches: TaskProvider = project.tasks.register("applyMcpPatches") { -// inputZips.add(ZipTarget.base(decompileVanillaJarForge.flatMap { it.outputJar })) -// serverPatchDir.set(extractMcpData.flatMap { it.patches }) -// } -// -// val remapSrgSourcesSpigotVanilla: TaskProvider = project.tasks.register("remapSrgSourcesSpigotVanilla") { -// inputZips.add(ZipTarget.base(applyMcpPatches.flatMap { outputZip })) -// methodsCsv.set(mcpRewrites.flatMap { methodsCsv }) -// fieldsCsv.set(mcpRewrites.flatMap { fieldsCsv }) -// paramsCsv.set(mcpRewrites.flatMap { paramsCsv }) -// } + // Pull in as many jars as possible to reduce the possibility of type bindings not resolving + classpathJars.add(generalTasks.downloadServerJar.flatMap { it.outputJar }) + classpathJars.add(spigotTasks.remapSpigotSources.flatMap { it.vanillaRemappedSpigotJar }) + classpathJars.add(applyVanillaSrgAt.flatMap { it.outputJar }) + + spigotApiDir.set(spigotTasks.patchSpigotApi.flatMap { it.outputDir }) + spigotServerDir.set(spigotTasks.patchSpigotServer.flatMap { it.outputDir }) + spigotDecompJar.set(spigotTasks.decompileVanillaJarSpigot.flatMap { it.outputJar }) + constructors.set(initialTasks.extractMcp.flatMap { it.constructors }) + + parameterNames.set(spigotTasks.remapSpigotSources.flatMap { it.parameterNames }) + + outputPatchDir.set(extension.paper.remappedSpigotServerPatchDir) + } } } diff --git a/src/main/kotlin/PaperweightException.kt b/src/main/kotlin/PaperweightException.kt index d70d79e..59f2660 100644 --- a/src/main/kotlin/PaperweightException.kt +++ b/src/main/kotlin/PaperweightException.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/src/main/kotlin/ext/CraftBukkitExtension.kt b/src/main/kotlin/ext/CraftBukkitExtension.kt index 885facc..c2b43b8 100644 --- a/src/main/kotlin/ext/CraftBukkitExtension.kt +++ b/src/main/kotlin/ext/CraftBukkitExtension.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,18 +22,35 @@ package io.papermc.paperweight.ext -import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory -open class CraftBukkitExtension(project: Project) { - val bukkitDir: DirectoryProperty = project.dirWithDefault("work/Bukkit") - var craftBukkitDir: DirectoryProperty = project.dirWithDefault("work/CraftBukkit") - var patchDir: DirectoryProperty = project.dirWithDefault("work/CraftBukkit/nms-patches") - var mappingsDir: DirectoryProperty = project.dirWithDefault("work/BuildData/mappings") - val excludesFile: RegularFileProperty = project.fileWithDefault("work/BuildData/mappings/bukkit-1.16.1.exclude") - var buildDataInfo: RegularFileProperty = project.fileWithDefault("work/BuildData/info.json") - var fernFlowerJar: RegularFileProperty = project.fileWithDefault("work/BuildData/bin/fernflower.jar") - var specialSourceJar: RegularFileProperty = project.fileWithDefault("work/BuildData/bin/SpecialSource.jar") - var specialSource2Jar: RegularFileProperty = project.fileWithDefault("work/BuildData/bin/SpecialSource-2.jar") +open class CraftBukkitExtension(objects: ObjectFactory, workDir: DirectoryProperty) { + + val bukkitDir: DirectoryProperty = objects.dirFrom(workDir, "Bukkit") + val craftBukkitDir: DirectoryProperty = objects.dirFrom(workDir, "CraftBukkit") + val patchDir: DirectoryProperty = objects.dirFrom(craftBukkitDir, "nms-patches") + @Suppress("MemberVisibilityCanBePrivate") + val buildDataDir: DirectoryProperty = objects.dirFrom(workDir, "BuildData") + val mappingsDir: DirectoryProperty = objects.dirFrom(buildDataDir, "mappings") + val excludesFile: RegularFileProperty = objects.bukkitFileFrom(mappingsDir, "exclude") + val atFile: RegularFileProperty = objects.bukkitFileFrom(mappingsDir, "at") + val buildDataInfo: RegularFileProperty = objects.fileFrom(buildDataDir, "info.json") + @Suppress("MemberVisibilityCanBePrivate") + val buildDataBinDir: DirectoryProperty = objects.dirFrom(buildDataDir, "bin") + val fernFlowerJar: RegularFileProperty = objects.fileFrom(buildDataBinDir, "fernflower.jar") + val specialSourceJar: RegularFileProperty = objects.fileFrom(buildDataBinDir, "SpecialSource.jar") + val specialSource2Jar: RegularFileProperty = objects.fileFrom(buildDataBinDir, "SpecialSource-2.jar") + + private fun ObjectFactory.bukkitFileFrom(base: DirectoryProperty, extension: String): RegularFileProperty = + fileProperty().convention(base.flatMap { dir -> + val file = dir.asFile.listFiles()?.firstOrNull { it.name.endsWith(extension) } + if (file != null) { + mappingsDir.file(file.name) + } else { + // empty + fileProperty() + } + }) } diff --git a/src/main/kotlin/ext/PaperExtension.kt b/src/main/kotlin/ext/PaperExtension.kt index 35a2fde..2d39f69 100644 --- a/src/main/kotlin/ext/PaperExtension.kt +++ b/src/main/kotlin/ext/PaperExtension.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,25 +22,28 @@ package io.papermc.paperweight.ext -import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.kotlin.dsl.property +import org.gradle.api.model.ObjectFactory -open class PaperExtension(project: Project) { - val spigotApiPatchDir: Property = project.objects.property().convention("Spigot-API-Patches") - val spigotServerPatchDir: Property = project.objects.property().convention("Spigot-Server-Patches") - val paperApiDir: Property = project.objects.property().convention("Paper-API") - val paperServerDir: Property = project.objects.property().convention("Paper-Server") +open class PaperExtension(objects: ObjectFactory, layout: ProjectLayout) { + @Suppress("MemberVisibilityCanBePrivate") + val baseTargetDir: DirectoryProperty = objects.dirWithDefault(layout, ".") + val spigotApiPatchDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Spigot-API-Patches") + val spigotServerPatchDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Spigot-Server-Patches") + val remappedSpigotServerPatchDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Spigot-Server-Patches-Remapped") + val unmappedSpigotServerPatchDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Spigot-Server-Patches-Unmapped") + val paperApiDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Paper-API") + val paperServerDir: DirectoryProperty = objects.dirFrom(baseTargetDir, "Paper-Server") - val mcpRewritesFile: RegularFileProperty = project.fileWithDefault("mcp/mcp-rewrites.txt") - val missingClassEntriesSrgFile: RegularFileProperty = project.fileWithDefault("mcp/missing-spigot-class-mappings.csrg") - val missingMemberEntriesSrgFile: RegularFileProperty = project.fileWithDefault("mcp/missing-spigot-member-mappings.csrg") - val extraNotchSrgMappings: RegularFileProperty = project.fileWithDefault("mcp/extra-notch-srg.tsrg") - val extraSpigotSrgMappings: RegularFileProperty = project.fileWithDefault("mcp/extra-spigot-srg.tsrg") - val preMapSrgFile: RegularFileProperty = project.fileWithDefault("mcp/paper.srg") - val removeListFile: RegularFileProperty = project.fileWithDefault("mcp/remove-list.txt") - val memberMoveListFile: RegularFileProperty = project.fileWithDefault("mcp/member-moves.txt") + @Suppress("MemberVisibilityCanBePrivate") + val mcpDir: DirectoryProperty = objects.dirWithDefault(layout, "mcp") + val mcpRewritesFile: RegularFileProperty = objects.fileFrom(mcpDir, "mcp-rewrites.txt") + val missingClassEntriesSrgFile: RegularFileProperty = objects.fileFrom(mcpDir, "missing-spigot-class-mappings.csrg") + val missingMemberEntriesSrgFile: RegularFileProperty = objects.fileFrom(mcpDir, "missing-spigot-member-mappings.csrg") + val extraNotchSrgMappings: RegularFileProperty = objects.fileFrom(mcpDir, "extra-notch-srg.tsrg") + val extraSpigotSrgMappings: RegularFileProperty = objects.fileFrom(mcpDir, "extra-spigot-srg.tsrg") init { spigotApiPatchDir.disallowUnsafeRead() @@ -50,8 +52,5 @@ open class PaperExtension(project: Project) { paperServerDir.disallowUnsafeRead() mcpRewritesFile.disallowUnsafeRead() - preMapSrgFile.disallowUnsafeRead() - removeListFile.disallowUnsafeRead() - memberMoveListFile.disallowUnsafeRead() } } diff --git a/src/main/kotlin/ext/PaperweightExtension.kt b/src/main/kotlin/ext/PaperweightExtension.kt index 34a7840..b70658c 100644 --- a/src/main/kotlin/ext/PaperweightExtension.kt +++ b/src/main/kotlin/ext/PaperweightExtension.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,26 +23,33 @@ package io.papermc.paperweight.ext import org.gradle.api.Action -import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.kotlin.dsl.property -open class PaperweightExtension(project: Project) { +open class PaperweightExtension(objects: ObjectFactory, layout: ProjectLayout) { - val minecraftVersion: Property = project.objects.property() - val mcpMinecraftVersion: Property = project.objects.property().convention(minecraftVersion) - val mcpVersion: Property = project.objects.property() - val mcpMappingsChannel: Property = project.objects.property() - val mcpMappingsVersion: Property = project.objects.property() + @Suppress("MemberVisibilityCanBePrivate") + val workDir: DirectoryProperty = objects.dirWithDefault(layout, "work") - val craftBukkit = CraftBukkitExtension(project) - val spigot = SpigotExtension(project) - val paper = PaperExtension(project) + val minecraftVersion: Property = objects.property() + val mcpMinecraftVersion: Property = objects.property().convention(minecraftVersion) + val mcpConfigVersion: Property = objects.property() + val mcpMappingsChannel: Property = objects.property() + val mcpMappingsVersion: Property = objects.property() + + val mcpConfigFile: RegularFileProperty = objects.fileProperty().convention(null) + + val craftBukkit = CraftBukkitExtension(objects, workDir) + val spigot = SpigotExtension(objects, workDir) + val paper = PaperExtension(objects, layout) init { minecraftVersion.disallowUnsafeRead() mcpMinecraftVersion.disallowUnsafeRead() - mcpVersion.disallowUnsafeRead() mcpMappingsChannel.disallowUnsafeRead() mcpMappingsVersion.disallowUnsafeRead() } diff --git a/src/main/kotlin/ext/SpigotExtension.kt b/src/main/kotlin/ext/SpigotExtension.kt index a1b6e1a..797241f 100644 --- a/src/main/kotlin/ext/SpigotExtension.kt +++ b/src/main/kotlin/ext/SpigotExtension.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,15 +22,15 @@ package io.papermc.paperweight.ext -import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory -open class SpigotExtension(project: Project) { - var spigotDir: DirectoryProperty = project.dirWithDefault("work/Spigot") - var spigotApiDir: DirectoryProperty = project.dirWithDefault("work/Spigot/Spigot-API") - var spigotServerDir: DirectoryProperty = project.dirWithDefault("work/Spigot/Spigot-Server") - var bukkitPatchDir: DirectoryProperty = project.dirWithDefault("work/Spigot/Bukkit-Patches") - var craftBukkitPatchDir: DirectoryProperty = project.dirWithDefault("work/Spigot/CraftBukkit-Patches") +open class SpigotExtension(objects: ObjectFactory, workDir: DirectoryProperty) { + var spigotDir: DirectoryProperty = objects.dirFrom(workDir, "Spigot") + var spigotApiDir: DirectoryProperty = objects.dirFrom(spigotDir, "Spigot-API") + var spigotServerDir: DirectoryProperty = objects.dirFrom(spigotDir, "Spigot-Server") + var bukkitPatchDir: DirectoryProperty = objects.dirFrom(spigotDir, "Bukkit-Patches") + var craftBukkitPatchDir: DirectoryProperty = objects.dirFrom(spigotDir, "CraftBukkit-Patches") init { spigotDir.disallowUnsafeRead() diff --git a/src/main/kotlin/ext/extensions.kt b/src/main/kotlin/ext/extensions.kt index fa14b2f..6ab0b5c 100644 --- a/src/main/kotlin/ext/extensions.kt +++ b/src/main/kotlin/ext/extensions.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,12 +22,16 @@ package io.papermc.paperweight.ext -import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory -fun Project.dirWithDefault(path: String): DirectoryProperty = - project.objects.directoryProperty().convention(layout.dir(provider { file(path) })) +fun ObjectFactory.dirWithDefault(layout: ProjectLayout, path: String): DirectoryProperty = + directoryProperty().convention(layout.projectDirectory.dir(path)) -fun Project.fileWithDefault(path: String): RegularFileProperty = - project.objects.fileProperty().convention(layout.file(provider { file(path) })) +fun ObjectFactory.dirFrom(base: DirectoryProperty, name: String): DirectoryProperty = + directoryProperty().convention(base.dir(name)) + +fun ObjectFactory.fileFrom(base: DirectoryProperty, name: String): RegularFileProperty = + fileProperty().convention(base.file(name)) diff --git a/src/main/kotlin/tasks/AddMissingSpigotClassMappings.kt b/src/main/kotlin/tasks/AddMissingSpigotClassMappings.kt index a25dd89..d5fa038 100644 --- a/src/main/kotlin/tasks/AddMissingSpigotClassMappings.kt +++ b/src/main/kotlin/tasks/AddMissingSpigotClassMappings.kt @@ -1,48 +1,73 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + package io.papermc.paperweight.tasks import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.file -import org.gradle.api.DefaultTask +import io.papermc.paperweight.util.fileOrNull import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import java.io.File -open class AddMissingSpigotClassMappings : DefaultTask() { +abstract class AddMissingSpigotClassMappings : BaseTask() { - @InputFile - val classSrg: RegularFileProperty = project.objects.fileProperty() - @InputFile - val memberSrg: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val classSrg: RegularFileProperty + @get:InputFile + abstract val memberSrg: RegularFileProperty + @get:Optional + @get:InputFile + abstract val missingClassEntriesSrg: RegularFileProperty + @get:Optional + @get:InputFile + abstract val missingMemberEntriesSrg: RegularFileProperty - @InputFile - val missingClassEntriesSrg: RegularFileProperty = project.objects.fileProperty() - @InputFile - val missingMemberEntriesSrg: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val outputClassSrg: RegularFileProperty + @get:OutputFile + abstract val outputMemberSrg: RegularFileProperty - @OutputFile - val outputClassSrg: RegularFileProperty = defaultOutput("class.csrg") - @OutputFile - val outputMemberSrg: RegularFileProperty = defaultOutput("member.csrg") + override fun init() { + outputClassSrg.convention(defaultOutput("class.csrg")) + outputMemberSrg.convention(defaultOutput("member.csrg")) + } @TaskAction fun run() { - addLines(classSrg.file, missingClassEntriesSrg.file, outputClassSrg.file) - addLines(memberSrg.file, missingMemberEntriesSrg.file, outputMemberSrg.file) + addLines(classSrg.file, missingClassEntriesSrg.fileOrNull, outputClassSrg.file) + addLines(memberSrg.file, missingMemberEntriesSrg.fileOrNull, outputMemberSrg.file) } - private fun addLines(inFile: File, appendFile: File, outputFile: File) { + private fun addLines(inFile: File, appendFile: File?, outputFile: File) { val lines = mutableListOf() - inFile.bufferedReader().use { reader -> - lines.addAll(reader.readLines()) - } - appendFile.bufferedReader().use { reader -> - lines.addAll(reader.readLines()) - } + inFile.useLines { seq -> seq.forEach { lines.add(it) } } + appendFile?.useLines { seq -> seq.forEach { lines.add(it) } } lines.sort() outputFile.bufferedWriter().use { writer -> - lines.forEach(writer::appendln) + lines.forEach { writer.appendln(it) } } } } diff --git a/src/main/kotlin/tasks/ApplyDiffPatches.kt b/src/main/kotlin/tasks/ApplyDiffPatches.kt index 8c8f5ab..3ea1ffc 100644 --- a/src/main/kotlin/tasks/ApplyDiffPatches.kt +++ b/src/main/kotlin/tasks/ApplyDiffPatches.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,10 +23,11 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.PaperweightException +import io.papermc.paperweight.util.Command import io.papermc.paperweight.util.Git +import io.papermc.paperweight.util.UselessOutputStream import io.papermc.paperweight.util.ensureParentExists import io.papermc.paperweight.util.file -import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property @@ -36,33 +36,34 @@ import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property import java.net.URI import java.nio.file.FileSystems import java.nio.file.Files import java.util.Date -open class ApplyDiffPatches : DefaultTask() { +abstract class ApplyDiffPatches : ControllableOutputTask() { - @InputFile - val sourceJar: RegularFileProperty = project.objects.fileProperty() - @Input - val sourceBasePath: Property = project.objects.property() - @InputDirectory - val patchDir: DirectoryProperty = project.objects.directoryProperty() - @Input - val branch: Property = project.objects.property() + @get:InputFile + abstract val sourceJar: RegularFileProperty + @get:Input + abstract val sourceBasePath: Property + @get:InputDirectory + abstract val patchDir: DirectoryProperty + @get:Input + abstract val branch: Property - @OutputDirectory - val outputDir: DirectoryProperty = project.objects.directoryProperty() + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + override fun init() { + printOutput.convention(false) + } @TaskAction fun run() { val git = Git(outputDir.file) git("checkout", "-B", branch.get(), "HEAD").executeSilently(silenceErr = true) - val uri = URI.create("jar:${sourceJar.file.toURI()}") - val basePatchDirFile = outputDir.file.resolve("src/main/java") val outputDirFile = basePatchDirFile.resolve(sourceBasePath.get()) outputDirFile.deleteRecursively() @@ -73,6 +74,7 @@ open class ApplyDiffPatches : DefaultTask() { } // Copy in patch targets + val uri = URI.create("jar:" + sourceJar.file.toURI()) FileSystems.newFileSystem(uri, mapOf()).use { fs -> for (file in patchList) { val javaName = file.name.replaceAfterLast('.', "java") @@ -88,19 +90,29 @@ open class ApplyDiffPatches : DefaultTask() { } } - git("add", "src").executeOut() - git("commit", "-m", "Vanilla $ ${Date()}", "--author=Vanilla ").executeOut() + git("add", "src").setupOut().execute() + git("commit", "-m", "Vanilla $ ${Date()}", "--author=Vanilla ").setupOut().execute() // Apply patches for (file in patchList) { val javaName = file.name.replaceAfterLast('.', "java") - println("Patching ${javaName.removeSuffix(".java")}") - git("apply", "--directory=${basePatchDirFile.relativeTo(outputDir.file).path}", file.absolutePath).executeOut() + if (printOutput.get()) { + println("Patching ${javaName.removeSuffix(".java")}") + } + git("apply", "--directory=${basePatchDirFile.relativeTo(outputDir.file).path}", file.absolutePath).setupOut().execute() } - git("add", "src").executeOut() - git("commit", "-m", "CraftBukkit $ ${Date()}", "--author=CraftBukkit ").executeOut() - git("checkout", "-f", "HEAD~2").executeSilently() + git("add", "src").setupOut().execute() + git("commit", "-m", "CraftBukkit $ ${Date()}", "--author=CraftBukkit ").setupOut().execute() + git("checkout", "-f", "HEAD~2").setupOut().execute() + } + + private fun Command.setupOut() = apply { + if (printOutput.get()) { + setup(System.out, System.err) + } else { + setup(UselessOutputStream, UselessOutputStream) + } } } diff --git a/src/main/kotlin/tasks/ApplyGitPatches.kt b/src/main/kotlin/tasks/ApplyGitPatches.kt index 80084e4..68db309 100644 --- a/src/main/kotlin/tasks/ApplyGitPatches.kt +++ b/src/main/kotlin/tasks/ApplyGitPatches.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,7 +25,6 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.PaperweightException import io.papermc.paperweight.util.Git import io.papermc.paperweight.util.file -import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input @@ -34,80 +32,95 @@ import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.internal.os.OperatingSystem -import org.gradle.kotlin.dsl.property +import java.io.File -open class ApplyGitPatches : DefaultTask() { +abstract class ApplyGitPatches : ControllableOutputTask() { - @Input - val branch: Property = project.objects.property() - @Input - val upstreamBranch: Property = project.objects.property() - @InputDirectory - val upstream: DirectoryProperty = project.objects.directoryProperty() - @InputDirectory - val patchDir: DirectoryProperty = project.objects.directoryProperty() + @get:Input + abstract val branch: Property + @get:Input + abstract val upstreamBranch: Property + @get:InputDirectory + abstract val upstream: DirectoryProperty + @get:InputDirectory + abstract val patchDir: DirectoryProperty - @OutputDirectory - val outputDir: DirectoryProperty = project.objects.directoryProperty() + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + override fun init() { + printOutput.convention(false) + } @TaskAction fun run() { Git(upstream.file).let { git -> - git("fetch").run() + git("fetch").setupOut().run() git("branch", "-f", upstreamBranch.get(), branch.get()).runSilently() } if (!outputDir.file.exists() || !outputDir.file.resolve(".git").exists()) { outputDir.file.deleteRecursively() - Git(outputDir.file.parentFile)("clone", upstream.file.absolutePath, outputDir.file.name).runOut() + Git(outputDir.file.parentFile)("clone", upstream.file.absolutePath, outputDir.file.name).setupOut().run() } val target = outputDir.file.name - println(" Resetting $target to ${upstream.file.name}...") + if (printOutput.get()) { + println(" Resetting $target to ${upstream.file.name}...") + } Git(outputDir.file).let { git -> git("remote", "rm", "upstream").runSilently(silenceErr = true) git("remote", "add", "upstream", upstream.file.absolutePath).runSilently(silenceErr = true) - if (git("checkout", "master").runSilently(silenceOut = false, silenceErr = true) != 0) { - git("checkout", "-b", "master").runOut() + if (git("checkout", "master").setupOut(showError = false).run() != 0) { + git("checkout", "-b", "master").setupOut().run() } git("fetch", "upstream").runSilently(silenceErr = true) - git("reset", "--hard", "upstream/${upstreamBranch.get()}").runOut() + git("reset", "--hard", "upstream/${upstreamBranch.get()}").setupOut().run() - println(" Applying patches to $target...") - - val statusFile = outputDir.file.resolve(".git/patch-apply-failed") - if (statusFile.exists()) { - statusFile.delete() - } - git("am", "--abort").runSilently(silenceErr = true) - - val patches = patchDir.file.listFiles { _, name -> name.endsWith(".patch") } ?: run { - println("No patches found") - return - } - - patches.sort() - - if (git("am", "--3way", "--ignore-whitespace", *patches.map { it.absolutePath }.toTypedArray()).runOut() != 0) { - Thread.sleep(100) // Wait for git - statusFile.writeText("1") - logger.error("*** Something did not apply cleanly to $target.") - logger.error("*** Please review above details and finish the apply then") - logger.error("*** save the changes with ./gradlew `rebuildPatches`") - - if (OperatingSystem.current().isWindows) { - logger.error("") - logger.error("*** Because you're on Windows you'll need to finish the AM,") - logger.error("*** rebuild all patches, and then re-run the patch apply again.") - logger.error("*** Consider using the scripts with Windows Subsystem for Linux.") - } - - throw PaperweightException("Failed to apply patches") - } else { - statusFile.delete() - println(" Patches applied cleanly to $target") - } + applyGitPatches(git, target, outputDir.file, patchDir.file, printOutput.get()) } } } + +fun ControllableOutputTask.applyGitPatches(git: Git, target: String, outputDir: File, patchDir: File, printOutput: Boolean) { + if (printOutput) { + println(" Applying patches to $target...") + } + + val statusFile = outputDir.resolve(".git/patch-apply-failed") + if (statusFile.exists()) { + statusFile.delete() + } + git("am", "--abort").runSilently(silenceErr = true) + + val patches = patchDir.listFiles { _, name -> name.endsWith(".patch") } ?: run { + if (printOutput) { + println("No patches found") + } + return + } + + patches.sort() + + if (git("am", "--3way", "--ignore-whitespace", *patches.map { it.absolutePath }.toTypedArray()).showErrors().run() != 0) { + statusFile.writeText("1") + logger.error("*** Please review above details and finish the apply then") + logger.error("*** save the changes with ./gradlew `rebuildPatches`") + + if (OperatingSystem.current().isWindows) { + logger.error("") + logger.error("*** Because you're on Windows you'll need to finish the AM,") + logger.error("*** rebuild all patches, and then re-run the patch apply again.") + logger.error("*** Consider using the scripts with Windows Subsystem for Linux.") + } + + throw PaperweightException("Failed to apply patches") + } else { + statusFile.delete() + if (printOutput) { + println(" Patches applied cleanly to $target") + } + } +} + diff --git a/src/main/kotlin/tasks/ApplyMcpPatches.kt b/src/main/kotlin/tasks/ApplyMcpPatches.kt index 34aee45..7576da5 100644 --- a/src/main/kotlin/tasks/ApplyMcpPatches.kt +++ b/src/main/kotlin/tasks/ApplyMcpPatches.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,39 +23,27 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.util.Git -import io.papermc.paperweight.util.mcpConfig -import io.papermc.paperweight.util.mcpFile +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile import java.io.File -open class ApplyMcpPatches : ZippedTask() { +abstract class ApplyMcpPatches : ZippedTask() { - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() - - init { - inputs.dir(configFile.map { mcpConfig(configFile) }.map { mcpFile(configFile, it.data.patches.server) }) - } + @get:InputDirectory + abstract val serverPatchDir: DirectoryProperty + @get:InputFile + abstract val configFile: RegularFileProperty override fun run(rootDir: File) { - val config = mcpConfig(configFile) - val serverPatchDir = mcpFile(configFile, config.data.patches.server) - val git = Git(rootDir) val extension = ".java.patch" - project.fileTree(serverPatchDir).matching { - include("*$extension") + objects.fileTree().from(serverPatchDir).matching { + include("**/*$extension") }.forEach { patch -> - val patchName = patch.name - print("Patching ${patchName.substring(0, extension.length)}") - val exit = git("apply", "--ignore-whitespace", patch.absolutePath).setup(System.out, null).run() - if (exit != 0) { - println("...Failed") - } else { - println() - } + git("apply", "--ignore-whitespace", patch.absolutePath).executeSilently() } } } diff --git a/src/main/kotlin/tasks/ApplyPaperPatches.kt b/src/main/kotlin/tasks/ApplyPaperPatches.kt new file mode 100644 index 0000000..523cecd --- /dev/null +++ b/src/main/kotlin/tasks/ApplyPaperPatches.kt @@ -0,0 +1,89 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.Git +import io.papermc.paperweight.util.file +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class ApplyPaperPatches : ControllableOutputTask() { + + @get:InputDirectory + abstract val patchDir: DirectoryProperty + @get:InputFile + abstract val remappedSource: RegularFileProperty + @get:InputFile + abstract val templateGitIgnore: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + @get:OutputDirectory + abstract val remapTargetDir: DirectoryProperty + + override fun init() { + printOutput.convention(true) + remapTargetDir.convention(outputDir.dir("src/main/java")) + } + + @TaskAction + fun run() { + val outputFile = outputDir.file + if (outputFile.exists()) { + outputFile.deleteRecursively() + } + outputFile.mkdirs() + + val target = outputFile.name + + if (printOutput.get()) { + println(" Creating $target from remapped source...") + } + + Git(outputFile).let { git -> + git("init").runSilently(silenceErr = true) + + val sourceDir = remapTargetDir.file + if (sourceDir.exists()) { + sourceDir.deleteRecursively() + } + sourceDir.mkdirs() + + fs.copy { + from(archives.zipTree(remappedSource.file)) + into(sourceDir) + } + + templateGitIgnore.file.copyTo(outputFile.resolve(".gitignore")) + + git("add", ".gitignore", ".").executeSilently() + git("commit", "-m", "Initial", "--author=Initial ").executeSilently() + + applyGitPatches(git, target, outputFile, patchDir.file, printOutput.get()) + } + } +} diff --git a/src/main/kotlin/tasks/ApplySourceAt.kt b/src/main/kotlin/tasks/ApplySourceAt.kt new file mode 100644 index 0000000..46dd825 --- /dev/null +++ b/src/main/kotlin/tasks/ApplySourceAt.kt @@ -0,0 +1,81 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.path +import org.cadixdev.at.io.AccessTransformFormats +import org.cadixdev.mercury.Mercury +import org.cadixdev.mercury.at.AccessTransformerRewriter +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import java.io.File + +abstract class ApplySourceAt : ZippedTask() { + + @get:InputFile + abstract val vanillaJar: RegularFileProperty + @get:InputFile + abstract val vanillaRemappedSrgJar: RegularFileProperty + @get:InputFile + abstract val atFile: RegularFileProperty + + override fun run(rootDir: File) { + val inputDir = rootDir.resolve("input") + val outputDir = rootDir.resolve("output") + + // Move everything into `input/` so we can put output into `output/` + inputDir.mkdirs() + rootDir.listFiles()?.forEach { file -> + if (file != inputDir) { + file.renameTo(inputDir.resolve(file.name)) + } + } + outputDir.mkdirs() + + val at = AccessTransformFormats.FML.read(atFile.path) + + Mercury().apply { + classPath.addAll(listOf( + vanillaJar.path, + vanillaRemappedSrgJar.path + )) + + processors.add(AccessTransformerRewriter.create(at)) + + rewrite(inputDir.toPath(), outputDir.toPath()) + } + + // Remove input files + rootDir.listFiles()?.forEach { file -> + if (file != outputDir) { + file.deleteRecursively() + } + } + + // Move output into root + outputDir.listFiles()?.forEach { file -> + file.renameTo(rootDir.resolve(file.name)) + } + outputDir.delete() + } +} diff --git a/src/main/kotlin/tasks/BaseTask.kt b/src/main/kotlin/tasks/BaseTask.kt new file mode 100644 index 0000000..6799146 --- /dev/null +++ b/src/main/kotlin/tasks/BaseTask.kt @@ -0,0 +1,48 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +abstract class BaseTask : DefaultTask() { + + @get:Inject + abstract val objects: ObjectFactory + @get:Inject + abstract val layout: ProjectLayout + @get:Inject + abstract val fs: FileSystemOperations + @get:Inject + abstract val archives: ArchiveOperations + + open fun init() {} + + init { + this.init() + } +} diff --git a/src/main/kotlin/tasks/ControllableOutputTask.kt b/src/main/kotlin/tasks/ControllableOutputTask.kt new file mode 100644 index 0000000..d6bc64f --- /dev/null +++ b/src/main/kotlin/tasks/ControllableOutputTask.kt @@ -0,0 +1,51 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.Command +import io.papermc.paperweight.util.UselessOutputStream +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Console + +abstract class ControllableOutputTask : BaseTask() { + + @get:Console + abstract val printOutput: Property + + fun Command.setupOut(showError: Boolean = true) = apply { + if (printOutput.get()) { + val err = if (showError) System.out else UselessOutputStream + setup(System.out, err) + } else { + setup(UselessOutputStream, UselessOutputStream) + } + } + + fun Command.showErrors() = apply { + if (printOutput.get()) { + setup(System.out, System.out) + } else { + setup(UselessOutputStream, System.out) + } + } +} diff --git a/src/main/kotlin/tasks/DecompileVanillaJar.kt b/src/main/kotlin/tasks/DecompileVanillaJar.kt index 9b5bd6a..aca67fd 100644 --- a/src/main/kotlin/tasks/DecompileVanillaJar.kt +++ b/src/main/kotlin/tasks/DecompileVanillaJar.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,27 +28,29 @@ import io.papermc.paperweight.util.cache import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.ensureDeleted import io.papermc.paperweight.util.runJar -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property import java.util.concurrent.ThreadLocalRandom -open class DecompileVanillaJar : DefaultTask() { +abstract class DecompileVanillaJar : BaseTask() { - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() - @InputFile - val fernFlowerJar: RegularFileProperty = project.objects.fileProperty() - @Input - val decompileCommand: Property = project.objects.property() + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:InputFile + abstract val fernFlowerJar: RegularFileProperty + @get:Input + abstract val decompileCommand: Property - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + @get:OutputFile + abstract val outputJar: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + } @TaskAction fun run() { @@ -68,10 +69,10 @@ open class DecompileVanillaJar : DefaultTask() { cmd += inputJarPath cmd += decomp.canonicalPath - val logFile = project.cache.resolve(paperTaskOutput("log")) + val logFile = layout.cache.resolve(paperTaskOutput("log")) logFile.delete() - runJar(fernFlowerJar, workingDir = project.cache, logFile = logFile, args = *cmd.toTypedArray()) + runJar(fernFlowerJar, workingDir = layout.cache, logFile = logFile, args = *cmd.toTypedArray()) ensureDeleted(outputJarFile) decomp.resolve(inputJarFile.name).renameTo(outputJarFile) diff --git a/src/main/kotlin/tasks/DownloadServerJar.kt b/src/main/kotlin/tasks/DownloadServerJar.kt index 4fb8de7..a4d31ee 100644 --- a/src/main/kotlin/tasks/DownloadServerJar.kt +++ b/src/main/kotlin/tasks/DownloadServerJar.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,38 +24,34 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.PaperweightException import io.papermc.paperweight.util.defaultOutput -import io.papermc.paperweight.util.ensureDeleted -import io.papermc.paperweight.util.ensureParentExists -import org.gradle.api.DefaultTask +import io.papermc.paperweight.util.download import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property import java.math.BigInteger -import java.net.URL import java.security.MessageDigest -open class DownloadServerJar : DefaultTask() { +abstract class DownloadServerJar : BaseTask() { - @Input - val downloadUrl: Property = project.objects.property() - @Input - val hash: Property = project.objects.property() + @get:Input + abstract val downloadUrl: Property + @get:Input + abstract val hash: Property - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + @get:OutputFile + abstract val outputJar: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + } @TaskAction fun run() { val file = outputJar.asFile.get() - ensureParentExists(file) - ensureDeleted(file) - file.outputStream().buffered().use { out -> - URL(downloadUrl.get()).openStream().copyTo(out) - } + download(downloadUrl, outputJar) val digest = MessageDigest.getInstance("MD5") diff --git a/src/main/kotlin/tasks/Extract.kt b/src/main/kotlin/tasks/Extract.kt deleted file mode 100644 index 7ba6752..0000000 --- a/src/main/kotlin/tasks/Extract.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * paperweight is a Gradle plugin for the PaperMC project. It uses - * some code and systems originally from ForgeGradle. - * - * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - */ - -package io.papermc.paperweight.tasks - -import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.property - -open class Extract : DefaultTask() { - - @Input - val config: Property = project.objects.property() - - @OutputDirectory - val outputDir: DirectoryProperty = project.objects.directoryProperty() - - @TaskAction - fun run() { - project.copy { - from(project.zipTree(project.configurations[config.get()].resolve().single())) - into(outputDir) - } - } -} - -open class ExtractMcpMappings : Extract() { - - @OutputFile - val fieldsCsv: RegularFileProperty = project.objects.fileProperty() - .convention(outputDir.map { it.file("fields.csv") }) - @OutputFile - val methodsCsv: RegularFileProperty = project.objects.fileProperty() - .convention(outputDir.map { it.file("methods.csv") }) - @OutputFile - val paramsCsv: RegularFileProperty = project.objects.fileProperty() - .convention(outputDir.map { it.file("params.csv") }) -} - -open class ExtractMcpData : Extract() { - - @OutputFile - val configJson: RegularFileProperty = project.objects.fileProperty() - .convention(outputDir.map { it.file("config.json") }) -} diff --git a/src/main/kotlin/tasks/FilterExcludes.kt b/src/main/kotlin/tasks/FilterExcludes.kt index 41d1066..be16a83 100644 --- a/src/main/kotlin/tasks/FilterExcludes.kt +++ b/src/main/kotlin/tasks/FilterExcludes.kt @@ -1,3 +1,25 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + package io.papermc.paperweight.tasks import io.papermc.paperweight.util.file @@ -9,10 +31,10 @@ import java.io.File * Because Spigot doesn't remap all classes, there are class and package name clashes if we don't do this in the source * remap step. Other than that, we don't need this jar */ -open class FilterExcludes : ZippedTask() { +abstract class FilterExcludes : ZippedTask() { - @InputFile - val excludesFile: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val excludesFile: RegularFileProperty override fun run(rootDir: File) { excludesFile.file.useLines { lines -> diff --git a/src/main/kotlin/tasks/GenerateSpigotSrgs.kt b/src/main/kotlin/tasks/GenerateSpigotSrgs.kt index 4c67494..cf9a571 100644 --- a/src/main/kotlin/tasks/GenerateSpigotSrgs.kt +++ b/src/main/kotlin/tasks/GenerateSpigotSrgs.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,6 +23,7 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.fileOrNull import io.papermc.paperweight.util.writeMappings import org.cadixdev.lorenz.MappingSet import org.cadixdev.lorenz.io.MappingFormats @@ -40,37 +40,40 @@ import org.cadixdev.lorenz.model.TopLevelClassMapping import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import java.lang.IllegalStateException -open class GenerateSpigotSrgs : DefaultTask() { +abstract class GenerateSpigotSrgs : DefaultTask() { - @InputFile - val notchToSrg: RegularFileProperty = project.objects.fileProperty() - @InputFile - val srgToMcp: RegularFileProperty = project.objects.fileProperty() - @InputFile - val classMappings: RegularFileProperty = project.objects.fileProperty() - @InputFile - val memberMappings: RegularFileProperty = project.objects.fileProperty() - @InputFile - val packageMappings: RegularFileProperty = project.objects.fileProperty() - @InputFile - val extraSpigotSrgMappings: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val notchToSrg: RegularFileProperty + @get:InputFile + abstract val srgToMcp: RegularFileProperty + @get:InputFile + abstract val classMappings: RegularFileProperty + @get:InputFile + abstract val memberMappings: RegularFileProperty + @get:InputFile + abstract val packageMappings: RegularFileProperty + @get:Optional + @get:InputFile + abstract val extraSpigotSrgMappings: RegularFileProperty + @get:InputFile + abstract val loggerFields: RegularFileProperty - @OutputFile - val spigotToSrg: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val spigotToMcp: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val spigotToNotch: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val srgToSpigot: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val mcpToSpigot: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val notchToSpigot: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val spigotToSrg: RegularFileProperty + @get:OutputFile + abstract val spigotToMcp: RegularFileProperty + @get:OutputFile + abstract val spigotToNotch: RegularFileProperty + @get:OutputFile + abstract val srgToSpigot: RegularFileProperty + @get:OutputFile + abstract val mcpToSpigot: RegularFileProperty + @get:OutputFile + abstract val notchToSpigot: RegularFileProperty @TaskAction fun run() { @@ -78,6 +81,12 @@ open class GenerateSpigotSrgs : DefaultTask() { val memberMappingSet = MappingFormats.CSRG.createReader(memberMappings.file.toPath()).use { it.read() } val mergedMappingSet = MappingSetMerger.create(classMappingSet, memberMappingSet).merge() + for (line in loggerFields.file.readLines(Charsets.UTF_8)) { + val (className, fieldName) = line.split(' ') + val classMapping = mergedMappingSet.getClassMapping(className).orElse(null) ?: continue + classMapping.getOrCreateFieldMapping(fieldName).deobfuscatedName = "LOGGER" + } + // Get the new package name val newPackage = packageMappings.asFile.get().readLines()[0].split(Regex("\\s+"))[1] @@ -88,14 +97,16 @@ open class GenerateSpigotSrgs : DefaultTask() { mergedMappingSet, notchToSrgSet, MergeConfig.builder() - .withHandler(SpigotPackageMergerHandler(newPackage)) + .withMergeHandler(SpigotPackageMergerHandler(newPackage)) .build() ).merge() val srgToMcpSet = MappingFormats.TSRG.createReader(srgToMcp.file.toPath()).use { it.read() } val spigotToSrgSet = MappingSetMerger.create(notchToSpigotSet.reverse(), notchToSrgSet).merge() - MappingFormats.TSRG.createReader(extraSpigotSrgMappings.file.toPath()).use { it.read(spigotToSrgSet) } + extraSpigotSrgMappings.fileOrNull?.toPath()?.let { path -> + MappingFormats.TSRG.createReader(path).use { it.read(spigotToSrgSet) } + } val mcpToNotchSet = MappingSetMerger.create(notchToSrgSet, srgToMcpSet).merge().reverse() val mcpToSpigotSet = MappingSetMerger.create(mcpToNotchSet, notchToSpigotSet).merge() @@ -135,7 +146,7 @@ class SpigotPackageMergerHandler(private val newPackage: String) : MappingSetMer ): MergeResult { // If both are provided, keep spigot return MergeResult( - target.createTopLevelClassMapping(left.obfuscatedName, prependPackage(left.deobfuscatedName)), + target.createTopLevelClassMapping(prependPackage(left.obfuscatedName), prependPackage(left.deobfuscatedName)), right ) } @@ -145,7 +156,7 @@ class SpigotPackageMergerHandler(private val newPackage: String) : MappingSetMer context: MergeContext ): MergeResult { return MergeResult( - target.createTopLevelClassMapping(left.obfuscatedName, prependPackage(left.deobfuscatedName)) + target.createTopLevelClassMapping(prependPackage(left.obfuscatedName), prependPackage(left.deobfuscatedName)) ) } override fun addRightTopLevelClassMapping( @@ -157,8 +168,10 @@ class SpigotPackageMergerHandler(private val newPackage: String) : MappingSetMer return if (right.deobfuscatedName.contains("/client/")) { MergeResult(null) } else { + // This is a mapping Spigot is totally missing, but Spigot maps all classes without a package to + // /net/minecraft regardless if there are mappings for the classes or not MergeResult( - target.createTopLevelClassMapping(right.obfuscatedName, prependPackage(right.obfuscatedName)), + target.createTopLevelClassMapping(prependPackage(right.obfuscatedName), right.obfuscatedName), right ) } @@ -195,19 +208,23 @@ class SpigotPackageMergerHandler(private val newPackage: String) : MappingSetMer override fun mergeFieldMappings( left: FieldMapping, - right: FieldMapping, + strictRight: FieldMapping?, + looseRight: FieldMapping?, target: ClassMapping<*, *>, context: MergeContext - ): FieldMapping? { + ): FieldMapping { throw IllegalStateException("Unexpectedly merged field: $left") } + override fun mergeDuplicateFieldMappings( left: FieldMapping, - right: FieldMapping, - rightContinuation: FieldMapping?, + strictRightDuplicate: FieldMapping?, + looseRightDuplicate: FieldMapping?, + strictRightContinuation: FieldMapping?, + looseRightContinuation: FieldMapping?, target: ClassMapping<*, *>, context: MergeContext - ): FieldMapping? { + ): FieldMapping { return target.createFieldMapping(left.obfuscatedName, left.deobfuscatedName) } diff --git a/src/main/kotlin/tasks/GenerateSrgs.kt b/src/main/kotlin/tasks/GenerateSrgs.kt index 5080bd4..b37c686 100644 --- a/src/main/kotlin/tasks/GenerateSrgs.kt +++ b/src/main/kotlin/tasks/GenerateSrgs.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,10 +24,10 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.util.ensureParentExists import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.fileOrNull import io.papermc.paperweight.util.getCsvReader -import io.papermc.paperweight.util.mcpConfig -import io.papermc.paperweight.util.mcpFile import io.papermc.paperweight.util.writeMappings +import org.cadixdev.bombe.type.signature.MethodSignature import org.cadixdev.lorenz.MappingSet import org.cadixdev.lorenz.io.MappingFormats import org.cadixdev.lorenz.merge.MappingSetMerger @@ -37,44 +36,46 @@ import org.cadixdev.lorenz.model.InnerClassMapping import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -open class GenerateSrgs : DefaultTask() { +abstract class GenerateSrgs : DefaultTask() { - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() - @InputFile - val methodsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val fieldsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val extraNotchSrgMappings: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val methodsCsv: RegularFileProperty + @get:InputFile + abstract val fieldsCsv: RegularFileProperty + @get:Optional + @get:InputFile + abstract val extraNotchSrgMappings: RegularFileProperty - @OutputFile - val notchToSrg: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val notchToMcp: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val mcpToNotch: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val mcpToSrg: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val srgToNotch: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val srgToMcp: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val inSrg: RegularFileProperty + + @get:OutputFile + abstract val notchToSrg: RegularFileProperty + @get:OutputFile + abstract val notchToMcp: RegularFileProperty + @get:OutputFile + abstract val mcpToNotch: RegularFileProperty + @get:OutputFile + abstract val mcpToSrg: RegularFileProperty + @get:OutputFile + abstract val srgToNotch: RegularFileProperty + @get:OutputFile + abstract val srgToMcp: RegularFileProperty @TaskAction fun run() { - val config = mcpConfig(configFile) - val inSrg = mcpFile(configFile, config.data.mappings) - val methods = HashMap() val fields = HashMap() readCsvs(methods, fields) - val inSet = MappingFormats.TSRG.createReader(inSrg.toPath()).use { it.read() } - MappingFormats.TSRG.createReader(extraNotchSrgMappings.file.toPath()).use { it.read(inSet) } + val inSet = MappingFormats.TSRG.createReader(inSrg.file.toPath()).use { it.read() } + extraNotchSrgMappings.fileOrNull?.toPath()?.let { path -> + MappingFormats.TSRG.createReader(path).use { it.read(inSet) } + } ensureParentExists(notchToSrg, notchToMcp, mcpToNotch, mcpToSrg, srgToNotch, srgToMcp) createMappings(inSet, methods, fields) @@ -96,10 +97,12 @@ open class GenerateSrgs : DefaultTask() { private fun createMappings(inSet: MappingSet, methods: Map, fields: Map) { val notchToSrgSet = MappingSet.create() + for (mapping in inSet.topLevelClassMappings) { val newMapping = notchToSrgSet.createTopLevelClassMapping(mapping.obfuscatedName, mapping.deobfuscatedName) remapMembers(mapping, newMapping) } + handleKeywordMappings(notchToSrgSet) // We have Notch -> SRG val srgToNotchSet = notchToSrgSet.reverse() @@ -138,6 +141,39 @@ open class GenerateSrgs : DefaultTask() { ) } + private fun handleKeywordMappings(mappings: MappingSet) { + for (classMapping in mappings.topLevelClassMappings) { + handleKeywordMappingClass(classMapping) + } + } + + private fun handleKeywordMappingClass(classMapping: ClassMapping<*, *>) { + for (innerClassMapping in classMapping.innerClassMappings) { + handleKeywordMappingClass(innerClassMapping) + } + for (fieldMapping in classMapping.fieldMappings) { + if (fieldMapping.obfuscatedName in javaKeywords) { + val sourceName = fieldMapping.obfuscatedName + '_' + if (classMapping.hasFieldMapping(sourceName)) { + // If the "source name" of the mapping already exists, just skip it. I don't even know what would + // happen in this case at decompile time to be honest + continue + } + classMapping.createFieldMapping(sourceName, fieldMapping.deobfuscatedName) + } + } + for (methodMapping in classMapping.methodMappings) { + if (methodMapping.obfuscatedName in javaKeywords) { + val sourceName = methodMapping.obfuscatedName + "_" + val sourceSignature = MethodSignature(sourceName, methodMapping.signature.descriptor) + if (classMapping.hasMethodMapping(sourceSignature)) { + continue + } + classMapping.createMethodMapping(sourceSignature, methodMapping.deobfuscatedName) + } + } + } + private fun mapClass(inClass: ClassMapping<*, *>, outClass: ClassMapping<*, *>, methods: Map, fields: Map) { for (fieldMapping in inClass.fieldMappings) { val mcpName = fields[fieldMapping.deobfuscatedName] ?: fieldMapping.deobfuscatedName @@ -155,13 +191,7 @@ open class GenerateSrgs : DefaultTask() { } private fun remapAnonymousClasses(mapping: InnerClassMapping, target: ClassMapping<*, *>) { - val newMapping = if (mapping.obfuscatedName.toIntOrNull() != null) { - // This is an anonymous class, keep the index - target.createInnerClassMapping(mapping.deobfuscatedName, mapping.deobfuscatedName) - } else { - target.createInnerClassMapping(mapping.obfuscatedName, mapping.deobfuscatedName) - } - + val newMapping = target.createInnerClassMapping(mapping.obfuscatedName, mapping.deobfuscatedName) remapMembers(mapping, newMapping) } @@ -177,3 +207,56 @@ open class GenerateSrgs : DefaultTask() { } } } + +private val javaKeywords: HashSet = hashSetOf( + "abstract", + "continue", + "for", + "new", + "switch", + "assert", + "default", + "goto", + "package", + "synchronized", + "boolean", + "do", + "if", + "private", + "this", + "break", + "double", + "implements", + "protected", + "throw", + "byte", + "else", + "import", + "public", + "throws", + "case", + "enum", + "instanceof", + "return", + "transient", + "catch", + "extends", + "int", + "short", + "try", + "char", + "final", + "interface", + "static", + "void", + "class", + "finally", + "long", + "strictfp", + "volatile", + "const", + "float", + "native", + "super", + "while" +) diff --git a/src/main/kotlin/tasks/GetRemoteJsons.kt b/src/main/kotlin/tasks/GetRemoteJsons.kt deleted file mode 100644 index bfe49b9..0000000 --- a/src/main/kotlin/tasks/GetRemoteJsons.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * paperweight is a Gradle plugin for the PaperMC project. It uses - * some code and systems originally from ForgeGradle. - * - * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - */ - -package io.papermc.paperweight.tasks - -import com.github.salomonbrys.kotson.array -import com.github.salomonbrys.kotson.fromJson -import com.github.salomonbrys.kotson.get -import com.github.salomonbrys.kotson.string -import com.google.gson.JsonObject -import io.papermc.paperweight.util.Constants -import io.papermc.paperweight.util.Constants.paperTaskOutput -import io.papermc.paperweight.util.cache -import io.papermc.paperweight.util.ensureParentExists -import io.papermc.paperweight.util.ext -import io.papermc.paperweight.util.file -import io.papermc.paperweight.util.getWithEtag -import io.papermc.paperweight.util.gson -import io.papermc.paperweight.util.toProvider -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property -import util.MinecraftManifest - -open class GetRemoteJsons : DefaultTask() { - - @Input - val config: Property = project.objects.property() - - @OutputFile - val artifactOutputFile: RegularFileProperty = project.objects.run { - fileProperty().convention(project.toProvider { - project.cache.resolve(paperTaskOutput("mc-libraries", "txt")) - }) - } - - init { - outputs.upToDateWhen { false } - } - - @TaskAction - fun run() { - val cache = project.cache - val extension = project.ext - - val mcManifestJson = cache.resolve(Constants.MC_MANIFEST) - val mcVersionJson = cache.resolve(Constants.VERSION_JSON) - ensureParentExists(mcManifestJson, mcVersionJson) - - // McManifest.json - val mcManifestEtag = cache.resolve("${Constants.MC_MANIFEST}.etag") - getWithEtag(Constants.MC_MANIFEST_URL, mcManifestJson, mcManifestEtag) - val mcManifestText = gson.fromJson(mcManifestJson.readText()) - - val mcVersionEtag = mcVersionJson.resolveSibling("${mcVersionJson.name}.etag") - getWithEtag(mcManifestText.versions.first { it.id == extension.minecraftVersion.get() }.url, mcVersionJson, mcVersionEtag) - - val jsonObject = mcVersionJson.bufferedReader().use { reader -> - gson.fromJson(reader) - } - - val conf = config.get() - artifactOutputFile.file.delete() - artifactOutputFile.file.bufferedWriter().use { writer -> - for (library in jsonObject["libraries"].array) { - val artifact = library["name"].string - project.dependencies.add(conf, artifact) - writer.appendln(artifact) - } - } - } -} diff --git a/src/main/kotlin/tasks/InspectVanillaJar.kt b/src/main/kotlin/tasks/InspectVanillaJar.kt new file mode 100644 index 0000000..924c9cc --- /dev/null +++ b/src/main/kotlin/tasks/InspectVanillaJar.kt @@ -0,0 +1,109 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.file +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.Opcodes + +abstract class InspectVanillaJar : BaseTask() { + + @get:InputFile + abstract val inputJar: RegularFileProperty + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + override fun init() { + outputFile.convention(defaultOutput("txt")) + } + + @TaskAction + fun run() { + val outputList = mutableListOf() + + val jarFile = inputJar.file + archives.zipTree(jarFile).matching { + include("/*.class") + include("/net/minecraft/**/*.class") + }.forEach { file -> + if (file.isDirectory) { + return@forEach + } + val classData = file.readBytes() + + val reader = ClassReader(classData) + reader.accept(LoggerFinder(outputList), 0) + + } + + outputFile.file.bufferedWriter(Charsets.UTF_8).use { writer -> + for (loggerField in outputList) { + writer.append(loggerField.className) + writer.append(' ') + writer.append(loggerField.fieldName) + writer.newLine() + } + } + } +} + +class LoggerFinder(private val fields: MutableList) : ClassVisitor(Opcodes.ASM8) { + + private var currentClass: String? = null + + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array? + ) { + this.currentClass = name + } + + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any? + ): FieldVisitor? { + val className = currentClass ?: return null + if (descriptor != "Lorg/apache/logging/log4j/Logger;") { + return null + } + fields += LoggerField(className, name) + return null + } +} + +data class LoggerField(val className: String, val fieldName: String) diff --git a/src/main/kotlin/tasks/RemapVanillaJarSrg.kt b/src/main/kotlin/tasks/MergeAccessTransforms.kt similarity index 52% rename from src/main/kotlin/tasks/RemapVanillaJarSrg.kt rename to src/main/kotlin/tasks/MergeAccessTransforms.kt index 081eaca..fad7690 100644 --- a/src/main/kotlin/tasks/RemapVanillaJarSrg.kt +++ b/src/main/kotlin/tasks/MergeAccessTransforms.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,42 +23,38 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.util.defaultOutput -import io.papermc.paperweight.util.ensureDeleted -import io.papermc.paperweight.util.ensureParentExists import io.papermc.paperweight.util.file -import org.cadixdev.atlas.Atlas -import org.cadixdev.bombe.asm.jar.JarEntryRemappingTransformer -import org.cadixdev.lorenz.asm.LorenzRemapper -import org.cadixdev.lorenz.io.MappingFormats -import org.gradle.api.DefaultTask +import org.cadixdev.at.AccessTransformSet +import org.cadixdev.at.io.AccessTransformFormats +import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -open class RemapVanillaJarSrg : DefaultTask() { +abstract class MergeAccessTransforms : BaseTask() { - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() + @get:InputFiles + abstract val inputFiles: ListProperty - @InputFile - val mappings: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val outputFile: RegularFileProperty - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + override fun init() { + outputFile.convention(defaultOutput("at")) + } @TaskAction fun run() { - ensureParentExists(outputJar.file) - ensureDeleted(outputJar.file) + val ats = inputFiles.get() + .map { AccessTransformFormats.FML.read(it.asFile.toPath()) } - val mappings = MappingFormats.TSRG.createReader(mappings.file.toPath()).use { it.read() } - Atlas().apply { - install { ctx -> - JarEntryRemappingTransformer(LorenzRemapper(mappings, ctx.inheritanceProvider())) - } - run(inputJar.file.toPath(), outputJar.file.toPath()) + val outputAt = AccessTransformSet.create() + for (at in ats) { + outputAt.merge(at) } + AccessTransformFormats.FML.write(outputFile.file.toPath(), outputAt) } } diff --git a/src/main/kotlin/tasks/PatchMcpCsv.kt b/src/main/kotlin/tasks/PatchMcpCsv.kt index 8cd7b49..fd7f2e6 100644 --- a/src/main/kotlin/tasks/PatchMcpCsv.kt +++ b/src/main/kotlin/tasks/PatchMcpCsv.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,23 +30,23 @@ import org.gradle.api.tasks.TaskAction import java.io.PrintWriter import java.util.regex.Pattern -open class PatchMcpCsv : DefaultTask() { +abstract class PatchMcpCsv : DefaultTask() { - @InputFile - val fieldsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val methodsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val paramsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val changesFile: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val fieldsCsv: RegularFileProperty + @get:InputFile + abstract val methodsCsv: RegularFileProperty + @get:InputFile + abstract val paramsCsv: RegularFileProperty + @get:InputFile + abstract val changesFile: RegularFileProperty - @OutputFile - val paperFieldCsv: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val paperMethodCsv: RegularFileProperty = project.objects.fileProperty() - @OutputFile - val paperParamCsv: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val paperFieldCsv: RegularFileProperty + @get:OutputFile + abstract val paperMethodCsv: RegularFileProperty + @get:OutputFile + abstract val paperParamCsv: RegularFileProperty @TaskAction fun run() { diff --git a/src/main/kotlin/tasks/SetupMcpDependencies.kt b/src/main/kotlin/tasks/RemapAccessTransform.kt similarity index 56% rename from src/main/kotlin/tasks/SetupMcpDependencies.kt rename to src/main/kotlin/tasks/RemapAccessTransform.kt index 4627601..ac480fb 100644 --- a/src/main/kotlin/tasks/SetupMcpDependencies.kt +++ b/src/main/kotlin/tasks/RemapAccessTransform.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,33 +22,35 @@ package io.papermc.paperweight.tasks -import io.papermc.paperweight.util.mcpConfig -import org.gradle.api.DefaultTask +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.file +import org.cadixdev.at.io.AccessTransformFormats +import org.cadixdev.lorenz.io.MappingFormats import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property -open class SetupMcpDependencies : DefaultTask() { +abstract class RemapAccessTransform : BaseTask() { - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() - @Input - val forgeFlowerConfig: Property = project.objects.property() - @Input - val mcInjectorConfig: Property = project.objects.property() + @get:InputFile + abstract val inputFile: RegularFileProperty + @get:InputFile + abstract val mappings: RegularFileProperty - init { - outputs.upToDateWhen { false } + @get:OutputFile + abstract val outputFile: RegularFileProperty + + override fun init() { + outputFile.convention(defaultOutput("at")) } @TaskAction fun run() { - val config = mcpConfig(configFile) + val at = AccessTransformFormats.FML.read(inputFile.file.toPath()) + val mappingSet = MappingFormats.TSRG.createReader(mappings.file.toPath()).use { it.read() } - project.dependencies.add(forgeFlowerConfig.get(), config.functions.getValue("decompile").version) - project.dependencies.add(mcInjectorConfig.get(), config.functions.getValue("mcinject").version) + val resultAt = at.remap(mappingSet) + AccessTransformFormats.FML.write(outputFile.file.toPath(), resultAt) } } diff --git a/src/main/kotlin/tasks/RemapSources.kt b/src/main/kotlin/tasks/RemapSources.kt deleted file mode 100644 index 2abf631..0000000 --- a/src/main/kotlin/tasks/RemapSources.kt +++ /dev/null @@ -1,267 +0,0 @@ -/* - * paperweight is a Gradle plugin for the PaperMC project. It uses - * some code and systems originally from ForgeGradle. - * - * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - */ - -package io.papermc.paperweight.tasks - -import io.papermc.paperweight.PaperweightException -import io.papermc.paperweight.util.defaultOutput -import io.papermc.paperweight.util.file -import io.papermc.paperweight.util.mcpConfig -import io.papermc.paperweight.util.mcpFile -import org.cadixdev.at.AccessTransformSet -import org.cadixdev.at.io.AccessTransformFormats -import org.cadixdev.lorenz.io.MappingFormats -import org.cadixdev.mercury.Mercury -import org.cadixdev.mercury.RewriteContext -import org.cadixdev.mercury.SourceProcessor -import org.cadixdev.mercury.SourceRewriter -import org.cadixdev.mercury.at.AccessTransformerRewriter -import org.cadixdev.mercury.extra.AccessAnalyzerProcessor -import org.cadixdev.mercury.extra.BridgeMethodRewriter -import org.cadixdev.mercury.remapper.MercuryRemapper -import org.cadixdev.mercury.util.BombeBindings -import org.eclipse.jdt.core.dom.ASTVisitor -import org.eclipse.jdt.core.dom.IMethodBinding -import org.eclipse.jdt.core.dom.IVariableBinding -import org.eclipse.jdt.core.dom.MethodDeclaration -import org.eclipse.jdt.core.dom.Modifier -import org.eclipse.jdt.core.dom.SimpleName -import org.eclipse.jdt.core.dom.VariableDeclaration -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.property -import java.io.File - -open class RemapSources : ZippedTask() { - - @InputFile - val vanillaJar: RegularFileProperty = project.objects.fileProperty() - @InputFile - val vanillaRemappedSpigotJar: RegularFileProperty = project.objects.fileProperty() // Required for pre-remap pass - @InputFile - val vanillaRemappedSrgJar: RegularFileProperty = project.objects.fileProperty() // Required for post-remap pass - @InputFile - val mappings: RegularFileProperty = project.objects.fileProperty() - @Input - val configuration: Property = project.objects.property() - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() - @InputDirectory - val spigotServerDir: DirectoryProperty = project.objects.directoryProperty() - @InputDirectory - val spigotApiDir: DirectoryProperty = project.objects.directoryProperty() - - @OutputFile - val generatedAt: RegularFileProperty = defaultOutput("at") - - override fun run(rootDir: File) { - val config = mcpConfig(configFile) - val constructors = mcpFile(configFile, config.data.constructors) - - val mappings = MappingFormats.TSRG.createReader(mappings.file.toPath()).use { it.read() } - - val srcDir = spigotServerDir.file.resolve("src/main/java") - val pass1Dir = rootDir.resolve("pass1") - val outDir = rootDir.resolve("out") - - val configuration = project.configurations[configuration.get()] - - val ats = AccessTransformSet.create() - - // Remap any references Spigot maps to SRG - Mercury().apply { - classPath.addAll(listOf( - vanillaJar.asFile.get().toPath(), - vanillaRemappedSpigotJar.asFile.get().toPath(), - spigotApiDir.file.resolve("src/main/java").toPath() - )) - - for (file in configuration.resolvedConfiguration.files) { - classPath.add(file.toPath()) - } - - // Generate AT - processors.add(AccessAnalyzerProcessor.create(ats, mappings)) - process(srcDir.toPath()) - - // Remap - processors.clear() - processors.addAll(listOf( - MercuryRemapper.create(mappings), - BridgeMethodRewriter.create(), - AccessTransformerRewriter.create(ats) - )) - - rewrite(srcDir.toPath(), pass1Dir.toPath()) - } - - Mercury().apply { - // Remap SRG parameters - classPath.addAll(listOf( - vanillaJar.asFile.get().toPath(), - vanillaRemappedSrgJar.asFile.get().toPath(), - spigotApiDir.file.resolve("src/main/java").toPath() - )) - - processors.add(SrgParameterRemapper(constructors)) - rewrite(pass1Dir.toPath(), outDir.toPath()) - } - - // Only leave remapped source - val fileList = rootDir.listFiles() ?: throw PaperweightException("Could not list files in $rootDir") - for (file in fileList) { - if (file != outDir) { - file.deleteRecursively() - } - } - - // Move out/* to base dir - val files = outDir.listFiles() ?: throw PaperweightException("No files found in $outDir") - for (file in files) { - file.renameTo(rootDir.resolve(file.name)) - } - outDir.delete() - - // And write generated AT file - AccessTransformFormats.FML.write(generatedAt.asFile.get().toPath(), ats) - } -} - -class SrgParameterRemapper(constructors: File) : SourceRewriter { - - private val constructorMap = hashMapOf>() - - init { - constructors.useLines { lines -> - lines.forEach { line -> - val parts = line.split(' ') - constructorMap.compute(parts[1].replace('/', '.')) { _, v -> - val node = ConstructorNode(parts[0].toInt(), parts[2]) - if (v == null) { - return@compute mutableListOf(node) - } else { - v += node - return@compute v - } - } - } - } - - for (list in constructorMap.values) { - // Put bigger numbers first - // Old constructor entries are still present, just with smaller numbers. So we don't want to grab an older - // entry - list.reverse() - } - } - - override fun getFlags(): Int = SourceProcessor.FLAG_RESOLVE_BINDINGS - - override fun rewrite(context: RewriteContext) { - context.compilationUnit.accept(SrgParameterVisitor(context, constructorMap)) - } -} - -data class ConstructorNode( - val id: Int, - val signature: String -) - -class SrgParameterVisitor( - private val context: RewriteContext, - private val constructors: Map> -) : ASTVisitor() { - - companion object { - private val MATCHER = Regex("func_(\\d+)_.*") - } - - override fun visit(node: SimpleName): Boolean { - val binding = node.resolveBinding() as? IVariableBinding ?: return false - if (!binding.isParameter) { - return false - } - - val variableDecl = context.compilationUnit.findDeclaringNode(binding.variableDeclaration) as VariableDeclaration - - val method = binding.declaringMethod - val methodDecl = context.compilationUnit.findDeclaringNode(method) as? MethodDeclaration ?: return false - - handleMethod(node, methodDecl, method, variableDecl) - - return false - } - - private fun handleMethod(node: SimpleName, methodDecl: MethodDeclaration, method: IMethodBinding, variableDecl: VariableDeclaration) { - if (method.isConstructor) { - handleConstructor(node, methodDecl, method, variableDecl) - return - } - - val match = MATCHER.matchEntire(method.name) ?: return - val isStatic = method.modifiers and Modifier.STATIC != 0 - - var index = getParameterIndex(methodDecl, variableDecl) - if (index == -1) { - return - } - if (!isStatic) { - index++ - } - - val paramName = "p_${match.groupValues[1]}_${index}_" - context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, paramName, null) - } - - private fun handleConstructor(node: SimpleName, methodDecl: MethodDeclaration, method: IMethodBinding, variableDecl: VariableDeclaration) { - val constructorNodes = constructors[method.declaringClass.binaryName] ?: return - - val constructorNode = constructorNodes.firstOrNull { constructorNode -> - constructorNode.signature == BombeBindings.convertSignature(method).descriptor.toString() - } ?: return - - val id = constructorNode.id - - var index = getParameterIndex(methodDecl, variableDecl) - if (index == -1) { - return - } - // Constructors are never static - index++ - - val paramName = "p_i${id}_${index}_" - context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, paramName, null) - } - - private fun getParameterIndex(methodDecl: MethodDeclaration, decl: VariableDeclaration): Int { - @Suppress("UNCHECKED_CAST") - val params = methodDecl.parameters() as List - return params.indexOfFirst { it === decl } - } -} diff --git a/src/main/kotlin/tasks/RemapSpigotAt.kt b/src/main/kotlin/tasks/RemapSpigotAt.kt new file mode 100644 index 0000000..84b21ba --- /dev/null +++ b/src/main/kotlin/tasks/RemapSpigotAt.kt @@ -0,0 +1,135 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.file +import org.cadixdev.at.AccessChange +import org.cadixdev.at.AccessTransform +import org.cadixdev.at.AccessTransformSet +import org.cadixdev.at.ModifierChange +import org.cadixdev.at.io.AccessTransformFormats +import org.cadixdev.bombe.type.MethodDescriptor +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.io.MappingFormats +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.util.jar.JarFile + +abstract class RemapSpigotAt : BaseTask() { + + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:InputFile + abstract val spigotAt: RegularFileProperty + @get:InputFile + abstract val mapping: RegularFileProperty + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + override fun init() { + outputFile.convention(defaultOutput("at")) + } + + @TaskAction + fun run() { + val outputAt = AccessTransformSet.create() + + spigotAt.file.useLines { lines -> + JarFile(inputJar.file).use { jarFile -> + for (line in lines) { + if (line.isBlank() || line.startsWith('#')) { + continue + } + + val (access, desc) = line.split(' ') + + if (desc.contains('(')) { + // method + val index = desc.indexOf('(') + val methodDesc = desc.substring(index) + val classAndMethodName = desc.substring(0, index) + val slashIndex = classAndMethodName.lastIndexOf('/') + val className = classAndMethodName.substring(0, slashIndex) + val methodName = classAndMethodName.substring(slashIndex + 1) + + outputAt.getOrCreateClass(className).replaceMethod( + MethodSignature(methodName, MethodDescriptor.of(methodDesc)), + parseAccess(access) + ) + } else { + // either field or class + if (jarFile.getJarEntry("$desc.class") == null) { + // field + val index = desc.lastIndexOf('/') + val className = desc.substring(0, index) + val fieldName = desc.substring(index + 1) + outputAt.getOrCreateClass(className).replaceField(fieldName, parseAccess(access)) + } else { + // class + outputAt.getOrCreateClass(desc).replace(parseAccess(access)) + } + } + } + } + } + + val mappings = MappingFormats.TSRG.createReader(mapping.file.toPath()).use { it.read() } + val remappedAt = outputAt.remap(mappings) + + AccessTransformFormats.FML.write(outputFile.file.toPath(), remappedAt) + } + + private fun parseAccess(text: String): AccessTransform { + val index = text.indexOfAny(charArrayOf('+', '-')) + return if (index == -1) { + // only access + AccessTransform.of(parseAccessChange(text)) + } else { + val accessChange = parseAccessChange(text.substring(0, index)) + val modifierChange = parseModifierChange(text[index]) + AccessTransform.of(accessChange, modifierChange) + } + } + + private fun parseAccessChange(text: String): AccessChange { + return when (text) { + "public" -> AccessChange.PUBLIC + "private" -> AccessChange.PRIVATE + "protected" -> AccessChange.PROTECTED + "default" -> AccessChange.PACKAGE_PRIVATE + else -> AccessChange.NONE + } + } + + private fun parseModifierChange(c: Char): ModifierChange { + return when (c) { + '+' -> ModifierChange.ADD + '-' -> ModifierChange.REMOVE + else -> ModifierChange.NONE + } + } +} diff --git a/src/main/kotlin/tasks/RemapSrgSources.kt b/src/main/kotlin/tasks/RemapSrgSources.kt index e5505d7..cd24522 100644 --- a/src/main/kotlin/tasks/RemapSrgSources.kt +++ b/src/main/kotlin/tasks/RemapSrgSources.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -32,14 +31,14 @@ import java.io.BufferedWriter import java.io.File import java.util.regex.Pattern -open class RemapSrgSources : ZippedTask() { +abstract class RemapSrgSources : ZippedTask() { - @InputFile - val methodsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val fieldsCsv: RegularFileProperty = project.objects.fileProperty() - @InputFile - val paramsCsv: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val methodsCsv: RegularFileProperty + @get:InputFile + abstract val fieldsCsv: RegularFileProperty + @get:InputFile + abstract val paramsCsv: RegularFileProperty private val methods = hashMapOf() private val methodDocs = hashMapOf() diff --git a/src/main/kotlin/tasks/RemapVanillaJarSpigot.kt b/src/main/kotlin/tasks/RemapVanillaJarSpigot.kt index c232c63..984f641 100644 --- a/src/main/kotlin/tasks/RemapVanillaJarSpigot.kt +++ b/src/main/kotlin/tasks/RemapVanillaJarSpigot.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,52 +27,44 @@ import io.papermc.paperweight.util.cache import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.runJar import io.papermc.paperweight.util.wrapException -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property -open class RemapVanillaJarSpigot : DefaultTask() { +abstract class RemapVanillaJarSpigot : BaseTask() { - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:InputFile + abstract val classMappings: RegularFileProperty + @get:InputFile + abstract val memberMappings: RegularFileProperty + @get:InputFile + abstract val packageMappings: RegularFileProperty + @get:InputFile + abstract val accessTransformers: RegularFileProperty + @get:Input + abstract val workDirName: Property + @get:InputFile + abstract val specialSourceJar: RegularFileProperty + @get:InputFile + abstract val specialSource2Jar: RegularFileProperty + @get:Input + abstract val classMapCommand: Property + @get:Input + abstract val memberMapCommand: Property + @get:Input + abstract val finalMapCommand: Property - @InputFile - val classMappings: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val outputJar: RegularFileProperty - @InputFile - val memberMappings: RegularFileProperty = project.objects.fileProperty() - - @InputFile - val packageMappings: RegularFileProperty = project.objects.fileProperty() - - @InputFile - val accessTransformers: RegularFileProperty = project.objects.fileProperty() - - @Input - val workDirName: Property = project.objects.property() - - @InputFile - val specialSourceJar: RegularFileProperty = project.objects.fileProperty() - - @InputFile - val specialSource2Jar: RegularFileProperty = project.objects.fileProperty() - - @Input - val classMapCommand: Property = project.objects.property() - - @Input - val memberMapCommand: Property = project.objects.property() - - @Input - val finalMapCommand: Property = project.objects.property() - - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + override fun init() { + outputJar.convention(defaultOutput()) + } @TaskAction fun run() { @@ -92,14 +83,15 @@ open class RemapVanillaJarSpigot : DefaultTask() { val packageMappingsPath = packageMappings.asFile.get().canonicalPath val accessTransformersPath = accessTransformers.asFile.get().canonicalPath + val work = layout.projectDirectory.file(workDirName.get()) + try { - println("Applying class mappings...") wrapException("Failed to apply class mappings") { - val logFile = project.cache.resolve(paperTaskOutput("class.log")) + val logFile = layout.cache.resolve(paperTaskOutput("class.log")) logFile.delete() runJar( specialSource2Jar, - workingDir = workDirName.get(), + workingDir = work, logFile = logFile, args = *doReplacements(classMapCommand.get(), inputJarPath, classMappingPath, classJarPath) { // ignore excludes, we actually want to map every class @@ -107,24 +99,22 @@ open class RemapVanillaJarSpigot : DefaultTask() { } ) } - println("Applying member mappings...") wrapException("Failed to apply member mappings") { - val logFile = project.cache.resolve(paperTaskOutput("member.log")) + val logFile = layout.cache.resolve(paperTaskOutput("member.log")) logFile.delete() runJar( specialSource2Jar, - workingDir = workDirName.get(), + workingDir = work, logFile = logFile, args = *doReplacements(memberMapCommand.get(), classJarPath, memberMappingsPath, membersJarPath) ) } - println("Creating remapped jar...") wrapException("Failed to create remapped jar") { - val logFile = project.cache.resolve(paperTaskOutput("final.log")) + val logFile = layout.cache.resolve(paperTaskOutput("final.log")) logFile.delete() runJar( specialSourceJar, - workingDir = workDirName.get(), + workingDir = work, logFile = logFile, args = *doReplacements(finalMapCommand.get(), membersJarPath, accessTransformersPath, packageMappingsPath, outputJarPath) ) diff --git a/src/main/kotlin/tasks/RunForgeFlower.kt b/src/main/kotlin/tasks/RunForgeFlower.kt index 0ac48b2..84b2282 100644 --- a/src/main/kotlin/tasks/RunForgeFlower.kt +++ b/src/main/kotlin/tasks/RunForgeFlower.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,57 +23,67 @@ package io.papermc.paperweight.tasks import io.papermc.paperweight.util.Constants.paperTaskOutput +import io.papermc.paperweight.util.McpConfig import io.papermc.paperweight.util.cache +import io.papermc.paperweight.util.decompile import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.file -import io.papermc.paperweight.util.mcpConfig +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson import io.papermc.paperweight.util.runJar -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.property -open class RunForgeFlower : DefaultTask() { +abstract class RunForgeFlower : BaseTask() { - @Input - val configuration: Property = project.objects.property() + @get:InputFile + abstract val configFile: RegularFileProperty + @get:InputFile + abstract val executable: RegularFileProperty - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() - @InputFile - val libraries: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:InputFile + abstract val libraries: RegularFileProperty - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val outputJar: RegularFileProperty - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + override fun init() { + outputJar.convention(defaultOutput()) + } @TaskAction fun run() { - val config = mcpConfig(configFile) + val out = outputJar.file + val target = out.resolveSibling("${out.name}.dir") + if (target.exists()) { + target.deleteRecursively() + } + target.mkdirs() - val forgeFlowerArgs = config.functions.getValue("decompile").args - val jvmArgs = config.functions.getValue("decompile").jvmargs ?: listOf() + val config = gson.fromJson(configFile) - val argList = forgeFlowerArgs.map { + val argList = config.functions.decompile.args.map { when (it) { "{libraries}" -> libraries.file.absolutePath "{input}" -> inputJar.file.absolutePath - "{output}" -> outputJar.file.absolutePath + "{output}" -> target.absolutePath else -> it } } - val logFile = project.cache.resolve(paperTaskOutput("log")) + val logFile = layout.cache.resolve(paperTaskOutput("log")) logFile.delete() - val forgeFlowerJar = project.configurations[configuration.get()].resolve().single() - runJar(forgeFlowerJar, project.cache, logFile, jvmArgs = jvmArgs, args = *argList.toTypedArray()) + val jvmArgs = config.functions.decompile.jvmargs ?: listOf() + + runJar(executable.file, layout.cache, logFile, jvmArgs = jvmArgs, args = *argList.toTypedArray()) + + // FernFlower is weird with how it does directory output + target.resolve(inputJar.file.name).renameTo(out) + target.deleteRecursively() } } diff --git a/src/main/kotlin/tasks/RunMcInjector.kt b/src/main/kotlin/tasks/RunMcInjector.kt index bb04d9e..f3484c4 100644 --- a/src/main/kotlin/tasks/RunMcInjector.kt +++ b/src/main/kotlin/tasks/RunMcInjector.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,60 +22,65 @@ package io.papermc.paperweight.tasks +import io.papermc.paperweight.util.McpConfig import io.papermc.paperweight.util.cache import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.file -import io.papermc.paperweight.util.mcpConfig -import io.papermc.paperweight.util.mcpFile +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson +import io.papermc.paperweight.util.mcinject import io.papermc.paperweight.util.runJar -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.property -open class RunMcInjector : DefaultTask() { +abstract class RunMcInjector : BaseTask() { - @Input - val configuration: Property = project.objects.property() - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() - @InputFile - val configFile: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val configFile: RegularFileProperty + @get:InputFile + abstract val executable: RegularFileProperty - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() - @OutputFile - val logFile: RegularFileProperty = defaultOutput("log") + @get:InputFile + abstract val exceptions: RegularFileProperty + @get:InputFile + abstract val access: RegularFileProperty + @get:InputFile + abstract val constructors: RegularFileProperty + + @get:InputFile + abstract val inputJar: RegularFileProperty + + @get:OutputFile + abstract val outputJar: RegularFileProperty + @get:Internal + abstract val logFile: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + logFile.convention(defaultOutput("log")) + } @TaskAction fun run() { - val config = mcpConfig(configFile) - val exceptions = mcpFile(configFile, config.data.exceptions) - val access = mcpFile(configFile, config.data.access) - val constructors = mcpFile(configFile, config.data.constructors) + val config = gson.fromJson(configFile) - val mcInjectorArgs = config.functions.getValue("mcinject").args - val jvmArgs = config.functions.getValue("mcinject").jvmargs ?: listOf() - - val argList = mcInjectorArgs.map { + val argList = config.functions.mcinject.args.map { when (it) { "{input}" -> inputJar.file.absolutePath "{output}" -> outputJar.file.absolutePath "{log}" -> logFile.file.absolutePath - "{exceptions}" -> exceptions.absolutePath - "{access}" -> access.absolutePath - "{constructors}" -> constructors.absolutePath + "{exceptions}" -> exceptions.file.absolutePath + "{access}" -> access.file.absolutePath + "{constructors}" -> constructors.file.absolutePath else -> it } } - val mcInjectorJar = project.configurations[configuration.get()].resolve().single() + val jvmArgs = config.functions.mcinject.jvmargs ?: listOf() - runJar(mcInjectorJar, project.cache, logFile = null, jvmArgs = jvmArgs, args = *argList.toTypedArray()) + runJar(executable, layout.cache, logFile = null, jvmArgs = jvmArgs, args = *argList.toTypedArray()) } } diff --git a/src/main/kotlin/tasks/RunSpecialSource.kt b/src/main/kotlin/tasks/RunSpecialSource.kt new file mode 100644 index 0000000..1726b82 --- /dev/null +++ b/src/main/kotlin/tasks/RunSpecialSource.kt @@ -0,0 +1,84 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.Constants.paperTaskOutput +import io.papermc.paperweight.util.McpConfig +import io.papermc.paperweight.util.cache +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.ensureDeleted +import io.papermc.paperweight.util.ensureParentExists +import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson +import io.papermc.paperweight.util.rename +import io.papermc.paperweight.util.runJar +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class RunSpecialSource : BaseTask() { + + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:InputFile + abstract val mappings: RegularFileProperty + + @get:InputFile + abstract val configFile: RegularFileProperty + @get:InputFile + abstract val executable: RegularFileProperty + + @get:OutputFile + abstract val outputJar: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + } + + @TaskAction + fun run() { + val out = outputJar.file + ensureParentExists(out) + ensureDeleted(out) + + val config = gson.fromJson(configFile) + + val argList = config.functions.rename.args.map { + when (it) { + "{input}" -> inputJar.file.absolutePath + "{output}" -> outputJar.file.absolutePath + "{mappings}" -> mappings.file.absolutePath + else -> it + } + } + + val jvmArgs = config.functions.rename.jvmargs ?: listOf() + + val logFile = layout.cache.resolve(paperTaskOutput("log")) + logFile.delete() + + runJar(executable.file, layout.cache, logFile, jvmArgs = jvmArgs, args = *argList.toTypedArray()) + } +} diff --git a/src/main/kotlin/tasks/GatherBuildData.kt b/src/main/kotlin/tasks/SetupMcLibraries.kt similarity index 65% rename from src/main/kotlin/tasks/GatherBuildData.kt rename to src/main/kotlin/tasks/SetupMcLibraries.kt index 553edd2..ecb9b0d 100644 --- a/src/main/kotlin/tasks/GatherBuildData.kt +++ b/src/main/kotlin/tasks/SetupMcLibraries.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,22 +22,30 @@ package io.papermc.paperweight.tasks -import com.github.salomonbrys.kotson.fromJson -import io.papermc.paperweight.util.gson +import io.papermc.paperweight.util.file import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Provider +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile -import util.BuildDataInfo +import org.gradle.api.tasks.TaskAction -open class GatherBuildData : DefaultTask() { +abstract class SetupMcLibraries : DefaultTask() { - @OutputFile - val buildDataInfoFile: RegularFileProperty = project.objects.fileProperty() + @get:Input + abstract val dependencies: ListProperty - val buildDataInfo: Provider = buildDataInfoFile.map { - it.asFile.bufferedReader().use { reader -> - gson.fromJson(reader) + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @TaskAction + fun run() { + val list = dependencies.get().sorted() + + outputFile.file.bufferedWriter().use { writer -> + for (line in list) { + writer.appendln(line) + } } } } diff --git a/src/main/kotlin/tasks/SetupSpigotDependencies.kt b/src/main/kotlin/tasks/SetupSpigotDependencies.kt deleted file mode 100644 index 60ce187..0000000 --- a/src/main/kotlin/tasks/SetupSpigotDependencies.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * paperweight is a Gradle plugin for the PaperMC project. It uses - * some code and systems originally from ForgeGradle. - * - * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - */ - -package io.papermc.paperweight.tasks - -import io.papermc.paperweight.util.file -import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.property -import org.w3c.dom.Element -import java.io.File -import javax.xml.parsers.DocumentBuilderFactory - -open class SetupSpigotDependencies : DefaultTask() { - - @InputDirectory - val spigotApi: DirectoryProperty = project.objects.directoryProperty() - @InputDirectory - val spigotServer: DirectoryProperty = project.objects.directoryProperty() - - @Input - val configurationName: Property = project.objects.property() - - init { - // we set dependencies here, can't cache this - outputs.upToDateWhen { false } - } - - @TaskAction - fun run() { - val apiDeps = parsePom(spigotApi.file.resolve("pom.xml")) - val serverDeps = parsePom(spigotServer.file.resolve("pom.xml")) - - for (dep in apiDeps) { - project.dependencies.add(configurationName.get(), dep) - } - for (dep in serverDeps) { - project.dependencies.add(configurationName.get(), dep) - } - } - - private fun parsePom(pomFile: File): List { - val depList = arrayListOf() - - val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() - val doc = pomFile.inputStream().buffered().use { stream -> - stream.buffered().use { buffered -> - builder.parse(buffered) - } - } - - doc.documentElement.normalize() - - val list = doc.getElementsByTagName("dependencies") - for (i in 0 until list.length) { - val node = list.item(i) as? Element ?: continue - - val depNode = node.getElementsByTagName("dependency") - for (j in 0 until depNode.length) { - val dependency = depNode.item(j) as? Element ?: continue - val text = getDependency(dependency) ?: continue - depList += text - } - } - - return depList - } - - private fun getDependency(node: Element): String? { - val scopeNode = node.getElementsByTagName("scope") - val scope = if (scopeNode.length == 0) { - "compile" - } else { - scopeNode.item(0).textContent - } - - if (scope != "compile") { - return null - } - - val group = node.getElementsByTagName("groupId").item(0).textContent - val artifact = node.getElementsByTagName("artifactId").item(0).textContent - val version = node.getElementsByTagName("version").item(0).textContent - - if (version.contains("\${")) { - // Don't handle complicated things - // We don't need to (for now anyways) - return null - } - - return "$group:$artifact:$version" - } -} diff --git a/src/main/kotlin/tasks/WriteLibrariesFile.kt b/src/main/kotlin/tasks/WriteLibrariesFile.kt index ec4a058..ad541c7 100644 --- a/src/main/kotlin/tasks/WriteLibrariesFile.kt +++ b/src/main/kotlin/tasks/WriteLibrariesFile.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,32 +22,31 @@ package io.papermc.paperweight.tasks +import io.papermc.paperweight.PaperweightException import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.file -import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.property -open class WriteLibrariesFile : DefaultTask() { +abstract class WriteLibrariesFile : BaseTask() { - @Input - val config: Property = project.objects.property() + @get:Classpath + abstract val libraries: DirectoryProperty - @OutputFile - val outputFile: RegularFileProperty = defaultOutput("txt") + @get:OutputFile + abstract val outputFile: RegularFileProperty - init { - outputs.upToDateWhen { false } + override fun init() { + outputFile.convention(defaultOutput("txt")) } @TaskAction fun run() { - val files = project.configurations[config.get()].resolve().sorted() + val files = libraries.file.listFiles()?.sorted() + ?: throw PaperweightException("Libraries directory does not exist") outputFile.file.delete() outputFile.file.bufferedWriter().use { writer -> diff --git a/src/main/kotlin/tasks/ZippedTask.kt b/src/main/kotlin/tasks/ZippedTask.kt index 416c9e2..6de5b55 100644 --- a/src/main/kotlin/tasks/ZippedTask.kt +++ b/src/main/kotlin/tasks/ZippedTask.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,7 +28,6 @@ import io.papermc.paperweight.util.file import io.papermc.paperweight.util.fileOrNull import io.papermc.paperweight.util.unzip import io.papermc.paperweight.util.zip -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Optional @@ -38,17 +36,21 @@ import org.gradle.api.tasks.TaskAction import java.io.File import java.util.concurrent.ThreadLocalRandom -abstract class ZippedTask : DefaultTask() { +abstract class ZippedTask : BaseTask() { - @InputFile - @Optional - val inputZip: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + @get:Optional + abstract val inputZip: RegularFileProperty - @OutputFile - val outputZip: RegularFileProperty = defaultOutput("zip") + @get:OutputFile + abstract val outputZip: RegularFileProperty abstract fun run(rootDir: File) + override fun init() { + outputZip.convention(defaultOutput("zip")) + } + @TaskAction fun exec() { val outputZipFile = outputZip.file diff --git a/src/main/kotlin/tasks/download-task.kt b/src/main/kotlin/tasks/download-task.kt new file mode 100644 index 0000000..5cbc522 --- /dev/null +++ b/src/main/kotlin/tasks/download-task.kt @@ -0,0 +1,317 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.Constants +import io.papermc.paperweight.util.MavenArtifact +import io.papermc.paperweight.util.McpConfig +import io.papermc.paperweight.util.decompile +import io.papermc.paperweight.util.download +import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson +import io.papermc.paperweight.util.mcinject +import io.papermc.paperweight.util.rename +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.w3c.dom.Element +import java.io.File +import javax.inject.Inject +import javax.xml.parsers.DocumentBuilderFactory + +abstract class DownloadTask : DefaultTask() { + + @get:Input + abstract val url: Property + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @TaskAction + fun run() = download(url, outputFile) +} + +abstract class DownloadMcpFiles : DefaultTask() { + + @get:Input + abstract val mcpMinecraftVersion: Property + @get:Input + abstract val mcpConfigVersion: Property + @get:Input + abstract val mcpMappingsChannel: Property + @get:Input + abstract val mcpMappingsVersion: Property + + @get:OutputFile + abstract val configZip: RegularFileProperty + @get:OutputFile + abstract val mappingsZip: RegularFileProperty + + @TaskAction + fun run() { + val repo = listOf(Constants.FORGE_MAVEN_URL) + + MavenArtifact( + group = "de.oceanlabs.mcp", + artifact = "mcp_config", + version = mcpMinecraftVersion.get() + "-" + mcpConfigVersion.get(), + extension = "zip" + ).downloadToFile(configZip.file, repo) + + MavenArtifact( + group = "de.oceanlabs.mcp", + artifact = "mcp_${mcpMappingsChannel.get()}", + version = mcpMappingsVersion.get(), + extension = "zip" + ).downloadToFile(mappingsZip.file, repo) + } +} + +abstract class DownloadMcpTools : DefaultTask() { + + @get:InputFile + abstract val configFile: RegularFileProperty + + @get:OutputFile + abstract val forgeFlowerFile: RegularFileProperty + @get:OutputFile + abstract val mcInjectorFile: RegularFileProperty + @get:OutputFile + abstract val specialSourceFile: RegularFileProperty + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @TaskAction + fun run() { + val config = gson.fromJson(configFile) + + val queue = workerExecutor.noIsolation() + queue.submit(DownloadWorker::class.java) { + repos.add(config.functions.decompile.repo) + artifact.set(config.functions.decompile.version) + target.set(forgeFlowerFile.file) + downloadToDir.set(false) + } + queue.submit(DownloadWorker::class.java) { + repos.add(config.functions.mcinject.repo) + artifact.set(config.functions.mcinject.version) + target.set(mcInjectorFile.file) + downloadToDir.set(false) + } + queue.submit(DownloadWorker::class.java) { + repos.add(config.functions.rename.repo) + artifact.set(config.functions.rename.version) + target.set(specialSourceFile.file) + downloadToDir.set(false) + } + } +} + +abstract class DownloadMcLibraries : DefaultTask() { + + @get:InputFile + abstract val mcLibrariesFile: RegularFileProperty + @get:Input + abstract val mcRepo: Property + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @TaskAction + fun run() { + val out = outputDir.file + out.deleteRecursively() + + val mcRepos = listOf(mcRepo.get()) + + val queue = workerExecutor.noIsolation() + mcLibrariesFile.file.useLines { lines -> + lines.forEach { line -> + queue.submit(DownloadWorker::class.java) { + repos.set(mcRepos) + artifact.set(line) + target.set(out) + downloadToDir.set(true) + } + } + } + } +} + +abstract class DownloadSpigotDependencies : BaseTask() { + + @get:InputFile + abstract val apiPom: RegularFileProperty + @get:InputFile + abstract val serverPom: RegularFileProperty + + @get:OutputDirectory + abstract val apiOutputDir: DirectoryProperty + @get:OutputDirectory + abstract val serverOutputDir: DirectoryProperty + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @TaskAction + fun run() { + val apiSetup = parsePom(apiPom.file) + val serverSetup = parsePom(serverPom.file) + + val apiOut = apiOutputDir.file + apiOut.deleteRecursively() + + val serverOut = serverOutputDir.file + serverOut.deleteRecursively() + + val queue = workerExecutor.noIsolation() + for (art in apiSetup.artifacts) { + queue.submit(DownloadWorker::class.java) { + repos.set(apiSetup.repos) + artifact.set(art.toString()) + target.set(apiOut) + downloadToDir.set(true) + } + } + for (art in serverSetup.artifacts) { + queue.submit(DownloadWorker::class.java) { + repos.set(serverSetup.repos) + artifact.set(art.toString()) + target.set(serverOut) + downloadToDir.set(true) + } + } + } + + private fun parsePom(pomFile: File): MavenSetup { + val depList = arrayListOf() + val repoList = arrayListOf() + // Maven Central is implicit + repoList += "https://repo.maven.apache.org/maven2/" + + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = pomFile.inputStream().buffered().use { stream -> + stream.buffered().use { buffered -> + builder.parse(buffered) + } + } + + doc.documentElement.normalize() + + val list = doc.getElementsByTagName("dependencies") + for (i in 0 until list.length) { + val node = list.item(i) as? Element ?: continue + + val depNode = node.getElementsByTagName("dependency") + for (j in 0 until depNode.length) { + val dependency = depNode.item(j) as? Element ?: continue + val artifact = getDependency(dependency) ?: continue + depList += artifact + } + } + + val repos = doc.getElementsByTagName("repositories") + for (i in 0 until repos.length) { + val node = repos.item(i) as? Element ?: continue + val depNode = node.getElementsByTagName("repository") + for (j in 0 until depNode.length) { + val repo = depNode.item(j) as? Element ?: continue + val repoUrl = repo.getElementsByTagName("url").item(0).textContent + repoList += repoUrl + } + } + + return MavenSetup(repos = repoList, artifacts = depList) + } + + private fun getDependency(node: Element): MavenArtifact? { + val scopeNode = node.getElementsByTagName("scope") + val scope = if (scopeNode.length == 0) { + "compile" + } else { + scopeNode.item(0).textContent + } + + if (scope != "compile") { + return null + } + + val group = node.getElementsByTagName("groupId").item(0).textContent + val artifact = node.getElementsByTagName("artifactId").item(0).textContent + val version = node.getElementsByTagName("version").item(0).textContent + + if (version.contains("\${")) { + // Don't handle complicated things + // We don't need to (for now anyways) + return null + } + + return MavenArtifact( + group = group, + artifact = artifact, + version = version + ) + } +} + +data class MavenSetup( + val repos: List, + val artifacts: List +) + +interface DownloadParams : WorkParameters { + val repos: ListProperty + val artifact: Property + val target: RegularFileProperty + val downloadToDir: Property +} +abstract class DownloadWorker : WorkAction { + @get:Inject + abstract val layout: ProjectLayout + + override fun execute() { + val artifact = MavenArtifact.parse(parameters.artifact.get()) + if (parameters.downloadToDir.get()) { + artifact.downloadToDir(parameters.target.file, parameters.repos.get()) + } else { + artifact.downloadToFile(parameters.target.file, parameters.repos.get()) + } + } +} + diff --git a/src/main/kotlin/tasks/extract-tasks.kt b/src/main/kotlin/tasks/extract-tasks.kt new file mode 100644 index 0000000..b45d8d9 --- /dev/null +++ b/src/main/kotlin/tasks/extract-tasks.kt @@ -0,0 +1,127 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.McpConfig +import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.fromJson +import io.papermc.paperweight.util.gson +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class Extract : BaseTask() { + + @get:InputFile + abstract val inputFile: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + open fun run() { + val input = inputFile.file + val output = outputDir.file + if (output.exists()) { + output.deleteRecursively() + } + output.mkdirs() + fs.copy { + from(archives.zipTree(input)) + into(output) + } + } +} + +abstract class ExtractMcp : Extract() { + + @get:OutputFile + abstract val configFile: RegularFileProperty + + @get:OutputFile + abstract val access: RegularFileProperty + @get:OutputFile + abstract val constructors: RegularFileProperty + @get:OutputFile + abstract val exceptions: RegularFileProperty + @get:OutputFile + abstract val mappings: RegularFileProperty + @get:OutputDirectory + abstract val patchDir: DirectoryProperty + + override fun init() { + configFile.convention(outputDir.file("config.json")) + access.convention(outputDir.file("config/access.txt")) + constructors.convention(outputDir.file("config/constructors.txt")) + exceptions.convention(outputDir.file("config/exceptions.txt")) + mappings.convention(outputDir.file("config/joined.tsrg")) + patchDir.convention(outputDir.dir("config/patches/server")) + } + + @TaskAction + override fun run() { + super.run() + + val output = outputDir.file + val config = gson.fromJson(output.resolve("config.json")) + + // We have to know what our output file paths are at configuration time, but these could change based on the + // config.json file. + // So as a workaround we just rename the files the config.json file points to to our expected paths. Likely + // is a no-op. + + output.resolve(config.data.access).renameTo(access.file.createParent()) + output.resolve(config.data.constructors).renameTo(constructors.file.createParent()) + output.resolve(config.data.exceptions).renameTo(exceptions.file.createParent()) + output.resolve(config.data.mappings).renameTo(mappings.file.createParent()) + output.resolve(config.data.patches.server).renameTo(patchDir.file.createParent()) + } + + private fun File.createParent(): File { + val par = this.parentFile + if (!par.exists()) { + par.mkdirs() + } + return this + } +} + +abstract class ExtractMappings : Extract() { + + @get:OutputFile + abstract val fieldsCsv: RegularFileProperty + @get:OutputFile + abstract val methodsCsv: RegularFileProperty + @get:OutputFile + abstract val paramsCsv: RegularFileProperty + + override fun init() { + fieldsCsv.convention(outputDir.file("fields.csv")) + methodsCsv.convention(outputDir.file("methods.csv")) + paramsCsv.convention(outputDir.file("params.csv")) + } +} diff --git a/src/main/kotlin/tasks/jar-tasks.kt b/src/main/kotlin/tasks/jar-tasks.kt new file mode 100644 index 0000000..c90eca4 --- /dev/null +++ b/src/main/kotlin/tasks/jar-tasks.kt @@ -0,0 +1,100 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks + +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.zip +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class Filter : BaseTask() { + + @get:InputFile + abstract val inputJar: RegularFileProperty + @get:Input + abstract val includes: ListProperty + + @get:OutputFile + abstract val outputJar: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + } + + @TaskAction + fun run() { + val out = outputJar.file + val target = out.resolveSibling("${out.name}.dir") + target.mkdirs() + + fs.copy { + from(archives.zipTree(inputJar)) { + for (inc in this@Filter.includes.get()) { + include(inc) + } + } + into(target) + } + + zip(target, outputJar) + target.deleteRecursively() + } +} + +abstract class Merge : BaseTask() { + @get:InputFiles + abstract val inputJars: ListProperty + + @get:OutputFile + abstract val outputJar: RegularFileProperty + + override fun init() { + outputJar.convention(defaultOutput()) + } + + @TaskAction + fun run() { + val out = outputJar.file + val target = out.resolveSibling("${out.name}.dir") + target.mkdirs() + + fs.copy { + for (file in inputJars.get()) { + from(archives.zipTree(file)) + } + into(target) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + + zip(target, outputJar) + target.deleteRecursively() + } +} diff --git a/src/main/kotlin/tasks/ApplyAccessTransform.kt b/src/main/kotlin/tasks/patchremap/ApplyAccessTransform.kt similarity index 73% rename from src/main/kotlin/tasks/ApplyAccessTransform.kt rename to src/main/kotlin/tasks/patchremap/ApplyAccessTransform.kt index 624881f..789f2d9 100644 --- a/src/main/kotlin/tasks/ApplyAccessTransform.kt +++ b/src/main/kotlin/tasks/patchremap/ApplyAccessTransform.kt @@ -1,5 +1,28 @@ -package io.papermc.paperweight.tasks +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ +package io.papermc.paperweight.tasks.patchremap + +import io.papermc.paperweight.tasks.BaseTask import io.papermc.paperweight.util.defaultOutput import io.papermc.paperweight.util.ensureDeleted import io.papermc.paperweight.util.ensureParentExists @@ -14,11 +37,8 @@ import org.cadixdev.atlas.Atlas import org.cadixdev.bombe.jar.JarClassEntry import org.cadixdev.bombe.jar.JarEntryTransformer import org.cadixdev.bombe.type.signature.MethodSignature -import org.cadixdev.lorenz.io.MappingFormats -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import org.objectweb.asm.ClassReader @@ -28,19 +48,20 @@ import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes -open class ApplyAccessTransform : DefaultTask() { +abstract class ApplyAccessTransform : BaseTask() { - @InputFile - val inputJar: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val inputJar: RegularFileProperty - @InputFile - val atFile: RegularFileProperty = project.objects.fileProperty() + @get:InputFile + abstract val atFile: RegularFileProperty - @InputFile - val mapping: RegularFileProperty = project.objects.fileProperty() + @get:OutputFile + abstract val outputJar: RegularFileProperty - @OutputFile - val outputJar: RegularFileProperty = defaultOutput() + override fun init() { + outputJar.convention(defaultOutput()) + } @TaskAction fun run() { @@ -48,12 +69,10 @@ open class ApplyAccessTransform : DefaultTask() { ensureDeleted(outputJar.file) val at = AccessTransformFormats.FML.read(atFile.file.toPath()) - val mappings = MappingFormats.TSRG.createReader(mapping.file.toPath()).use { it.read() } - val remappedAt = at.remap(mappings) Atlas().apply { install { - AtJarEntryTransformer(remappedAt) + AtJarEntryTransformer(at) } run(inputJar.file.toPath(), outputJar.file.toPath()) } diff --git a/src/main/kotlin/tasks/patchremap/PatchApplier.kt b/src/main/kotlin/tasks/patchremap/PatchApplier.kt new file mode 100644 index 0000000..5ad6477 --- /dev/null +++ b/src/main/kotlin/tasks/patchremap/PatchApplier.kt @@ -0,0 +1,91 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.patchremap + +import io.papermc.paperweight.PaperweightException +import io.papermc.paperweight.util.Git +import java.io.File + +class PatchApplier( + private val remappedBranch: String, + private val unmappedBranch: String, + targetDir: File +) { + + private val git = Git(targetDir) + + private var commitMessage: String? = null + private var commitAuthor: String? = null + private var commitTime: String? = null + + fun initRepo() { + println("Initializing patch remap repo") + git("init").executeSilently() + git("commit", "-m", "Initial", "--author=Initial ", "--allow-empty").executeSilently() + git("branch", remappedBranch).executeSilently() + git("branch", unmappedBranch).executeSilently() + git("checkout", unmappedBranch).executeSilently() + } + + fun checkoutRemapped() { + println("Switching to $remappedBranch without losing changes") + git("symbolic-ref", "HEAD", "refs/heads/$remappedBranch").executeSilently() + } + + fun checkoutOld() { + println("Resetting back to $unmappedBranch branch") + git("checkout", unmappedBranch).executeSilently() + } + + fun commitInitialSource() { + git("add", ".").executeSilently() + git("commit", "-m", "Initial Source", "--author=Initial ").executeSilently() + } + + fun recordCommit() { + commitMessage = git("git", "log", "--format=%B", "-n", "1", "HEAD").getText() + commitAuthor = git("git", "log", "--format=%an <%ae>", "-n", "1", "HEAD").getText() + commitTime = git("git", "log", "--format=%aD", "-n", "1", "HEAD").getText() + } + + fun commitChanges() { + println("Committing remapped changes to $remappedBranch") + val message = commitMessage ?: throw PaperweightException("commitMessage not set") + val author = commitAuthor ?: throw PaperweightException("commitAuthor not set") + val time = commitTime ?: throw PaperweightException("commitTime not set") + commitMessage = null + commitAuthor = null + commitTime = null + + git("add", ".").executeSilently() + git("commit", "-m", message, "--author=$author", "--date=$time").execute() + } + + fun applyPatch(patch: File) { + println("Applying patch ${patch.name}") + val result = git("am", "--3way", "--ignore-whitespace", patch.absolutePath).runOut() + if (result != 0) { + System.err.println("Patch failed to apply: $patch") + } + } +} diff --git a/src/main/kotlin/tasks/patchremap/PatchSourceRemapWorker.kt b/src/main/kotlin/tasks/patchremap/PatchSourceRemapWorker.kt new file mode 100644 index 0000000..42dbdfd --- /dev/null +++ b/src/main/kotlin/tasks/patchremap/PatchSourceRemapWorker.kt @@ -0,0 +1,96 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.patchremap + +import io.papermc.paperweight.tasks.sourceremap.ConstructorsData +import io.papermc.paperweight.tasks.sourceremap.ParamNames +import io.papermc.paperweight.tasks.sourceremap.PatchParameterRemapper +import io.papermc.paperweight.tasks.sourceremap.SrgParameterRemapper +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.mercury.Mercury +import org.cadixdev.mercury.remapper.MercuryRemapper +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.REPLACE_EXISTING + +class PatchSourceRemapWorker( + private val mappings: MappingSet, + private val classpath: Collection, + private val paramNames: ParamNames, + private val constructorsData: ConstructorsData, + private val inputDir: Path, + private val outputDir: Path +) { + + private val reverseMappings: MappingSet = mappings.reverse() + + fun remap() { + setup() + + Mercury().apply { + classPath.addAll(classpath) + + processors.addAll(listOf( + MercuryRemapper.create(reverseMappings), + PatchParameterRemapper(paramNames, constructorsData) + )) + + rewrite(inputDir, outputDir) + } + + cleanup() + } + + fun remapBack() { + setup() + + Mercury().apply { + classPath.addAll(classpath) + + processors.addAll(listOf( + MercuryRemapper.create(mappings), + SrgParameterRemapper(mappings, constructorsData, paramNames) + )) + + rewrite(inputDir, outputDir) + } + + cleanup() + } + + private fun setup() { + Files.walk(outputDir).use { it.forEach(Files::delete) } + Files.createDirectories(outputDir) + } + + private fun cleanup() { + Files.walk(inputDir).use { it.forEach(Files::delete) } + Files.walk(outputDir).use { + it.forEach { src -> + val dest = inputDir.resolve(outputDir.relativize(src)) + Files.createDirectories(dest.parent) + Files.copy(src, dest, REPLACE_EXISTING) + } + } + } +} diff --git a/src/main/kotlin/tasks/patchremap/RemapPatches.kt b/src/main/kotlin/tasks/patchremap/RemapPatches.kt new file mode 100644 index 0000000..9d5c524 --- /dev/null +++ b/src/main/kotlin/tasks/patchremap/RemapPatches.kt @@ -0,0 +1,187 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.patchremap + +import io.papermc.paperweight.tasks.BaseTask +import io.papermc.paperweight.tasks.sourceremap.parseConstructors +import io.papermc.paperweight.tasks.sourceremap.parseParamNames +import io.papermc.paperweight.util.cache +import io.papermc.paperweight.util.file +import org.cadixdev.lorenz.io.MappingFormats +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.get +import java.io.File +import java.util.zip.ZipFile + +abstract class RemapPatches : BaseTask() { + + @get:InputDirectory + abstract val inputPatchDir: DirectoryProperty + @get:InputFile + abstract val sourceJar: RegularFileProperty + @get:InputDirectory + abstract val apiPatchDir: DirectoryProperty + + @get:InputFile + abstract val mappingsFile: RegularFileProperty + + @get:Classpath + abstract val classpathJars: ListProperty + + @get:InputDirectory + abstract val spigotApiDir: DirectoryProperty + @get:InputDirectory + abstract val spigotServerDir: DirectoryProperty + @get:InputFile + abstract val spigotDecompJar: RegularFileProperty + + // For parameter name remapping + @get:InputFile + abstract val parameterNames: RegularFileProperty + @get:InputFile + abstract val constructors: RegularFileProperty + + @get:OutputDirectory + abstract val outputPatchDir: DirectoryProperty + + @TaskAction + fun run() { + // Check patches + val patches = inputPatchDir.file.listFiles() ?: return run { + println("No input patches found") + } + + patches.sort() + + // Setup param remapping + val constructorsData = parseConstructors(constructors.file) + val paramMap = parseParamNames(parameterNames.file) + + val mappings = MappingFormats.TSRG.createReader(mappingsFile.file.toPath()).use { it.read() } + + // This should pull in any libraries needed for type bindings + val configFiles = project.project(":Paper-Server").configurations["runtimeClasspath"].resolve() + val classpathFiles = classpathJars.get().map { it.asFile } + configFiles + + // Remap output directory, after each output this directory will be re-named to the input directory below for + // the next remap operation + val tempApiDir = createWorkDir("patch-remap-api", source = spigotApiDir.file) + val tempInputDir = createWorkDir("patch-remap-input", source = spigotServerDir.file) + val tempOutputDir = createWorkDir("patch-remap-output") + + val sourceInputDir = tempInputDir.resolve("src/main/java") + sourceInputDir.deleteRecursively() + sourceInputDir.mkdirs() + + project.copy { + from(project.zipTree(sourceJar.file)) + into(sourceInputDir) + } + + tempInputDir.resolve(".git").deleteRecursively() + + PatchSourceRemapWorker( + mappings, + listOf(*classpathFiles.toTypedArray(), tempApiDir.resolve("src/main/java")).map { it.toPath() }, + paramMap, + constructorsData, + sourceInputDir.toPath(), + tempOutputDir.toPath() + ).let { remapper -> + val patchApplier = PatchApplier("remapped", "old", tempInputDir) + // Setup patch remapping repo + patchApplier.initRepo() // Create empty initial commit + remapper.remap() // Remap to Spigot mappings + + // We need to include any missing classes for the patches later on + importMcDev(patches, tempInputDir.resolve("src/main/java")) + patchApplier.commitInitialSource() // Initial commit of Spigot sources + patchApplier.checkoutRemapped() // Switch to remapped branch without checking out files + + remapper.remapBack() // Remap to new mappings + patchApplier.commitInitialSource() // Initial commit of Spigot sources mapped to new mappings + patchApplier.checkoutOld() // Normal checkout back to Spigot mappings branch + + // Repo setup is done, we can begin the patch "loop" now + // - not a loop yet cause it doesn't even work for the first patch + remapper.remap() // Remap to to Spigot mappings TODO: verify this step produces correct results + patchApplier.applyPatch(patches.first()) // Apply patch on Spigot mappings + patchApplier.recordCommit() // Keep track of commit author, message, and time + patchApplier.checkoutRemapped() // Switch to remapped branch without checkout out files + remapper.remapBack() // Remap to new mappings + patchApplier.commitChanges() // Commit the changes + patchApplier.checkoutOld() // Normal checkout back to Spigot mappings branch + } + } + + private fun importMcDev(patches: Array, inputDir: File) { + val importMcDev = readMcDevNames(patches).asSequence() + .map { inputDir.resolve("net/minecraft/server/$it.java") } + .filter { !it.exists() } + .toSet() + ZipFile(spigotDecompJar.file).use { zipFile -> + for (file in importMcDev) { + val zipEntry = zipFile.getEntry(file.relativeTo(inputDir).path) ?: continue + zipFile.getInputStream(zipEntry).use { input -> + file.outputStream().buffered().use { output -> + input.copyTo(output) + } + } + } + } + } + + private fun readMcDevNames(patches: Array): Set { + val result = hashSetOf() + + val prefix = "+++ b/src/main/java/net/minecraft/server/" + val suffix = ".java" + + for (patch in patches) { + patch.useLines { lines -> + lines + .filter { it.startsWith(prefix) } + .map { it.substring(prefix.length, it.length - suffix.length) } + .forEach { result.add(it) } + } + } + + return result + } + + private fun createWorkDir(name: String, source: File? = null): File { + return layout.cache.resolve("paperweight").resolve(name).apply { + deleteRecursively() + mkdirs() + source?.copyRecursively(this) + } + } +} diff --git a/src/main/kotlin/tasks/sourceremap/AbstractParameterVisitor.kt b/src/main/kotlin/tasks/sourceremap/AbstractParameterVisitor.kt new file mode 100644 index 0000000..280044f --- /dev/null +++ b/src/main/kotlin/tasks/sourceremap/AbstractParameterVisitor.kt @@ -0,0 +1,74 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.sourceremap + +import org.cadixdev.mercury.RewriteContext +import org.eclipse.jdt.core.dom.ASTVisitor +import org.eclipse.jdt.core.dom.IMethodBinding +import org.eclipse.jdt.core.dom.IVariableBinding +import org.eclipse.jdt.core.dom.MethodDeclaration +import org.eclipse.jdt.core.dom.SimpleName +import org.eclipse.jdt.core.dom.VariableDeclaration + +abstract class AbstractParameterVisitor(protected val context: RewriteContext) : ASTVisitor() { + + override fun visit(node: SimpleName): Boolean { + val binding = node.resolveBinding() as? IVariableBinding ?: return false + if (!binding.isParameter) { + return false + } + + val variableDecl = context.compilationUnit.findDeclaringNode(binding.variableDeclaration) as VariableDeclaration + + val method = binding.declaringMethod + val methodDecl = context.compilationUnit.findDeclaringNode(method) as? MethodDeclaration ?: return false + + if (method.isConstructor) { + handleConstructor(node, methodDecl, method, variableDecl) + } else { + handleMethod(node, methodDecl, method, variableDecl) + } + + return false + } + + abstract fun handleMethod( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) + + abstract fun handleConstructor( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) + + fun getParameterIndex(methodDecl: MethodDeclaration, decl: VariableDeclaration): Int { + @Suppress("UNCHECKED_CAST") + val params = methodDecl.parameters() as List + return params.indexOfFirst { it === decl } + } +} diff --git a/src/main/kotlin/tasks/sourceremap/PatchParameterRemapper.kt b/src/main/kotlin/tasks/sourceremap/PatchParameterRemapper.kt new file mode 100644 index 0000000..121e538 --- /dev/null +++ b/src/main/kotlin/tasks/sourceremap/PatchParameterRemapper.kt @@ -0,0 +1,95 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.sourceremap + +import io.papermc.paperweight.PaperweightException +import org.cadixdev.mercury.RewriteContext +import org.cadixdev.mercury.SourceProcessor +import org.cadixdev.mercury.SourceRewriter +import org.cadixdev.mercury.util.BombeBindings +import org.eclipse.jdt.core.dom.IMethodBinding +import org.eclipse.jdt.core.dom.MethodDeclaration +import org.eclipse.jdt.core.dom.SimpleName +import org.eclipse.jdt.core.dom.VariableDeclaration + +class PatchParameterRemapper( + private val paramNames: ParamNames, + private val constructorsData: ConstructorsData +) : SourceRewriter { + override fun getFlags(): Int = SourceProcessor.FLAG_RESOLVE_BINDINGS + + override fun rewrite(context: RewriteContext) { + context.compilationUnit.accept(PatchParameterVisitor(context, paramNames, constructorsData)) + } +} + +class PatchParameterVisitor( + context: RewriteContext, + private val paramNames: ParamNames, + private val constructorsData: ConstructorsData +) : AbstractParameterVisitor(context) { + + override fun handleMethod( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) { + val paramNames = paramNames[methodDecl.name.identifier] ?: return + val params = methodDecl.parameters() + + if (paramNames.size != params.size) { + throw PaperweightException("Invalid parameter length; expected ${paramNames.size}, actual ${params.size} " + + "for method ${methodDecl.name.identifier}") + } + + val index = getParameterIndex(methodDecl, variableDecl) + val newName = paramNames[index] ?: return + + context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, newName, null) + } + + override fun handleConstructor( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) { + val className = method.declaringClass.binaryName.replace('.', '/') + val descriptor = BombeBindings.convertSignature(method).descriptor + + val constructorNode = constructorsData.findConstructorNode(className, descriptor) ?: return + val paramNames = paramNames["const_${constructorNode.id}"] ?: return + val params = methodDecl.parameters() + + if (paramNames.size != params.size) { + throw PaperweightException("Invalid parameter length; expected ${paramNames.size}, actual ${params.size} " + + "for constructor $className $descriptor") + } + + val index = getParameterIndex(methodDecl, variableDecl) + val newName = paramNames[index] + + context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, newName, null) + } +} diff --git a/src/main/kotlin/tasks/sourceremap/RemapSources.kt b/src/main/kotlin/tasks/sourceremap/RemapSources.kt new file mode 100644 index 0000000..91a469a --- /dev/null +++ b/src/main/kotlin/tasks/sourceremap/RemapSources.kt @@ -0,0 +1,114 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.sourceremap + +import io.papermc.paperweight.tasks.ZippedTask +import io.papermc.paperweight.util.defaultOutput +import io.papermc.paperweight.util.file +import io.papermc.paperweight.util.path +import org.cadixdev.at.AccessTransformSet +import org.cadixdev.at.io.AccessTransformFormats +import org.cadixdev.lorenz.io.MappingFormats +import org.cadixdev.mercury.Mercury +import org.cadixdev.mercury.at.AccessTransformerRewriter +import org.cadixdev.mercury.extra.AccessAnalyzerProcessor +import org.cadixdev.mercury.extra.BridgeMethodRewriter +import org.cadixdev.mercury.remapper.MercuryRemapper +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import java.io.File + +abstract class RemapSources : ZippedTask() { + + @get:InputFile + abstract val vanillaJar: RegularFileProperty + @get:InputFile + abstract val vanillaRemappedSpigotJar: RegularFileProperty // Required for pre-remap pass + @get:InputFile + abstract val mappings: RegularFileProperty + + @get:InputDirectory + abstract val spigotApiDeps: DirectoryProperty + @get:InputDirectory + abstract val spigotServerDeps: DirectoryProperty + + @get:InputFile + abstract val constructors: RegularFileProperty + @get:InputDirectory + abstract val spigotServerDir: DirectoryProperty + @get:InputDirectory + abstract val spigotApiDir: DirectoryProperty + + @get:OutputFile + abstract val generatedAt: RegularFileProperty + @get:OutputFile + abstract val parameterNames: RegularFileProperty + + override fun init() { + super.init() + generatedAt.convention(defaultOutput("at")) + parameterNames.convention(defaultOutput("params")) + } + + override fun run(rootDir: File) { + val constructorsData = parseConstructors(constructors.file) + + val paramNames: ParamNames = newParamNames() + + val srcDir = spigotServerDir.file.resolve("src/main/java") + + val mappingSet = MappingFormats.TSRG.read(mappings.path) + val processAt = AccessTransformSet.create() + + // Remap any references Spigot maps to SRG + Mercury().apply { + classPath.addAll(listOf( + vanillaJar.path, + vanillaRemappedSpigotJar.path, + spigotApiDir.path.resolve("src/main/java"), + *spigotApiDeps.get().asFileTree.files.map { it.toPath() }.toTypedArray(), + *spigotServerDeps.get().asFileTree.files.map { it.toPath() }.toTypedArray() + )) + + processors += AccessAnalyzerProcessor.create(processAt, mappingSet) + + process(srcDir.toPath()) + + processors.clear() + processors.addAll(listOf( + MercuryRemapper.create(mappingSet), + BridgeMethodRewriter.create(), + AccessTransformerRewriter.create(processAt), + SrgParameterRemapper(mappingSet, constructorsData, paramNames) + )) + + rewrite(srcDir.toPath(), rootDir.toPath()) + } + + AccessTransformFormats.FML.write(generatedAt.path, processAt) + writeParamNames(paramNames, parameterNames.file) + } +} diff --git a/src/main/kotlin/tasks/sourceremap/SrgParameterRemapper.kt b/src/main/kotlin/tasks/sourceremap/SrgParameterRemapper.kt new file mode 100644 index 0000000..a44068b --- /dev/null +++ b/src/main/kotlin/tasks/sourceremap/SrgParameterRemapper.kt @@ -0,0 +1,142 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.sourceremap + +import org.cadixdev.bombe.type.MethodDescriptor +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.mercury.RewriteContext +import org.cadixdev.mercury.SourceProcessor +import org.cadixdev.mercury.SourceRewriter +import org.cadixdev.mercury.util.BombeBindings +import org.eclipse.jdt.core.dom.IMethodBinding +import org.eclipse.jdt.core.dom.MethodDeclaration +import org.eclipse.jdt.core.dom.Modifier +import org.eclipse.jdt.core.dom.SimpleName +import org.eclipse.jdt.core.dom.VariableDeclaration + +class SrgParameterRemapper( + private val mappings: MappingSet, + private val constructorsData: ConstructorsData, + private val parameterNames: ParamNames? = null +) : SourceRewriter { + + override fun getFlags(): Int = SourceProcessor.FLAG_RESOLVE_BINDINGS + + override fun rewrite(context: RewriteContext) { + context.compilationUnit.accept(SrgParameterVisitor(context, mappings, constructorsData, parameterNames)) + } +} + +class SrgParameterVisitor( + context: RewriteContext, + private val mappings: MappingSet, + private val constructorsData: ConstructorsData, + private val paramNames: ParamNames? +) : AbstractParameterVisitor(context) { + + companion object { + private val MATCHER = Regex("func_(\\d+)_.*") + } + + override fun handleMethod( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) { + val methodName = mappings.getClassMapping(method.declaringClass.binaryName) + .flatMap { it.getMethodMapping(BombeBindings.convertSignature(method)) } + .map { it.deobfuscatedName } + .orElse(null) ?: return + + val match = MATCHER.matchEntire(methodName) ?: return + val isStatic = method.modifiers and Modifier.STATIC != 0 + + var index = getParameterIndex(methodDecl, variableDecl) + if (index == -1) { + return + } + + recordName(methodName, method, node, index) + + if (!isStatic) { + index++ + } + + val paramName = "p_${match.groupValues[1]}_${index}_" + context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, paramName, null) + } + + override fun handleConstructor( + node: SimpleName, + methodDecl: MethodDeclaration, + method: IMethodBinding, + variableDecl: VariableDeclaration + ) { + val binaryName = method.declaringClass.binaryName + val classMapping = mappings.getClassMapping(binaryName) + val className = classMapping + .map { it.fullDeobfuscatedName } + .orElse(binaryName) + + val descriptor = BombeBindings.convertSignature(method).descriptor + val constructorNode = constructorsData.findConstructorNode(className, descriptor) ?: return + + val id = constructorNode.id + + var index = getParameterIndex(methodDecl, variableDecl) + if (index == -1) { + return + } + + recordName("const_$id", method, node, index) + + // Constructors are never static + index++ + + val paramName = "p_i${id}_${index}_" + context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, paramName, null) + } + + private fun recordName( + methodName: String, + method: IMethodBinding, + node: SimpleName, + index: Int + ) { + paramNames?.let { map -> + val paramCount = method.parameterTypes.size + map.computeIfAbsent(methodName) { arrayOfNulls(paramCount) }[index] = node.identifier + } + } +} + +fun ConstructorsData.findConstructorNode(className: String, desc: MethodDescriptor): ConstructorNode? { + val constructorNodes = constructors[className] ?: return null + + val descriptorText = desc.toString() + return constructorNodes.firstOrNull { constructorNode -> + constructorNode.descriptor == descriptorText + } +} + diff --git a/src/main/kotlin/tasks/sourceremap/remap.kt b/src/main/kotlin/tasks/sourceremap/remap.kt new file mode 100644 index 0000000..0a901d3 --- /dev/null +++ b/src/main/kotlin/tasks/sourceremap/remap.kt @@ -0,0 +1,90 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.tasks.sourceremap + +import java.io.File + +data class ConstructorsData(val constructors: Map>) + +data class ConstructorNode( + val id: Int, + val descriptor: String +) + +fun parseConstructors(constructors: File): ConstructorsData { + val constructorMap = hashMapOf>() + + constructors.useLines { lines -> + lines.forEach { line -> + val parts = line.split(' ') + constructorMap.compute(parts[1]) { _, v -> + val node = ConstructorNode(parts[0].toInt(), parts[2]) + if (v == null) { + return@compute mutableListOf(node) + } else { + v += node + return@compute v + } + } + } + } + + for (list in constructorMap.values) { + // Put bigger numbers first + // Old constructor entries are still present, just with smaller numbers. So we don't want to grab an older + // entry + list.reverse() + } + + return ConstructorsData(constructorMap) +} + +typealias ParamNames = MutableMap> +fun newParamNames(): ParamNames = mutableMapOf() + +fun writeParamNames(names: ParamNames, file: File) { + file.bufferedWriter().use { writer -> + for ((desc, params) in names.entries) { + writer.append(desc).append(' ') + for (i in params.indices) { + writer.append(i.toString()).append(' ').append(params[i]) + if (i != params.lastIndex) { + writer.append(' ') + } + } + writer.newLine() + } + } +} + +fun parseParamNames(file: File): ParamNames { + val paramNames: MutableMap> = mutableMapOf() + file.useLines { lines -> + for (line in lines) { + val parts = line.split(' ') + val params = parts.asSequence().drop(1).chunked(2).associate { it[0].toInt() to it[1] } + paramNames[parts.first()] = Array(params.size) { params.getValue(it) } + } + } + return paramNames +} diff --git a/src/main/kotlin/util/BuildDataInfo.kt b/src/main/kotlin/util/BuildDataInfo.kt index 6a21f44..9d4756c 100644 --- a/src/main/kotlin/util/BuildDataInfo.kt +++ b/src/main/kotlin/util/BuildDataInfo.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,7 +20,7 @@ * USA */ -package util +package io.papermc.paperweight.util data class BuildDataInfo( val minecraftVersion: String, diff --git a/src/main/kotlin/util/Constants.kt b/src/main/kotlin/util/Constants.kt index be7e32e..12dcdd0 100644 --- a/src/main/kotlin/util/Constants.kt +++ b/src/main/kotlin/util/Constants.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,14 +27,6 @@ import org.gradle.api.Task object Constants { const val EXTENSION = "paperweight" - const val MCP_MAPPINGS_CONFIG = "mcpConfig" - - const val MCP_DATA_CONFIG = "mcpData" - const val SPIGOT_DEP_CONFIG = "spigotDeps" - const val MINECRAFT_DEP_CONFIG = "minecraft" - const val FORGE_FLOWER_CONFIG = "forgeFlower" - const val MCINJECT_CONFIG = "mcinject" - const val FORGE_MAVEN_URL = "https://files.minecraftforge.net/maven" const val MC_LIBRARY_URL = "https://libraries.minecraft.net/" const val MC_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json" @@ -43,9 +34,19 @@ object Constants { const val CACHE_PATH = "caches" private const val PAPER_PATH = "paperweight" + private const val JARS_PATH = "$PAPER_PATH/jars" + const val MINECRAFT_JARS_PATH = "$JARS_PATH/minecraft" + const val MCP_TOOLS_PATH = "$JARS_PATH/tools" + const val MCP_ZIPS_PATH = "$JARS_PATH/mcp" + private const val SPIGOT_JARS_PATH = "$JARS_PATH/spigot" + const val SPIGOT_API_JARS_PATH = "$SPIGOT_JARS_PATH/api" + const val SPIGOT_SERVER_JARS_PATH = "$SPIGOT_JARS_PATH/server" + const val MCP_DATA_DIR = "mcp/data" const val MCP_MAPPINGS_DIR = "mcp/mappings" - const val SRG_DIR = "$MCP_MAPPINGS_DIR/srgs" + private const val SRG_DIR = "$MCP_MAPPINGS_DIR/srgs" + + const val MCP_CONFIG_JSON = "$MCP_DATA_DIR/config.json" const val PAPER_FIELDS_CSV = "$MCP_MAPPINGS_DIR/paper_fields.csv" const val PAPER_METHODS_CSV = "$MCP_MAPPINGS_DIR/paper_methods.csv" @@ -69,10 +70,10 @@ object Constants { const val MC_MANIFEST = "jsons/McManifest.json" const val VERSION_JSON = "jsons/McVersion.json" + const val MC_LIBRARIES = "jsons/McLibraries.txt" - const val TASK_CACHE = "$PAPER_PATH/taskCache" + private const val TASK_CACHE = "$PAPER_PATH/taskCache" - fun Task.paperTaskOutput() = paperTaskOutput("jar") fun Task.paperTaskOutput(ext: String) = paperTaskOutput(name, ext) - fun Task.paperTaskOutput(name: String, ext: String) = "$TASK_CACHE/$name.$ext" + fun paperTaskOutput(name: String, ext: String) = "$TASK_CACHE/$name.$ext" } diff --git a/src/main/kotlin/util/ArtifactDescriptor.kt b/src/main/kotlin/util/MavenArtifact.kt similarity index 58% rename from src/main/kotlin/util/ArtifactDescriptor.kt rename to src/main/kotlin/util/MavenArtifact.kt index cf5777a..3be8353 100644 --- a/src/main/kotlin/util/ArtifactDescriptor.kt +++ b/src/main/kotlin/util/MavenArtifact.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,23 +23,66 @@ package io.papermc.paperweight.util import io.papermc.paperweight.PaperweightException +import java.io.File -data class ArtifactDescriptor( - val group: String, - val artifact: String, - val version: String, - val classifier: String?, - val extension: String +class MavenArtifact( + private val group: String, + private val artifact: String, + private val version: String, + private val classifier: String? = null, + private val extension: String? = null ) { + + private val classifierText: String + get() = if (classifier != null) "-$classifier" else "" + + private val ext: String + get() = extension ?: "jar" + + private val path: String + get() = "${group.replace('.', '/')}/$artifact/$version/$file" + val file: String + get() = "$artifact-$version$classifierText.$ext" + + fun downloadToFile(targetFile: File, repos: List) { + targetFile.parentFile.mkdirs() + + var thrown: Exception? = null + for (repo in repos) { + try { + download(addSlash(repo) + path, targetFile) + return + } catch (e: Exception) { + if (thrown != null) { + thrown.addSuppressed(e) + } else { + thrown = e + } + } + } + thrown?.let { throw PaperweightException("Failed to download artifact: $this. Checked repos: $repos", it) } + } + + fun downloadToDir(targetDir: File, repos: List): File { + val out = targetDir.resolve(file) + downloadToFile(targetDir.resolve(file), repos) + return out + } + override fun toString(): String { - val path = group.replace('.', '/') - val classifierText = classifier?.let { "-$it" } ?: "" - val file = "$artifact-$version$classifierText.$extension" - return "$path/$artifact/$version/$file" + return if (classifier == null) { + "$group:$artifact:$version" + } else { + "$group:$artifact:$version:$classifier" + } + } + + private fun addSlash(url: String): String { + return if (url.endsWith('/')) url else "$url/" } companion object { - fun parse(text: String): ArtifactDescriptor { + fun parse(text: String): MavenArtifact { val (group, groupIndex) = text.nextSubstring(0, charArrayOf(':')) val (artifact, artifactIndex) = text.nextSubstring(groupIndex, charArrayOf(':')) val (version, versionIndex) = text.nextSubstring(artifactIndex, charArrayOf(':', '@'), goToEnd = true) @@ -51,7 +93,7 @@ data class ArtifactDescriptor( artifact ?: throw PaperweightException("Invalid Maven artifact descriptor (no artifactId found): $text") version ?: throw PaperweightException("Invalid Maven artifact descriptor (no version found): $text") - return ArtifactDescriptor(group, artifact, version, classifier, extension ?: "jar") + return MavenArtifact(group, artifact, version, classifier, extension) } private fun String.nextSubstring(startIndex: Int, stops: CharArray, goToEnd: Boolean = false): Pair { diff --git a/src/main/kotlin/util/McpConfig.kt b/src/main/kotlin/util/McpConfig.kt index 52e4914..2a99b58 100644 --- a/src/main/kotlin/util/McpConfig.kt +++ b/src/main/kotlin/util/McpConfig.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,6 +22,14 @@ package io.papermc.paperweight.util +typealias FunctionMap = Map +val FunctionMap.decompile: McpJvmCommand + get() = getValue("decompile") +val FunctionMap.mcinject: McpJvmCommand + get() = getValue("mcinject") +val FunctionMap.rename: McpJvmCommand + get() = getValue("rename") + data class McpConfig( val spec: Int, val version: String, diff --git a/src/main/kotlin/util/MinecraftManifest.kt b/src/main/kotlin/util/MinecraftManifest.kt index e573689..33ab579 100644 --- a/src/main/kotlin/util/MinecraftManifest.kt +++ b/src/main/kotlin/util/MinecraftManifest.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,7 +20,7 @@ * USA */ -package util +package io.papermc.paperweight.util data class MinecraftManifest( internal val latest: Map, diff --git a/src/main/kotlin/util/download.kt b/src/main/kotlin/util/download.kt new file mode 100644 index 0000000..9b355ca --- /dev/null +++ b/src/main/kotlin/util/download.kt @@ -0,0 +1,140 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. It uses + * some code and systems originally from ForgeGradle. + * + * Copyright (C) 2020 Kyle Wood + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.util + +import io.papermc.paperweight.PaperweightException +import org.apache.http.HttpHost +import org.apache.http.HttpStatus +import org.apache.http.client.config.CookieSpecs +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.utils.DateUtils +import org.apache.http.impl.client.HttpClientBuilder +import org.gradle.api.provider.Provider +import java.io.File +import java.net.URI +import java.net.URL +import java.util.Date +import java.util.concurrent.TimeUnit + +fun download(source: Any, target: Any) { + val url = source.convertToUrl() + val file = target.convertToFile() + download(url, file) +} + +private fun download(source: URL, target: File) { + target.parentFile.mkdirs() + + val etagFile = target.resolveSibling(target.name + ".etag") + val etag = if (etagFile.exists()) etagFile.readText() else null + + val host = HttpHost(source.host, source.port, source.protocol) + val httpClient = HttpClientBuilder.create().run { + setRetryHandler { _, count, _ -> count < 3 } + useSystemProperties() + build() + } + + val time = if (target.exists()) target.lastModified() else 0 + + httpClient.use { client -> + val httpGet = HttpGet(source.file) + // Super high timeout, reduce chances of weird things going wrong + val timeouts = TimeUnit.MINUTES.toMillis(5).toInt() + + httpGet.config = RequestConfig.custom() + .setConnectTimeout(timeouts) + .setConnectionRequestTimeout(timeouts) + .setSocketTimeout(timeouts) + .setCookieSpec(CookieSpecs.STANDARD) + .build() + + if (time > 0) { + httpGet.setHeader("If-Modified-Since", DateUtils.formatDate(Date(time))) + } + if (etag != null) { + httpGet.setHeader("If-None-Match", etag) + } + + client.execute(host, httpGet).use { response -> + val code = response.statusLine.statusCode + if ((code < 200 || code > 299) && code != HttpStatus.SC_NOT_MODIFIED) { + val reason = response.statusLine.reasonPhrase + throw PaperweightException("Download failed, HTTP code: $code; URL: $source; Reason: $reason") + } + + val lastModified = handleResponse(response, time, target) + saveEtag(response, lastModified, target, etagFile) + } + } +} + +private fun handleResponse(response: CloseableHttpResponse, time: Long, target: File): Long { + val lastModified = with(response.getLastHeader("Last-Modified")) { + if (this == null) { + return@with 0 + } + if (value.isNullOrBlank()) { + return@with 0 + } + val date = DateUtils.parseDate(value) ?: return@with 0 + return@with date.time + } + if (response.statusLine.statusCode == HttpStatus.SC_NOT_MODIFIED) { + if (lastModified != 0L && time >= lastModified) { + return lastModified + } + } + + val entity = response.entity ?: return lastModified + entity.content.use { input -> + target.outputStream().buffered().use { output -> + input.copyTo(output) + } + } + + return lastModified +} + +private fun saveEtag(response: CloseableHttpResponse, lastModified: Long, target: File, etagFile: File) { + if (lastModified > 0) { + target.setLastModified(lastModified) + } + + val header = response.getFirstHeader("ETag") ?: return + val etag = header.value + + etagFile.writeText(etag) +} + +private fun Any.convertToUrl(): URL { + return when (this) { + is URL -> this + is URI -> this.toURL() + is String -> URI.create(this).toURL() + is Provider<*> -> this.get().convertToUrl() + else -> throw PaperweightException("Unknown URL type: ${this.javaClass.name}") + } +} diff --git a/src/main/kotlin/util/etag-util.kt b/src/main/kotlin/util/etag-util.kt deleted file mode 100644 index d5af5e6..0000000 --- a/src/main/kotlin/util/etag-util.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * paperweight is a Gradle plugin for the PaperMC project. It uses - * some code and systems originally from ForgeGradle. - * - * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - */ - -package io.papermc.paperweight.util - -import io.papermc.paperweight.PaperweightException -import java.io.File -import java.net.HttpURLConnection -import java.net.URL -import java.util.concurrent.TimeUnit - -private const val USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11" - -fun getWithEtag(urlText: String, cache: File, etagFile: File) { - if (cache.exists() && cache.lastModified() + TimeUnit.MINUTES.toMillis(1) >= System.currentTimeMillis()) { - return - } - - val etag = if (etagFile.exists()) { - etagFile.readText() - } else { - etagFile.parentFile.mkdirs() - "" - } - - var thrown: Throwable? = null - - try { - val url = URL(urlText) - - val con = url.openConnection() as HttpURLConnection - con.instanceFollowRedirects = true - con.setRequestProperty("User-Agent", USER_AGENT) - con.ifModifiedSince = cache.lastModified() - - if (etag.isNotEmpty()) { - con.setRequestProperty("If-None-Match", etag) - } - - try { - con.connect() - - when (con.responseCode) { - 304 -> { - cache.setLastModified(System.currentTimeMillis()) - return - } - 200 -> { - val data = con.inputStream.use { stream -> - stream.readBytes() - } - - cache.writeBytes(data) - - val newEtag = con.getHeaderField("ETag") - if (newEtag.isNullOrEmpty()) { - if (!etagFile.createNewFile()) { - etagFile.setLastModified(System.currentTimeMillis()) - } - } else { - etagFile.writeText(newEtag) - } - - return - } - else -> throw RuntimeException("Etag download for $urlText failed with code ${con.responseCode}") - } - } finally { - con.disconnect() - } - } catch (e: Exception) { - if (thrown == null) { - thrown = e - } else { - thrown.addSuppressed(e) - } - } - - val errorString = "Unable to download from $urlText with etag" - val ex = if (thrown != null) { - PaperweightException(errorString, thrown) - } else { - PaperweightException(errorString) - } - throw ex -} diff --git a/src/main/kotlin/util/git.kt b/src/main/kotlin/util/git.kt index 3bd03a9..8387b3f 100644 --- a/src/main/kotlin/util/git.kt +++ b/src/main/kotlin/util/git.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -37,12 +36,6 @@ class Git(private var repo: File) { } } - val status - get() = this("status", "-z").getText() - - val ref - get() = this("rev-parse", "HEAD").getText().replace('\n', ' ').replace(Regex("\\s+"), "") - operator fun invoke(vararg args: String, disableGpg: Boolean = true): Command { val cmd = if (disableGpg) { arrayOf("git", "-c", "commit.gpgsign=false", *args) @@ -57,17 +50,37 @@ class Git(private var repo: File) { } } -class Command(internal val process: Process, private val command: String) { +class Command(private val process: Process, private val command: String) { - var outStream: OutputStream? = null + private var outStream: OutputStream = UselessOutputStream + private var errStream: OutputStream = UselessOutputStream - fun run(): Int = try { - outStream?.let { out -> - process.inputStream.copyTo(out) + fun run(): Int { + try { + val input = process.inputStream + val error = process.errorStream + val buffer = ByteArray(1000) + + while (process.isAlive) { + // Read both stdout and stderr on the same thread + // This is important for how Gradle outputs the logs + if (input.available() > 0) { + val count = input.read(buffer) + outStream.write(buffer, 0, count) + } + if (error.available() > 0) { + val count = error.read(buffer) + errStream.write(buffer, 0, count) + } + Thread.sleep(1) + } + // Catch any other output we may have missed + outStream.write(input.readBytes()) + errStream.write(error.readBytes()) + return process.waitFor() + } catch (e: Exception) { + throw PaperweightException("Failed to call git command: $command", e) } - process.waitFor() - } catch (e: Exception) { - throw PaperweightException("Failed to call git command: $command", e) } fun runSilently(silenceOut: Boolean = true, silenceErr: Boolean = false): Int { @@ -76,7 +89,7 @@ class Command(internal val process: Process, private val command: String) { } fun runOut(): Int { - setup(System.out, System.out) + setup(System.out, System.err) return run() } @@ -93,8 +106,8 @@ class Command(internal val process: Process, private val command: String) { } private fun silence(silenceOut: Boolean, silenceErr: Boolean) { - val out = if (silenceOut) UselessOutputStream else System.out - val err = if (silenceErr) UselessOutputStream else System.err + val out = if (silenceOut) null else System.out + val err = if (silenceErr) null else System.err setup(out, err) } @@ -104,16 +117,15 @@ class Command(internal val process: Process, private val command: String) { } fun setup(out: OutputStream? = null, err: OutputStream? = null): Command { - outStream = out - if (err != null) { - redirect(process.errorStream, err) - } + outStream = out ?: UselessOutputStream + errStream = err ?: UselessOutputStream return this } fun getText(): String { val out = ByteArrayOutputStream() setup(out, System.err) + execute() return String(out.toByteArray(), Charsets.UTF_8) } diff --git a/src/main/kotlin/util/jvm.kt b/src/main/kotlin/util/jvm.kt index b529b49..d1a1848 100644 --- a/src/main/kotlin/util/jvm.kt +++ b/src/main/kotlin/util/jvm.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,19 +23,12 @@ package io.papermc.paperweight.util import io.papermc.paperweight.PaperweightException -import org.gradle.api.Task import org.gradle.internal.jvm.Jvm import java.io.OutputStream -fun Task.runJar( - jar: Any, - workingDir: Any, - logFile: Any?, - jvmArgs: List = listOf(), - vararg args: String -) { - val jarFile = project.file(jar) - val dir = project.file(workingDir) +fun runJar(jar: Any, workingDir: Any, logFile: Any?, jvmArgs: List = listOf(), vararg args: String) { + val jarFile = jar.convertToFile() + val dir = workingDir.convertToFile() val process = ProcessBuilder( Jvm.current().javaExecutable.canonicalPath, *jvmArgs.toTypedArray(), @@ -47,20 +39,15 @@ fun Task.runJar( val output = when { logFile is OutputStream -> logFile logFile != null -> { - val log = project.file(logFile) + val log = logFile.convertToFile() log.outputStream().buffered() } - else -> null + else -> UselessOutputStream } output.use { - output?.let { - redirect(process.inputStream, it) - redirect(process.errorStream, it) - } ?: run { - redirect(process.inputStream, UselessOutputStream) - redirect(process.errorStream, UselessOutputStream) - } + redirect(process.inputStream, it) + redirect(process.errorStream, it) val e = process.waitFor() if (e != 0) { diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 69fceb9..44933dd 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,28 +28,36 @@ import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson import io.papermc.paperweight.PaperweightException import io.papermc.paperweight.ext.PaperweightExtension +import io.papermc.paperweight.tasks.BaseTask import io.papermc.paperweight.util.Constants.paperTaskOutput import org.cadixdev.lorenz.MappingSet import org.cadixdev.lorenz.io.TextMappingFormat -import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.api.artifacts.Configuration import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider import java.io.File import java.io.InputStream import java.io.OutputStream +import java.nio.file.Path import java.util.Optional +import kotlin.reflect.KClass +import kotlin.reflect.KProperty val gson: Gson = Gson() -inline val Project.ext: PaperweightExtension +inline fun Gson.fromJson(file: Any): T = + file.convertToFile().bufferedReader().use { fromJson(it) } + +val Project.ext: PaperweightExtension get() = extensions.getByName(Constants.EXTENSION) as PaperweightExtension -inline val Project.cache: File - get() = file(".gradle").resolve(Constants.CACHE_PATH) +val ProjectLayout.cache: File + get() = projectDirectory.file(".gradle/${Constants.CACHE_PATH}").asFile fun writeMappings(format: TextMappingFormat, vararg mappings: Pair) { for ((set, file) in mappings) { @@ -58,8 +65,8 @@ fun writeMappings(format: TextMappingFormat, vararg mappings: Pair this + is Path -> this.toFile() + is RegularFile -> this.asFile + is Provider<*> -> this.get().convertToFile() + else -> throw PaperweightException("Unknown type representing a file: ${this.javaClass.name}") + } +} + +fun ensureParentExists(vararg files: Any) { for (file in files) { - val parent = project.file(file).parentFile + val parent = file.convertToFile().parentFile if (!parent.exists() && !parent.mkdirs()) { throw PaperweightException("Failed to create directory $parent") } } } -fun Task.ensureDeleted(vararg files: Any) { +fun ensureDeleted(vararg files: Any) { for (file in files) { - val f = project.file(file) + val f = file.convertToFile() if (f.exists() && !f.deleteRecursively()) { throw PaperweightException("Failed to delete file $f") } } } -inline fun Project.toProvider(crossinline func: () -> File): Provider { - return layout.file(provider { func() }) +fun BaseTask.defaultOutput(name: String, ext: String): RegularFileProperty { + return objects.fileProperty().convention { + layout.cache.resolve(paperTaskOutput(name, ext)) + } } - -fun Task.defaultOutput(name: String, ext: String): RegularFileProperty { - return project.objects.fileProperty().convention(project.toProvider { - project.cache.resolve(paperTaskOutput(name, ext)) - }) -} -fun Task.defaultOutput(ext: String): RegularFileProperty { +fun BaseTask.defaultOutput(ext: String): RegularFileProperty { return defaultOutput(name, ext) } -fun Task.defaultOutput(): RegularFileProperty { +fun BaseTask.defaultOutput(): RegularFileProperty { return defaultOutput("jar") } val Optional.orNull: T? get() = orElse(null) -val RegularFileProperty.file +val RegularFileProperty.file: File get() = get().asFile -val RegularFileProperty.fileOrNull +val RegularFileProperty.fileOrNull: File? get() = orNull?.asFile -val DirectoryProperty.file +val RegularFileProperty.path: Path + get() = file.toPath() +val DirectoryProperty.file: File get() = get().asFile -val DirectoryProperty.fileOrNull - get() = orNull?.asFile +val DirectoryProperty.path: Path + get() = file.toPath() +inline fun Project.contents(contentFile: Any, crossinline convert: (String) -> T): Provider { + return providers.fileContents(layout.projectDirectory.file(contentFile.convertToFile().absolutePath)) + .asText + .forUseAtConfigurationTime() + .map { convert(it) } +} -private var parsedConfig: McpConfig? = null -fun mcpConfig(file: Provider): McpConfig { - if (parsedConfig != null) { - return parsedConfig as McpConfig - } - parsedConfig = file.get().asFile.bufferedReader().use { reader -> - gson.fromJson(reader) - } - return parsedConfig as McpConfig +// We have to create our own delegate because the ones Gradle provides don't work in plugin dev environments +inline fun TaskContainer.registering(noinline configure: T.() -> Unit): TaskDelegateProvider { + return TaskDelegateProvider(this, T::class, configure) } -fun mcpFile(configFile: RegularFileProperty, path: String): File { - return configFile.file.resolveSibling(path) +class TaskDelegateProvider( + private val container: TaskContainer, + private val type: KClass, + private val configure: T.() -> Unit +) { + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): TaskDelegate { + val provider = container.register(property.name, type.java, configure) + return TaskDelegate(provider) + } +} +class TaskDelegate(private val provider: TaskProvider) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): TaskProvider = provider } diff --git a/src/main/kotlin/util/zip.kt b/src/main/kotlin/util/zip.kt index 3709b64..deab8a5 100644 --- a/src/main/kotlin/util/zip.kt +++ b/src/main/kotlin/util/zip.kt @@ -3,7 +3,6 @@ * some code and systems originally from ForgeGradle. * * Copyright (C) 2020 Kyle Wood - * Copyright (C) 2018 Forge Development LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,7 +23,7 @@ package io.papermc.paperweight.util import io.papermc.paperweight.PaperweightException -import org.gradle.api.Task +import io.papermc.paperweight.tasks.BaseTask import java.io.File import java.net.URI import java.nio.file.FileSystems @@ -35,26 +34,26 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.ThreadLocalRandom -fun Task.unzip(zip: Any, target: Any? = null): File { - val input = project.file(zip) - val outputDir = target?.let { project.file(it) } +fun BaseTask.unzip(zip: Any, target: Any? = null): File { + val input = zip.convertToFile() + val outputDir = target?.convertToFile() ?: input.resolveSibling("${input.name}-" + ThreadLocalRandom.current().nextInt()) - project.copy { - from(project.zipTree(zip)) + fs.copy { + from(archives.zipTree(zip)) into(outputDir) } return outputDir } -fun Task.zip(inputDir: Any, zip: Any) { - val outputZipFile = project.file(zip) +fun zip(inputDir: Any, zip: Any) { + val outputZipFile = zip.convertToFile() if (outputZipFile.exists() && !outputZipFile.delete()) { throw PaperweightException("Could not delete $outputZipFile") } - val dirPath = project.file(inputDir).toPath() + val dirPath = inputDir.convertToFile().toPath() val outUri = URI.create("jar:${outputZipFile.toURI()}") FileSystems.newFileSystem(outUri, mapOf("create" to "true")).use { fs ->