diff --git a/build.gradle b/build.gradle index a93dafb4..f713f184 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,6 @@ buildscript { } dependencies { classpath 'com.anatawa12.forge:ForgeGradle:1.2-1.1.0' - classpath 'io.github.cruciblemc:CrucibleGradle:1.2-SNAPSHOT' } } diff --git a/buildSrc/LICENSE b/buildSrc/LICENSE new file mode 100644 index 00000000..8000a6fa --- /dev/null +++ b/buildSrc/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/buildSrc/README.md b/buildSrc/README.md new file mode 100644 index 00000000..561ad304 --- /dev/null +++ b/buildSrc/README.md @@ -0,0 +1,18 @@ +# CrucibleGradle +Make ForgeGradle do more + +## About +This is Crucible's extension over anatawa12's [ForgeGradle-1.2](https://github.com/anatawa12/ForgeGradle-1.2/). +Here we keep all necessary machinery for building Crucible. + +This project still needs a lot of proper cleanup since it was more or less a copy/paste of old Cauldron dev plugin. +A lot of tasks still does not cache properly, and we don't have much of an idea of what is going on certain parts of the +FG workflow. + +This is basically the bare minimum to get Crucible to build on modern Gradle. + +## TODO +* [ ] Document all tasks/add descriptions +* [ ] Fix caching issues +* [ ] Trim down unneeded tasks +* [ ] Clean up old code and deprecated usages \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..25df39ea --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,120 @@ +plugins { + java + `java-gradle-plugin` + `maven-publish` +} + +group = "io.github.cruciblemc" +version = "1.2-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.minecraftforge.net/") { + name = "forge" + } +} + +dependencies { + // TODO? figure a way to use the runtime dependencies of FG? + implementation("org.ow2.asm:asm:9.4") + implementation("org.ow2.asm:asm-tree:9.4") + implementation("com.google.guava:guava:31.1-jre") + implementation("com.opencsv:opencsv:5.7.0") + implementation("com.cloudbees:diff4j:1.3") + implementation("com.github.abrarsyed.jastyle:jAstyle:1.2") + implementation("net.sf.trove4j:trove4j:2.1.0") + implementation("com.github.jponge:lzma-java:1.3") + implementation("com.nothome:javaxdelta:2.0.1") + implementation("com.google.code.gson:gson:2.10.1") + implementation("com.anatawa12.forge:SpecialSource:1.11.1") + implementation("org.apache.httpcomponents:httpclient:4.5.14") + implementation("org.apache.httpcomponents:httpmime:4.5.14") + implementation("de.oceanlabs.mcp:RetroGuard:3.6.6") + implementation("de.oceanlabs.mcp:mcinjector:3.2-SNAPSHOT") + implementation("net.minecraftforge:Srg2Source:4.2.7") + + api("com.anatawa12.forge:ForgeGradle:1.2-1.1.0") + testImplementation(platform("org.junit:junit-bom:5.9.1")) + testImplementation("org.junit.jupiter:junit-jupiter") + + implementation("com.github.tony19:named-regexp:0.2.3") + + // Java 9+ syntax + annotationProcessor("com.github.bsideup.jabel:jabel-javac-plugin:0.4.2") + compileOnly("com.github.bsideup.jabel:jabel-javac-plugin:0.4.2") + + // Lombok + compileOnly("org.projectlombok:lombok:1.18.26") + annotationProcessor("org.projectlombok:lombok:1.18.26") +} + +tasks.test { + useJUnitPlatform() +} + +tasks.named("compileJava").configure { + this as JavaCompile + sourceCompatibility = "17" // for the IDE support + options.release.set(8) + + javaCompiler.set( + javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(17)) + } + ) +} + +gradlePlugin { + plugins { + create("crucible") { + id = "crucible" + implementationClass = "io.github.cruciblemc.forgegradle.CrucibleDevPlugin" + } + } +} + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + artifactId = base.archivesName.get() + + pom { + name.set(project.base.archivesName.get()) + description.set("Gradle plugin for Crucible") + url.set("https://github.com/CrucibleMC/CrucibleGradle") + + scm { + url.set("https://github.com/CrucibleMC/CrucibleGradle") + connection.set("scm:git:git://github.com/CrucibleMC/CrucibleGradle.git") + developerConnection.set("scm:git:git@github.com:CrucibleMC/CrucibleGradle.git") + } + + issueManagement { + system.set("github") + url.set("https://github.com/CrucibleMC/CrucibleGradle/issues") + } + + licenses { + license { + name.set("Lesser GNU Public License, Version 2.1") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + distribution.set("repo") + } + } + + developers { + developer { + id.set("juanmuscaria") + name.set("juanmuscaria") + } + } + } + } + repositories { + maven(buildDir.absolutePath + "/repo") { + name = "filesystem" + } + } + } +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..8a47b84c --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "CrucibleGradle" + diff --git a/buildSrc/src/main/java/com/juanmuscaria/uncode/ASMCodeRemover.java b/buildSrc/src/main/java/com/juanmuscaria/uncode/ASMCodeRemover.java new file mode 100644 index 00000000..85fb5ab5 --- /dev/null +++ b/buildSrc/src/main/java/com/juanmuscaria/uncode/ASMCodeRemover.java @@ -0,0 +1,88 @@ +package com.juanmuscaria.uncode; + +import com.google.common.io.ByteStreams; +import com.juanmuscaria.uncode.cleaners.ClassCleaner; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class ASMCodeRemover { + + private static final Logger logger + = LoggerFactory.getLogger(ASMCodeRemover.class); + + /** + * Removes all the code, assets, and private elements from given jar, + * keeping only public classes, methods and fields with no code body. + * + * @param jarFile the jar file to process + * @param outputFile the output file + * @param overwrite if the output file should be overwritten if it already exists + * @return a map with jarEntry-reason for all entries from the input jar that where removed (resources and class files) + * @throws IOException if an I/O error occurs + */ + public static Map removeContent(Path jarFile, Path outputFile, boolean overwrite) throws IOException { + if (!Files.exists(jarFile)) { + throw new IllegalArgumentException("Input file does not exist"); + } else if (!Files.isReadable(jarFile)) { + throw new IllegalArgumentException("Input file is not readable"); + } else if (Files.exists(outputFile) && !overwrite) { + throw new IllegalArgumentException("Output file already exists"); + } + + var failedEntries = new LinkedHashMap(); + try (var out = new ZipOutputStream(Files.newOutputStream(outputFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) { + try (var zip = new ZipFile(jarFile.toFile())) { + Collections.list(zip.entries()).iterator().forEachRemaining(entry -> { + if (entry.getName().endsWith(".class")) { + try { + var classBytes = processClass(ByteStreams.toByteArray(zip.getInputStream(entry))); + var newEntry = new ZipEntry(entry.getName()); + out.putNextEntry(newEntry); + out.write(classBytes); + out.closeEntry(); + } catch (Exception e) { + failedEntries.put(entry.getName(), e.getMessage()); + logger.debug("Failed to process class: {}", entry.getName()); + logger.debug("Exception:", e); + } + } else if (!entry.getName().endsWith("/")) { + failedEntries.put(entry.getName(), "Not a class file"); + } + }); + } catch (ZipException e) { + throw new IllegalArgumentException("Input file is corrupted or not a valid jar file: " + e.getMessage(), e); + } + } + + return failedEntries; + } + + /** + * Removes all the code from a class, keeping only its public members without any code body. + * + * @param classBytes input class bytes + * @return the processed class bytes + * @throws IllegalArgumentException if the class is not readable by the current ASM version, + * if the class is synthetic or if the class is not public + */ + public static byte[] processClass(byte[] classBytes) throws IllegalArgumentException { + var classReader = new ClassReader(classBytes); + var classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); + classReader.accept(new ClassCleaner(classWriter), 0); + return classWriter.toByteArray(); + } +} diff --git a/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/ClassCleaner.java b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/ClassCleaner.java new file mode 100644 index 00000000..d24a2a90 --- /dev/null +++ b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/ClassCleaner.java @@ -0,0 +1,117 @@ +package com.juanmuscaria.uncode.cleaners; + +import org.objectweb.asm.*; + +/** + * Remove most attributes from a class, leaving only public methods without a body, public fields and inner classes. + * All synthetic members are removed, classes produced by this is not intended to by loaded by the jvm. + */ +public class ClassCleaner extends ClassVisitor { + + public ClassCleaner(ClassVisitor cv) { + super(Opcodes.ASM9, cv); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + // We only want public classes + if ((access & Opcodes.ACC_PUBLIC) == 0) { + throw new IllegalArgumentException("Class is not public, skipping"); + } else if ((access & Opcodes.ACC_SYNTHETIC) != 0) { + throw new IllegalArgumentException("Class is synthetic (compiler generated), skipping"); + } + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitSource(String source, String debug) { + // Warns this class was touched by the code killer + super.visitSource("uncoded", null); + } + + @Override + public ModuleVisitor visitModule(String name, int access, String version) { + // NO-OP - Remove element + return null; + } + + @Override + public void visitNestHost(String nestHost) { + // NO-OP - Remove element + } + + @Override + public void visitOuterClass(String owner, String name, String descriptor) { + super.visitOuterClass(owner, name, descriptor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return super.visitAnnotation(descriptor, visible); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + + @Override + public void visitAttribute(Attribute attribute) { + // NO-OP - Remove element + } + + @Override + public void visitNestMember(String nestMember) { + // NO-OP - Remove element + } + + @Override + public void visitPermittedSubclass(String permittedSubclass) { + // NO-OP - Remove element + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // We only want public classes + if ((access & Opcodes.ACC_PUBLIC) == 0) { + return; + } else if ((access & Opcodes.ACC_SYNTHETIC) != 0) { + return; + } + super.visitInnerClass(name, outerName, innerName, access); + } + + @Override + public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) { + // NO-OP - Remove element + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + // We only want public fields + if ((access & Opcodes.ACC_PUBLIC) == 0) { + return null; + } else if ((access & Opcodes.ACC_SYNTHETIC) != 0) { + return null; + } + // Keep the initial value if it's a constant + return new FieldCleaner(super.visitField(access, name, descriptor, signature, (access & Opcodes.ACC_STATIC) != 0 ? value : null)); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + // We only want public methods + if ((access & Opcodes.ACC_PUBLIC) == 0) { + return null; + } else if ((access & Opcodes.ACC_SYNTHETIC) != 0) { + return null; + } + return new MethodCleaner(super.visitMethod(access, name, descriptor, signature, exceptions), name, descriptor); + } + + @Override + public void visitEnd() { + super.visitEnd(); + } +} diff --git a/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/FieldCleaner.java b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/FieldCleaner.java new file mode 100644 index 00000000..4e7c4276 --- /dev/null +++ b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/FieldCleaner.java @@ -0,0 +1,33 @@ +package com.juanmuscaria.uncode.cleaners; + +import org.objectweb.asm.*; + +/** + * Cleans annotation and attributes from a field. + */ +public class FieldCleaner extends FieldVisitor { + + public FieldCleaner(FieldVisitor fieldVisitor) { + super(Opcodes.ASM9, fieldVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return super.visitAnnotation(descriptor, visible); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + + @Override + public void visitAttribute(Attribute attribute) { + // NO-OP + } + + @Override + public void visitEnd() { + super.visitEnd(); + } +} diff --git a/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/MethodCleaner.java b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/MethodCleaner.java new file mode 100644 index 00000000..a56a65bd --- /dev/null +++ b/buildSrc/src/main/java/com/juanmuscaria/uncode/cleaners/MethodCleaner.java @@ -0,0 +1,201 @@ +package com.juanmuscaria.uncode.cleaners; + +import org.objectweb.asm.*; + +import static org.objectweb.asm.Opcodes.*; + +/** + * Cleans a method from all its code, annotations and attributes. + */ +public class MethodCleaner extends MethodVisitor { + + private final String name; + private final String descriptor; + + public MethodCleaner(MethodVisitor methodVisitor, final String name, + final String descriptor) { + super(Opcodes.ASM9, methodVisitor); + this.name = name; + this.descriptor = descriptor; + } + + @Override + public void visitParameter(String name, int access) { + super.visitParameter(name, access); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + // NO-OP - Remove element + return super.visitAnnotationDefault(); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return super.visitAnnotation(descriptor, visible); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + + @Override + public void visitAnnotableParameterCount(int parameterCount, boolean visible) { + super.visitAnnotableParameterCount(parameterCount, visible); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { + return super.visitParameterAnnotation(parameter, descriptor, visible); + } + + @Override + public void visitAttribute(Attribute attribute) { + // NO-OP - Remove element + } + + @Override + public void visitCode() { + var type = Type.getType(descriptor); + super.visitCode(); + + if ("".equals(name)) { // It's a constructor, call super + super.visitLabel(new Label()); + // Load "this" into the operand stack + super.visitVarInsn(ALOAD, 0); + // Calls the super constructor + super.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + // Return + super.visitInsn(RETURN); + } else if (type.getReturnType().equals(Type.VOID_TYPE)) { // Void return type + // Return + super.visitInsn(RETURN); + } else { + // Load null into the operand stack + super.visitInsn(ACONST_NULL); + // Return + super.visitInsn(ARETURN); + } + + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + // NO-OP - Remove element + } + + @Override + public void visitInsn(int opcode) { + // NO-OP - Remove element + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // NO-OP - Remove element + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + // NO-OP - Remove element + } + + @Override + public void visitTypeInsn(int opcode, String type) { + // NO-OP - Remove element + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // NO-OP - Remove element + } + + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + // NO-OP - Remove element + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + // NO-OP - Remove element + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // NO-OP - Remove element + } + + @Override + public void visitLabel(Label label) { + // NO-OP - Remove element + } + + @Override + public void visitLdcInsn(Object value) { + // NO-OP - Remove element + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + // NO-OP - Remove element + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + // NO-OP - Remove element + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // NO-OP - Remove element + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + // NO-OP - Remove element + } + + @Override + public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + // NO-OP - Remove element + return null; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // NO-OP - Remove element + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + // NO-OP - Remove element + return null; + } + + @Override + public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { + // NO-OP - Remove element + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String descriptor, boolean visible) { + // NO-OP - Remove element + return null; + } + + @Override + public void visitLineNumber(int line, Label start) { + // NO-OP - Remove element + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + super.visitMaxs(1, 1); + } + + @Override + public void visitEnd() { + super.visitEnd(); + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/CrucibleDevPlugin.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/CrucibleDevPlugin.java new file mode 100644 index 00000000..74982613 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/CrucibleDevPlugin.java @@ -0,0 +1,440 @@ +package io.github.cruciblemc.forgegradle; + +import com.anatawa12.forge.gradle.separated.SeparatedLauncher; +import io.github.cruciblemc.forgegradle.tasks.DelayedJar; +import io.github.cruciblemc.forgegradle.tasks.DeterministicDecompileTask; +import io.github.cruciblemc.forgegradle.tasks.ExtractS2SRangeTask; +import io.github.cruciblemc.forgegradle.tasks.UncodeJarTask; +import io.github.cruciblemc.forgegradle.tasks.dev.GenDevProjectsTask; +import io.github.cruciblemc.forgegradle.tasks.dev.ObfuscateTask; +import io.github.cruciblemc.forgegradle.tasks.dev.SubprojectTask; +import net.minecraftforge.gradle.common.Constants; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.tasks.ApplyS2STask; +import net.minecraftforge.gradle.tasks.ProcessJarTask; +import net.minecraftforge.gradle.tasks.ProcessSrcJarTask; +import net.minecraftforge.gradle.tasks.RemapSourcesTask; +import net.minecraftforge.gradle.tasks.abstractutil.ExtractTask; +import net.minecraftforge.gradle.tasks.dev.GenBinaryPatches; +import net.minecraftforge.gradle.tasks.dev.GeneratePatches; +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.ConfigurableFileTree; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Delete; + +import java.io.File; + +import static io.github.cruciblemc.forgegradle.DevConstants.*; + +public class CrucibleDevPlugin extends DevBasePlugin { + @Override + public void applyPlugin() { + System.setProperty("com.anatawa12.forge.gradle.no-forge-maven-warn", "true"); + System.setProperty("com.anatawa12.forge.gradle.no-maven-central-warn", "true"); + super.applyPlugin(); + + // FIXME: Work around unknown version on FG + project.getDependencies().add(SeparatedLauncher.configurationName, + "com.anatawa12.forge:separated:1.2-1.1.0"); + + // set folders + getExtension().setFmlDir("forge/fml"); + getExtension().setForgeDir("forge"); + getExtension().setBukkitDir("bukkit"); + + createJarProcessTasks(); + createProjectTasks(); + createEclipseTasks(); + createMiscTasks(); + createSourceCopyTasks(); + createPackageTasks(); + + // the master setup task. + Task task = makeTask("setupCrucible", DefaultTask.class); + task.dependsOn("extractCauldronSources", "generateProjects", "eclipse", "copyAssets"); + task.setGroup("crucible"); + task.setDescription("Configures the development workspace for Crucible."); + + // clean packages + { + Delete del = makeTask("cleanPackages", Delete.class); + del.delete("build/distributions"); + del.setDescription("Cleans up old packages"); + } + + // the master task. + task = makeTask("buildPackages"); + task.dependsOn("cleanPackages", "packageServer", "packageApi"); + task.setGroup("crucible"); + task.setDescription("Builds all distribution package for Crucible"); + } + + @Override + protected final DelayedFile getDevJson() { + return delayedFile(DevConstants.EXTRA_JSON_DEV); + } + + protected void createJarProcessTasks() { + ProcessJarTask task2 = makeTask("deobfuscateJar", ProcessJarTask.class); + { + task2.setInJar(delayedFile(Constants.JAR_MERGED)); + task2.setOutCleanJar(delayedFile(JAR_SRG_CDN)); + task2.setSrg(delayedFile(JOINED_SRG)); + task2.setExceptorCfg(delayedFile(JOINED_EXC)); + task2.setExceptorJson(delayedFile(EXC_JSON)); + task2.addTransformerClean(delayedFile(FML_RESOURCES + "/fml_at.cfg")); + task2.addTransformerClean(delayedFile(FORGE_RESOURCES + "/forge_at.cfg")); + task2.setApplyMarkers(true); + task2.dependsOn("downloadMcpTools", "mergeJars"); + } + + DeterministicDecompileTask task3 = makeTask("decompile", DeterministicDecompileTask.class); + { + task3.setInJar(delayedFile(JAR_SRG_CDN)); + task3.setOutJar(delayedFile(ZIP_DECOMP_CDN)); + task3.setFernFlower(delayedFile(Constants.FERNFLOWER)); + task3.setPatch(delayedFile(MCP_PATCH_DIR)); + task3.setAstyleConfig(delayedFile(ASTYLE_CFG)); + task3.setDoesCache(false); + task3.dependsOn("downloadMcpTools", "deobfuscateJar"); + } + + ProcessSrcJarTask task4 = makeTask("forgePatchJar", ProcessSrcJarTask.class); + { + task4.setInJar(delayedFile(ZIP_DECOMP_CDN)); + task4.setOutJar(delayedFile(ZIP_FORGED_CDN)); + task4.addStage("fml", delayedFile(FML_PATCH_DIR), delayedFile(FML_SOURCES), delayedFile(FML_RESOURCES), delayedFile("{FML_CONF_DIR}/patches/Start.java"), delayedFile(DEOBF_DATA), delayedFile(FML_VERSIONF)); + task4.addStage("forge", delayedFile(FORGE_PATCH_DIR), delayedFile(FORGE_SOURCES), delayedFile(FORGE_RESOURCES)); + task4.addStage("bukkit", null, delayedFile(BUKKIT_SOURCES)); + task4.setDoesCache(false); + task4.setMaxFuzz(2); + task4.dependsOn("decompile", "compressDeobfData"); + } + + RemapSourcesTask task6 = makeTask("remapCleanJar", RemapSourcesTask.class); + { + task6.setInJar(delayedFile(ZIP_FORGED_CDN)); + task6.setOutJar(delayedFile(REMAPPED_CLEAN)); + task6.setMethodsCsv(delayedFile(METHODS_CSV)); + task6.setFieldsCsv(delayedFile(FIELDS_CSV)); + task6.setParamsCsv(delayedFile(PARAMS_CSV)); + task6.setDoesCache(true); + task6.setNoJavadocs(); + task6.dependsOn("forgePatchJar"); + } + + task4 = makeTask("cauldronPatchJar", ProcessSrcJarTask.class); + { + //task4.setInJar(delayedFile(ZIP_FORGED_CDN)); UNCOMMENT FOR SRG NAMES + task4.setInJar(delayedFile(REMAPPED_CLEAN)); + task4.setOutJar(delayedFile(ZIP_PATCHED_CDN)); + task4.addStage("Cauldron", delayedFile(EXTRA_PATCH_DIR)); + task4.setDoesCache(false); + task4.setMaxFuzz(2); + task4.dependsOn("forgePatchJar", "remapCleanJar"); + } + + task6 = makeTask("remapCauldronJar", RemapSourcesTask.class); + { + task6.setInJar(delayedFile(ZIP_PATCHED_CDN)); + task6.setOutJar(delayedFile(ZIP_RENAMED_CDN)); + task6.setMethodsCsv(delayedFile(METHODS_CSV)); + task6.setFieldsCsv(delayedFile(FIELDS_CSV)); + task6.setParamsCsv(delayedFile(PARAMS_CSV)); + task6.setDoesCache(true); + task6.setNoJavadocs(); + task6.dependsOn("cauldronPatchJar"); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void createSourceCopyTasks() { + ExtractTask task = makeTask("extractCleanResources", ExtractTask.class); + { + task.exclude(JAVA_FILES); + task.setIncludeEmptyDirs(false); + task.from(delayedFile(REMAPPED_CLEAN)); + task.into(delayedFile(ECLIPSE_CLEAN_RES)); + task.dependsOn("extractWorkspace", "remapCleanJar"); + } + + task = makeTask("extractCleanSource", ExtractTask.class); + { + task.include(JAVA_FILES); + task.setIncludeEmptyDirs(false); + task.from(delayedFile(REMAPPED_CLEAN)); + task.into(delayedFile(ECLIPSE_CLEAN_SRC)); + task.dependsOn("extractCleanResources"); + } + + task = makeTask("extractCauldronResources", ExtractTask.class); + { + task.exclude(JAVA_FILES); + task.from(delayedFile(ZIP_RENAMED_CDN)); + task.into(delayedFile(ECLIPSE_CDN_RES)); + task.dependsOn("remapCauldronJar", "extractWorkspace"); + task.onlyIf((Spec) __ -> { + File dir = delayedFile(ECLIPSE_CDN_RES).call(); + if (!dir.exists()) + return true; + + ConfigurableFileTree tree = project.fileTree(dir); + tree.include("**/*.java"); + + return !tree.isEmpty(); + }); + } + + task = makeTask("extractCauldronSources", ExtractTask.class); + { + task.include(JAVA_FILES); + task.from(delayedFile(ZIP_RENAMED_CDN)); + task.into(delayedFile(ECLIPSE_CDN_SRC)); + task.dependsOn("extractCauldronResources"); + task.onlyIf((Spec) __ -> { + File dir = delayedFile(ECLIPSE_CDN_SRC).call(); + if (!dir.exists()) + return true; + + ConfigurableFileTree tree = project.fileTree(dir); + tree.include("**/*.java"); + + return !tree.isEmpty(); + }); + } + } + + private void createProjectTasks() { + ExtractTask extract = makeTask("extractRes", ExtractTask.class); + { + extract.into(delayedFile(EXTRACTED_RES)); + var projectFiles = delayedFile("src/main").call().listFiles(); + if (projectFiles != null) { + for (File f : projectFiles) { + if (f.isDirectory()) + continue; + String path = f.getAbsolutePath(); + if (path.endsWith(".jar") || path.endsWith(".zip")) + extract.from(delayedFile(path)); + } + } + } + + GenDevProjectsTask task = makeTask("generateProjectClean", GenDevProjectsTask.class); + { + task.setTargetDir(delayedFile(ECLIPSE_CLEAN)); + task.setJson(delayedFile(EXTRA_JSON_DEV)); // Change to FmlConstants.JSON_BASE eventually, so that it's the base vanilla json + task.addSource(delayedFile(ECLIPSE_CLEAN_SRC)); + task.addResource(delayedFile(ECLIPSE_CLEAN_RES)); + + task.setMcVersion(delayedString("{MC_VERSION}")); + task.setMappingChannel(delayedString("{MAPPING_CHANNEL}")); + task.setMappingVersion(delayedString("{MAPPING_VERSION}")); + + task.dependsOn("extractNatives"); + } + + task = makeTask("generateProjectCauldron", GenDevProjectsTask.class); + { + task.setJson(delayedFile(EXTRA_JSON_DEV)); + task.setTargetDir(delayedFile(ECLIPSE_CDN)); + task.setUseLibrariesConfiguration(true); + + task.addSource(delayedFile(ECLIPSE_CDN_SRC)); + task.addSource(delayedFile(EXTRA_SOURCES)); + task.addTestSource(delayedFile(EXTRA_TEST_SOURCES)); + + task.addResource(delayedFile(ECLIPSE_CDN_RES)); + task.addResource(delayedFile(EXTRA_RESOURCES)); + task.addResource(delayedFile(EXTRACTED_RES)); + task.addTestSource(delayedFile(EXTRA_TEST_SOURCES)); + + task.setMcVersion(delayedString("{MC_VERSION}")); + task.setMappingChannel(delayedString("{MAPPING_CHANNEL}")); + task.setMappingVersion(delayedString("{MAPPING_VERSION}")); + + task.dependsOn("extractRes", "extractNatives"); + } + + makeTask("generateProjects").dependsOn("generateProjectClean", "generateProjectCauldron"); + } + + private void createEclipseTasks() { + SubprojectTask task = makeTask("eclipseClean", SubprojectTask.class); + { + task.setProjectName(ECLIPSE_CLEAN_PROJECT); + task.setTasks("eclipse"); + task.dependsOn("extractCleanSource", "generateProjects"); + } + task = makeTask("eclipseCauldron", SubprojectTask.class); + { + task.setProjectName(ECLIPSE_CAULDRON_PROJECT); + task.setTasks("eclipse"); + task.dependsOn("extractCauldronSources", "generateProjects"); + } + + makeTask("eclipse").dependsOn("eclipseClean", "eclipseCauldron"); + } + + @SuppressWarnings("unused") + private void createMiscTasks() { + DelayedFile rangeMapClean = delayedFile("{BUILD_DIR}/tmp/rangemapCLEAN.txt"); + DelayedFile rangeMapDirty = delayedFile("{BUILD_DIR}/tmp/rangemapDIRTY.txt"); + + ExtractS2SRangeTask extractRange = makeTask("extractRangeCauldron", ExtractS2SRangeTask.class); + { + extractRange.setLibsFromProject(delayedFile(ECLIPSE_CDN + "/build.gradle"), "compile", true); + extractRange.addIn(delayedFile(ECLIPSE_CDN_SRC)); + extractRange.setRangeMap(rangeMapDirty); + } + + ApplyS2STask applyS2S = makeTask("retroMapCauldron", ApplyS2STask.class); + { + applyS2S.addIn(delayedFile(ECLIPSE_CDN_SRC)); + applyS2S.setOut(delayedFile(PATCH_DIRTY)); + applyS2S.addSrg(delayedFile(MCP_2_SRG_SRG)); + applyS2S.addExc(delayedFile(MCP_EXC)); + applyS2S.addExc(delayedFile(SRG_EXC)); // just in case + applyS2S.setRangeMap(rangeMapDirty); + applyS2S.dependsOn("genSrgs", extractRange); + String[] paths = {DevConstants.FML_RESOURCES, DevConstants.FORGE_RESOURCES, DevConstants.EXTRA_RESOURCES}; + for (String path : paths) { + for (File f : project.fileTree(delayedFile(path).call()).getFiles()) { + if (f.getPath().endsWith(".exc")) + applyS2S.addExc(f); + else if (f.getPath().endsWith(".srg")) + applyS2S.addSrg(f); + } + } + } + + extractRange = makeTask("extractRangeClean", ExtractS2SRangeTask.class); + { + extractRange.setLibsFromProject(delayedFile(ECLIPSE_CLEAN + "/build.gradle"), "compile", true); + extractRange.addIn(delayedFile(REMAPPED_CLEAN)); + extractRange.setRangeMap(rangeMapClean); + } + + applyS2S = makeTask("retroMapClean", ApplyS2STask.class); + { + applyS2S.addIn(delayedFile(REMAPPED_CLEAN)); + applyS2S.setOut(delayedFile(PATCH_CLEAN)); + applyS2S.addSrg(delayedFile(MCP_2_SRG_SRG)); + applyS2S.addExc(delayedFile(MCP_EXC)); + applyS2S.addExc(delayedFile(SRG_EXC)); // just in case + applyS2S.setRangeMap(rangeMapClean); + applyS2S.dependsOn("genSrgs", extractRange); + } + + GeneratePatches task2 = makeTask("genPatches", GeneratePatches.class); + { + task2.setPatchDir(delayedFile(EXTRA_PATCH_DIR)); + task2.setOriginal(delayedFile(ECLIPSE_CLEAN_SRC)); + task2.setChanged(delayedFile(ECLIPSE_CDN_SRC)); + task2.setOriginalPrefix("../src-base/minecraft"); + task2.setChangedPrefix("../src-work/minecraft"); + task2.getTaskDependencies().getDependencies(task2).clear(); // remove all the old dependants. + task2.setGroup("Crucible"); + task2.setDescription("Create patches from your changes within the dev workspace"); + } + + Delete clean = makeTask("cleanCauldron", Delete.class); + { + clean.delete("eclipse"); + clean.setGroup("Clean"); + } + project.getTasks().getByName("clean").dependsOn("cleanCauldron"); + + ObfuscateTask obf = makeTask("obfuscateJar", ObfuscateTask.class); + { + obf.setSrg(delayedFile(MCP_2_NOTCH_SRG)); + obf.setExc(delayedFile(JOINED_EXC)); + obf.setReverse(false); + obf.setPreFFJar(delayedFile(JAR_SRG_CDN)); + obf.setOutJar(delayedFile(REOBF_TMP)); + obf.setProjectName(ECLIPSE_CAULDRON_PROJECT); + obf.setMethodsCsv(delayedFile(METHODS_CSV)); + obf.setFieldsCsv(delayedFile(FIELDS_CSV)); + obf.dependsOn("genSrgs", ECLIPSE_CAULDRON_PROJECT + ":jar"); + } + + GenBinaryPatches task3 = makeTask("genBinPatches", GenBinaryPatches.class); + { + task3.setCleanClient(delayedFile(Constants.JAR_CLIENT_FRESH)); + task3.setCleanServer(delayedFile(Constants.JAR_SERVER_FRESH)); + task3.setCleanMerged(delayedFile(Constants.JAR_MERGED)); + task3.setDirtyJar(delayedFile(REOBF_TMP)); + task3.setDeobfDataLzma(delayedFile(DEOBF_DATA)); + task3.setOutJar(delayedFile(BINPATCH_TMP)); + task3.setSrg(delayedFile(JOINED_SRG)); + task3.addPatchList(delayedFileTree(EXTRA_PATCH_DIR)); + task3.addPatchList(delayedFileTree(FORGE_PATCH_DIR)); + task3.addPatchList(delayedFileTree(FML_PATCH_DIR)); + task3.dependsOn("obfuscateJar", "compressDeobfData"); + } + + var uncode = makeTask("uncodeCrucible", UncodeJarTask.class); + { + uncode.setDescription("Removes all code from minecraft classes"); + uncode.setInputJar(delayedFile(ECLIPSE_CDN + "/build/libs/cauldron.jar")); + uncode.setOutJar(delayedFile("build/tmp/crucibleUncoded.jar")); + uncode.dependsOn(ECLIPSE_CAULDRON_PROJECT + ":jar"); + } + } + + private void createPackageTasks() { + + final DelayedJar uni = makeTask("packageServer", DelayedJar.class); + { + uni.getArchiveClassifier().set("server"); + uni.getInputs().file(delayedFile(EXTRA_JSON_REL)); + uni.getOutputs().upToDateWhen(CALL_FALSE); + uni.from(delayedZipTree(BINPATCH_TMP)); + uni.from(delayedFileTree(EXTRA_RESOURCES)); + uni.from(delayedFileTree(FORGE_RESOURCES)); + uni.from(delayedFileTree(FML_RESOURCES)); + uni.from(delayedFileTree(EXTRACTED_RES)); + uni.from(delayedFile(FML_VERSIONF)); + uni.from(delayedFile(FML_LICENSE)); + uni.from(delayedFile(FML_CREDITS)); + uni.from(delayedFile(FORGE_LICENSE)); + uni.from(delayedFile(FORGE_CREDITS)); + uni.from(delayedFile(PAULSCODE_LISCENCE1)); + uni.from(delayedFile(PAULSCODE_LISCENCE2)); + uni.from(delayedFile(DEOBF_DATA)); + uni.from(delayedFile(CHANGELOG)); + uni.from(delayedFile(VERSION_JSON)); + uni.exclude("devbinpatches.pack.lzma"); + uni.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + uni.setIncludeEmptyDirs(false); + + uni.getDestinationDirectory().set(delayedFile("{BUILD_DIR}/distributions").call()); + uni.dependsOn("genBinPatches"); + } + project.getArtifacts().add("archives", uni); + + final DelayedJar lib = makeTask("packageApi", DelayedJar.class); + { + lib.from(delayedZipTree("build/tmp/crucibleUncoded.jar")); + lib.getDestinationDirectory().set(delayedFile("{BUILD_DIR}/libs").call()); + lib.setIncludeEmptyDirs(false); + lib.dependsOn("uncodeCrucible"); + } + project.getArtifacts().add("archives", lib); + } + + @Override + public void afterEvaluate() { + super.afterEvaluate(); + + SubprojectTask task = (SubprojectTask) project.getTasks().getByName("eclipseClean"); + task.configureProject(getExtension().getSubprojects()); + task.configureProject(getExtension().getCleanProject()); + + task = (SubprojectTask) project.getTasks().getByName("eclipseCauldron"); + task.configureProject(getExtension().getSubprojects()); + task.configureProject(getExtension().getCleanProject()); + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevBasePlugin.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevBasePlugin.java new file mode 100644 index 00000000..58cc1c41 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevBasePlugin.java @@ -0,0 +1,197 @@ +package io.github.cruciblemc.forgegradle; + +import com.google.common.base.Throwables; +import groovy.lang.Closure; +import io.github.cruciblemc.forgegradle.tasks.dev.ObfuscateTask; +import net.minecraftforge.gradle.common.BasePlugin; +import net.minecraftforge.gradle.common.Constants; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.json.JsonFactory; +import net.minecraftforge.gradle.json.version.AssetIndex; +import net.minecraftforge.gradle.json.version.Library; +import net.minecraftforge.gradle.tasks.CopyAssetsTask; +import net.minecraftforge.gradle.tasks.GenSrgTask; +import net.minecraftforge.gradle.tasks.MergeJarsTask; +import net.minecraftforge.gradle.tasks.abstractutil.DownloadTask; +import net.minecraftforge.gradle.tasks.abstractutil.ExtractTask; +import net.minecraftforge.gradle.tasks.dev.CompressLZMA; +import org.gradle.api.Project; +import org.gradle.api.tasks.Copy; + +import java.io.File; + +public class DevBasePlugin extends BasePlugin { + protected static final String[] JAVA_FILES = new String[]{"**.java", "*.java", "**/*.java"}; + + @Override + public void applyPlugin() { + ExtractTask extractWorkspace = makeTask("extractWorkspace", ExtractTask.class); + { + extractWorkspace.getOutputs().upToDateWhen(new Closure(null) { + public Boolean call(Object... obj) { + File file = new File(project.getProjectDir(), "eclipse"); + return (file.exists() && file.isDirectory()); + } + }); + extractWorkspace.from(delayedFile(DevConstants.WORKSPACE_ZIP)); + extractWorkspace.into(delayedFile(DevConstants.WORKSPACE)); + } + + CompressLZMA compressDeobfData = makeTask("compressDeobfData", CompressLZMA.class); + { + compressDeobfData.setInputFile(delayedFile(DevConstants.NOTCH_2_SRG_SRG)); + compressDeobfData.setOutputFile(delayedFile(DevConstants.DEOBF_DATA)); + compressDeobfData.dependsOn("genSrgs"); + } + + MergeJarsTask mergeJars = makeTask("mergeJars", MergeJarsTask.class); + { + mergeJars.setClient(delayedFile(Constants.JAR_CLIENT_FRESH)); + mergeJars.setServer(delayedFile(Constants.JAR_SERVER_FRESH)); + mergeJars.setOutJar(delayedFile(Constants.JAR_MERGED)); + mergeJars.setMergeCfg(delayedFile(DevConstants.MERGE_CFG)); + mergeJars.setMcVersion(delayedString("{MC_VERSION}")); + mergeJars.dependsOn("downloadClient", "downloadServer"); + } + + CopyAssetsTask copyAssets = makeTask("copyAssets", CopyAssetsTask.class); + { + copyAssets.setAssetsDir(delayedFile(Constants.ASSETS)); + copyAssets.setOutputDir(delayedFile(DevConstants.ECLIPSE_ASSETS)); + copyAssets.setAssetIndex(assetIndexClosure()); + copyAssets.dependsOn("getAssets", "extractWorkspace"); + } + + GenSrgTask genSrgs = makeTask("genSrgs", GenSrgTask.class); + { + genSrgs.setInSrg(delayedFile(DevConstants.JOINED_SRG)); + genSrgs.setInExc(delayedFile(DevConstants.JOINED_EXC)); + genSrgs.setMethodsCsv(delayedFile(DevConstants.METHODS_CSV)); + genSrgs.setFieldsCsv(delayedFile(DevConstants.FIELDS_CSV)); + genSrgs.setNotchToSrg(delayedFile(DevConstants.NOTCH_2_SRG_SRG)); + genSrgs.setNotchToMcp(delayedFile(DevConstants.NOTCH_2_MCP_SRG)); + genSrgs.setSrgToMcp(delayedFile(DevConstants.SRG_2_MCP_SRG)); + genSrgs.setMcpToSrg(delayedFile(DevConstants.MCP_2_SRG_SRG)); + genSrgs.setMcpToNotch(delayedFile(DevConstants.MCP_2_NOTCH_SRG)); + genSrgs.setSrgExc(delayedFile(DevConstants.SRG_EXC)); + genSrgs.setMcpExc(delayedFile(DevConstants.MCP_EXC)); + genSrgs.dependsOn("extractMcpData"); + } + } + + @Override + public final void applyOverlayPlugin() { + // nothing. + } + + @Override + public final boolean canOverlayPlugin() { + return false; + } + + @Override + protected DelayedFile getDevJson() { + return delayedFile(DevConstants.JSON_DEV); + } + + @Override + public void afterEvaluate() { + super.afterEvaluate(); + + // set obfuscate extras + ObfuscateTask obfuscateJar = (ObfuscateTask) project.getTasks().getByName("obfuscateJar"); + obfuscateJar.setExtraSrg(getExtension().getSrgExtra()); + obfuscateJar.configureProject(getExtension().getSubprojects()); + obfuscateJar.configureProject(getExtension().getDirtyProject()); + + project.getTasks().getByName("getAssetsIndex").dependsOn("getVersionJson"); + + ExtractTask extractNatives = makeTask("extractNativesNew", ExtractTask.class); + { + extractNatives.exclude("META-INF", "META-INF/**", "META-INF/*"); + extractNatives.into(delayedFile(Constants.NATIVES_DIR)); + } + + Copy copyNatives = makeTask("extractNatives", Copy.class); + { + copyNatives.from(delayedFile(Constants.NATIVES_DIR)); + copyNatives.exclude("META-INF", "META-INF/**", "META-INF/*"); + copyNatives.into(delayedFile(DevConstants.ECLIPSE_NATIVES)); + copyNatives.dependsOn("extractWorkspace", extractNatives); + } + + DelayedFile devJson = getDevJson(); + if (devJson == null) { + project.getLogger().info("Dev json not set, could not create native downloads tasks"); + return; + } + + if (version == null) { + File jsonFile = devJson.call().getAbsoluteFile(); + try { + version = JsonFactory.loadVersion(jsonFile, jsonFile.getParentFile()); + } catch (Exception e) { + project.getLogger().error(jsonFile + " could not be parsed"); + Throwables.throwIfUnchecked(e); + } + } + + for (Library lib : version.getLibraries()) { + if (lib.natives != null) { + String path = lib.getPathNatives(); + String taskName = "downloadNatives-" + lib.getArtifactName().split(":")[1]; + + DownloadTask task = makeTask(taskName, DownloadTask.class); + { + task.setOutput(delayedFile("{CACHE_DIR}/minecraft/" + path)); + task.setUrl(delayedString(lib.getUrl() + path)); + } + + extractNatives.from(delayedFile("{CACHE_DIR}/minecraft/" + path)); + extractNatives.dependsOn(taskName); + } + } + } + + protected Class getExtensionClass() { + return DevExtension.class; + } + + protected DevExtension getOverlayExtension() { + // never happens. + return null; + } + + @Override + public String resolve(String pattern, Project project, DevExtension exten) { + pattern = super.resolve(pattern, project, exten); + + // MCP_DATA_DIR wont be resolved if the data dir doesnt eixts,,, hence... + pattern = pattern.replace("{MCP_DATA_DIR}", "{FML_CONF_DIR}"); + + // For simplicities sake, if the version is in the standard format of {MC_VERSION}-{realVersion} + // lets trim the MC version from the replacement string. + String version = project.getVersion().toString(); + String mcSafe = exten.getVersion().replace('-', '_'); + if (version.startsWith(mcSafe + "-")) { + version = version.substring(mcSafe.length() + 1); + } + pattern = pattern.replace("{VERSION}", version); + pattern = pattern.replace("{MAIN_CLASS}", exten.getMainClass()); + pattern = pattern.replace("{FML_TWEAK_CLASS}", exten.getTweakClass()); + pattern = pattern.replace("{INSTALLER_VERSION}", exten.getInstallerVersion()); + pattern = pattern.replace("{FML_DIR}", exten.getFmlDir()); + pattern = pattern.replace("{FORGE_DIR}", exten.getForgeDir()); + pattern = pattern.replace("{BUKKIT_DIR}", exten.getBukkitDir()); + pattern = pattern.replace("{FML_CONF_DIR}", exten.getFmlDir() + "/conf"); + return pattern; + } + + public Closure assetIndexClosure() { + return new Closure<>(this, null) { + public AssetIndex call(Object... obj) { + return getAssetIndex(); + } + }; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevConstants.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevConstants.java new file mode 100644 index 00000000..f526490f --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevConstants.java @@ -0,0 +1,179 @@ +package io.github.cruciblemc.forgegradle; + +import groovy.lang.Closure; +import org.gradle.api.Project; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; + +public final class DevConstants { + private DevConstants() { + + } + + static final String DEOBF_DATA = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_srg/{MC_VERSION}/deobfuscation_data-{MC_VERSION}.lzma"; + + // other generated stuff + static final String INSTALLER_BASE = "{BUILD_DIR}/tmp/installer_base.{INSTALLER_VERSION}.jar"; + static final String INSTALL_PROFILE = "{BUILD_DIR}/tmp/install_profile.json"; + static final String REOBF_TMP = "{BUILD_DIR}/tmp/recomp_obfed.jar"; + static final String MCP_2_SRG_SRG = "{BUILD_DIR}/tmp/mcp2srg.srg"; + static final String MCP_2_NOTCH_SRG = "{BUILD_DIR}/tmp/mcp2notch.srg"; + static final String SRG_2_MCP_SRG = "{BUILD_DIR}/tmp/srg2mcp.srg"; + static final String NOTCH_2_MCP_SRG = "{BUILD_DIR}/tmp/notch2mcp.srg"; + static final String NOTCH_2_SRG_SRG = "{BUILD_DIR}/tmp/notch2srg.srg"; + static final String SRG_EXC = "{BUILD_DIR}/tmp/srg.exc"; + static final String MCP_EXC = "{BUILD_DIR}/tmp/mcp.exc"; + static final String JAVADOC_TMP = "{BUILD_DIR}/tmp/javadoc"; + static final String BINPATCH_TMP = "{BUILD_DIR}/tmp/bin_patches.jar"; + static final String LAUNCH4J_DIR = "{BUILD_DIR}/launch4j_exec"; + static final String VERSION_JSON = "{BUILD_DIR}/tmp/version.json"; + static final String USERDEV_RANGEMAP = "{BUILD_DIR}/tmp/user_dev_range.txt"; + static final String EXC_MODIFIERS_DIRTY = "{BUILD_DIR}/tmp/exc_modifiers_dirty.txt"; + static final String EXC_MODIFIERS_CLEAN = "{BUILD_DIR}/tmp/exc_modifiers_clean.txt"; + + // mappings + static final String METHODS_CSV = "{MCP_DATA_DIR}/methods.csv"; + static final String FIELDS_CSV = "{MCP_DATA_DIR}/fields.csv"; + static final String PARAMS_CSV = "{MCP_DATA_DIR}/params.csv"; + static final String PACK_CSV = "{FML_CONF_DIR}/packages.csv"; + static final String JOINED_SRG = "{FML_CONF_DIR}/joined.srg"; + static final String JOINED_EXC = "{FML_CONF_DIR}/joined.exc"; + static final String ASTYLE_CFG = "{FML_CONF_DIR}/astyle.cfg"; + static final String EXC_JSON = "{FML_CONF_DIR}/exceptor.json"; + static final String MCP_PATCH = "{FML_CONF_DIR}/patches/minecraft_ff.patch"; + static final String MCP_PATCH_DIR = "{FML_CONF_DIR}/patches/minecraft_ff"; + static final String MERGE_CFG = "{FML_DIR}/mcp_merge.cfg"; + + // jars. + static final String JAR_SRG_FML = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_srg/{MC_VERSION}/minecraft_srg_fml-{MC_VERSION}.jar"; + static final String JAR_SRG_FORGE = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_srg/{MC_VERSION}/minecraft_srg_forge-{MC_VERSION}.jar"; + static final String JAR_SRG_CDN = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_srg/{MC_VERSION}/minecraft_srg_cauldron-{MC_VERSION}.jar"; + static final String JAR_SRG_EDU = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_srg/{MC_VERSION}/minecraft_srg_edu-{MC_VERSION}.jar"; + static final String ZIP_DECOMP_FML = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_decomp/{MC_VERSION}/minecraft_decomp_fml-{MC_VERSION}.zip"; + static final String ZIP_DECOMP_FORGE = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_decomp/{MC_VERSION}/minecraft_decomp_forge-{MC_VERSION}.zip"; + static final String ZIP_DECOMP_CDN = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_decomp/{MC_VERSION}/minecraft_decomp_cauldron-{MC_VERSION}.zip"; + static final String ZIP_DECOMP_EDU = "{CACHE_DIR}/minecraft/net/minecraft/minecraft_decomp/{MC_VERSION}/minecraft_decomp_edu-{MC_VERSION}.zip"; + static final String LAUNCH4J = "{CACHE_DIR}/minecraft/launch4j.zip"; + + // fml intermediate jars + static final String ZIP_PATCHED_FML = "{BUILD_DIR}/fmlTmp/minecraft_patched.zip"; + + // forge intermediate jars + static final String ZIP_FMLED_FORGE = "{BUILD_DIR}/forgeTmp/minecraft_fmlpatched.zip"; + static final String ZIP_PATCHED_FORGE = "{BUILD_DIR}/forgeTmp/minecraft_patches.zip"; + static final String ZIP_RENAMED_FORGE = "{BUILD_DIR}/forgeTmp/minecraft_renamed.zip"; + + //Cauldron intermediate jars + static final String ZIP_FORGED_CDN = "{BUILD_DIR}/cauldronTmp/minecraft_fmlpatched.zip"; + static final String ZIP_PATCHED_CDN = "{BUILD_DIR}/cauldronTmp/minecraft_patched.zip"; + static final String ZIP_RENAMED_CDN = "{BUILD_DIR}/cauldronTmp/minecraft_renamed.zip"; + + //MC EDU intermediate jars + static final String ZIP_FORGED_EDU = "{BUILD_DIR}/eduTmp/minecraft_fmlpatched.zip"; + static final String ZIP_PATCHED_EDU = "{BUILD_DIR}/eduTmp/minecraft_patched.zip"; + static final String ZIP_RENAMED_EDU = "{BUILD_DIR}/eduTmp/minecraft_renamed.zip"; + + // other stuff + static final String CHANGELOG = "{BUILD_DIR}/distributions/{PROJECT}-{MC_VERSION_SAFE}-{VERSION}-changelog.txt"; + static final String USERDEV_SRG_SRC = "{BUILD_DIR}/tmp/user_dev_srg_src.zip"; + + // necesary for patch generation + static final String PATCH_CLEAN = "{BUILD_DIR}/tmp/clean-path-base.zip"; + static final String PATCH_DIRTY = "{BUILD_DIR}/tmp/dirty-patch-base.zip"; + static final String REMAPPED_CLEAN = "{BUILD_DIR}/tmp/clean.jar"; + static final String REMAPPED_DIRTY = "{BUILD_DIR}/tmp/dirty.jar"; + + // jsons + static final String JSON_DEV = "{FML_DIR}/jsons/{MC_VERSION}-dev.json"; + static final String JSON_REL = "{FML_DIR}/jsons/{MC_VERSION}-rel.json"; + static final String JSON_BASE = "{FML_DIR}/jsons/{MC_VERSION}.json"; + static final String EXTRA_JSON_DEV = "jsons/{MC_VERSION}-dev.json"; + static final String EXTRA_JSON_REL = "jsons/{MC_VERSION}-rel.json"; + static final String EXTRA_JSON_BASE = "jsons/{MC_VERSION}.json"; + + // eclipse folders More stuff only for the Dev plugins + static final String WORKSPACE_ZIP = "eclipse-workspace-dev.zip"; + static final String WORKSPACE = "eclipse"; + static final String ECLIPSE_CLEAN = WORKSPACE + "/Clean"; + static final String ECLIPSE_CLEAN_PROJECT = ":" + ECLIPSE_CLEAN.replace('/', ':'); + static final String ECLIPSE_CLEAN_SRC = ECLIPSE_CLEAN + "/src/main/java"; + static final String ECLIPSE_CLEAN_START = ECLIPSE_CLEAN + "/src/main/start"; + static final String ECLIPSE_CLEAN_RES = ECLIPSE_CLEAN + "/src/main/resources"; + static final String ECLIPSE_FML = WORKSPACE + "/FML"; + static final String ECLIPSE_FML_SRC = ECLIPSE_FML + "/src/main/java"; + static final String ECLIPSE_FML_START = ECLIPSE_FML + "/src/main/start"; + static final String ECLIPSE_FML_RES = ECLIPSE_FML + "/src/main/resources"; + static final String ECLIPSE_FORGE = WORKSPACE + "/Forge"; + static final String ECLIPSE_FORGE_SRC = ECLIPSE_FORGE + "/src/main/java"; + static final String ECLIPSE_FORGE_START = ECLIPSE_FORGE + "/src/main/start"; + static final String ECLIPSE_FORGE_RES = ECLIPSE_FORGE + "/src/main/resources"; + static final String ECLIPSE_CDN = WORKSPACE + "/cauldron"; + static final String ECLIPSE_CAULDRON_PROJECT = ":" + ECLIPSE_CDN.replace('/', ':'); + static final String ECLIPSE_CDN_SRC = ECLIPSE_CDN + "/src/main/java"; + static final String ECLIPSE_CDN_RES = ECLIPSE_CDN + "/src/main/resources"; + static final String ECLIPSE_EDU = WORKSPACE + "/McEdu"; + static final String ECLIPSE_EDU_SRC = ECLIPSE_EDU + "/src/main/java"; + static final String ECLIPSE_EDU_RES = ECLIPSE_EDU + "/src/main/resources"; + static final String ECLIPSE_RUN = WORKSPACE + "/run"; + static final String ECLIPSE_NATIVES = ECLIPSE_RUN + "/bin/natives"; + static final String ECLIPSE_ASSETS = ECLIPSE_RUN + "/assets"; + + // FML stuff only... + static final String FML_PATCH_DIR = "{FML_DIR}/patches/minecraft"; + static final String FML_SOURCES = "{FML_DIR}/src/main/java"; + static final String FML_RESOURCES = "{FML_DIR}/src/main/resources"; + static final String FML_TEST_SOURCES = "{FML_DIR}/src/test/java"; + static final String FML_TEST_RES = "{FML_DIR}/src/test/resources"; + static final String FML_VERSIONF = "{FML_DIR}/build/tmp/fmlversion.properties"; + static final String FML_LICENSE = "{FML_DIR}/LICENSE-fml.txt"; + static final String FML_CREDITS = "{FML_DIR}/CREDITS-fml.txt"; + static final String FML_LOGO = "{FML_DIR}/jsons/big_logo.png"; + + // Forge stuff only + static final String FORGE_PATCH_DIR = "{FORGE_DIR}/patches/minecraft"; + static final String FORGE_SOURCES = "{FORGE_DIR}/src/main/java"; + static final String FORGE_RESOURCES = "{FORGE_DIR}/src/main/resources"; + static final String FORGE_TEST_SOURCES = "{FORGE_DIR}/src/test/java"; + static final String FORGE_TEST_RES = "{FORGE_DIR}/src/test/resources"; + static final String FORGE_LICENSE = "{FORGE_DIR}/MinecraftForge-License.txt"; + static final String FORGE_CREDITS = "{FORGE_DIR}/MinecraftForge-Credits.txt"; + static final String PAULSCODE_LISCENCE1 = "{FORGE_DIR}/Paulscode IBXM Library License.txt"; + static final String PAULSCODE_LISCENCE2 = "{FORGE_DIR}/Paulscode SoundSystem CodecIBXM License.txt"; + static final String FORGE_LOGO = FORGE_RESOURCES + "/forge_logo.png"; + static final String FORGE_VERSION_JAVA = FORGE_SOURCES + "/net/minecraftforge/common/ForgeVersion.java"; + + // Extra stuff only, for the current project + static final String EXTRA_PATCH_DIR = "patches"; + static final String EXTRA_SOURCES = "src/main/java"; + static final String EXTRA_RESOURCES = "src/main/resources"; + static final String EXTRA_TEST_SOURCES = "src/test/java"; + static final String EXTRA_TEST_RES = "src/test/resources"; + + // USED ONLY FOR Cauldron.. BUT ITS BUKKIT STUFF + static final String BUKKIT_SOURCES = "{BUKKIT_DIR}/src/main/java"; + static final String EXTRACTED_RES = "{BUILD_DIR}/extractedResources"; + + static final Closure CALL_FALSE = new Closure<>(null) { + public Boolean call(Object o) { + return false; + } + }; + + public static PrintStream getTaskLogStream(Project project, String name) { + return getTaskLogStream(project.getBuildDir(), name); + } + + public static PrintStream getTaskLogStream(File buildDir, String name) { + final File taskLogs = new File(buildDir, "taskLogs"); + taskLogs.mkdirs(); + final File logFile = new File(taskLogs, name); + logFile.delete(); //Delete the old log + try { + return new PrintStream(logFile); + } catch (FileNotFoundException ignored) { + } + return null; // Should never get to here + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevExtension.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevExtension.java new file mode 100644 index 00000000..7d6da07b --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/DevExtension.java @@ -0,0 +1,128 @@ +package io.github.cruciblemc.forgegradle; + +import groovy.lang.Closure; +import net.minecraftforge.gradle.common.BaseExtension; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.util.ClosureBackedAction; + +public class DevExtension extends BaseExtension { + private String fmlDir; + private String forgeDir; + private String bukkitDir; + private String mainClass; + private String tweakClass; + private String[] repos = new String[0]; + private boolean makeJavadoc = true; + private String installerVersion = "null"; + private Action subprojects = null; + private Action cleanProject = null; + private Action dirtyProject = null; + + public DevExtension(DevBasePlugin plugin) { + super(plugin); + } + + public String getFmlDir() { + return fmlDir == null ? project.getProjectDir().getPath().replace('\\', '/') : fmlDir.replace('\\', '/'); + } + + public void setFmlDir(String fmlDir) { + this.fmlDir = fmlDir; + } + + public String getForgeDir() { + return forgeDir == null ? project.getProjectDir().getPath().replace('\\', '/') : forgeDir.replace('\\', '/'); + } + + public void setForgeDir(String forgeDir) { + this.forgeDir = forgeDir; + } + + public String getBukkitDir() { + return bukkitDir == null ? project.getProjectDir().getPath().replace('\\', '/') : bukkitDir.replace('\\', '/'); + } + + public void setBukkitDir(String bukkitDir) { + this.bukkitDir = bukkitDir; + } + + public String getMainClass() { + return mainClass == null ? "" : mainClass; + } + + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + public String getInstallerVersion() { + return installerVersion; + } + + public void setInstallerVersion(String installerVersion) { + this.installerVersion = installerVersion; + } + + public String getTweakClass() { + return tweakClass == null ? "" : tweakClass; + } + + public void setTweakClass(String tweakClass) { + this.tweakClass = tweakClass; + } + + public Action getSubprojects() { + return subprojects; + } + + public void setSubprojects(Action subprojects) { + this.subprojects = subprojects; + } + + @SuppressWarnings("rawtypes") + public void subprojects(Closure subprojects) { + this.subprojects = new ClosureBackedAction<>(subprojects); + } + + public Action getCleanProject() { + return cleanProject; + } + + public void setCleanProject(Action cleanProject) { + this.cleanProject = cleanProject; + } + + @SuppressWarnings("rawtypes") + public void cleanProject(Closure subprojects) { + this.cleanProject = new ClosureBackedAction(subprojects); + } + + public Action getDirtyProject() { + return dirtyProject; + } + + public void setDirtyProject(Action dirtyProject) { + this.dirtyProject = dirtyProject; + } + + @SuppressWarnings("rawtypes") + public void dirtyProject(Closure subprojects) { + this.dirtyProject = new ClosureBackedAction<>(subprojects); + } + + public boolean getMakeJavadoc() { + return makeJavadoc; + } + + public void setMakeJavadoc(boolean makeJavadoc) { + this.makeJavadoc = makeJavadoc; + } + + public String[] getRepos() { + return repos; + } + + public void setRepos(String[] repos) { + this.repos = repos; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/reobf/JarRemapperWrapper.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/reobf/JarRemapperWrapper.java new file mode 100644 index 00000000..38b01430 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/reobf/JarRemapperWrapper.java @@ -0,0 +1,95 @@ +package io.github.cruciblemc.forgegradle.reobf; + +import net.md_5.specialsource.Jar; +import net.md_5.specialsource.JarMapping; +import net.md_5.specialsource.JarRemapper; +import net.md_5.specialsource.RemapperProcessor; +import net.md_5.specialsource.repo.ClassRepo; +import net.md_5.specialsource.repo.JarRepo; + +import java.io.*; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Contains features from here. + * + * @author MJaroslav + */ + +public class JarRemapperWrapper extends JarRemapper { + private static final int CLASS_LEN = ".class".length(); // From parent + + private boolean copyResources = true; // From parent + private boolean copyEmptyDirectories = false; // Default value changed for this project + + public JarRemapperWrapper(RemapperProcessor preProcessor, JarMapping jarMapping, RemapperProcessor postProcessor) { + super(preProcessor, jarMapping, postProcessor); + } + + public JarRemapperWrapper(RemapperProcessor remapperPreprocessor, JarMapping jarMapping) { + super(remapperPreprocessor, jarMapping); + } + + public JarRemapperWrapper(JarMapping jarMapping) { + super(jarMapping); + } + + @Override + public void setGenerateAPI(boolean generateAPI) { + super.setGenerateAPI(generateAPI); + this.copyResources = !generateAPI; // Copy parent private value + } + + public void setCopyEmptyDirectories(boolean copyEmptyDirectories) { + this.copyEmptyDirectories = copyEmptyDirectories; + } + + @Override + public void remapJar(Jar jar, File target) throws IOException { + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(target))) { + ClassRepo repo = new JarRepo(jar); + if (jar == null) + return; + for (String name : jar.getEntryNames()) { + JarEntry entry; + try (InputStream is = jar.getResource(name)) { + byte[] data; + if (name.endsWith(".class")) { + // remap classes + name = name.substring(0, name.length() - CLASS_LEN); + + data = this.remapClassFile(is, repo); + String newName = this.map(name); + + entry = new JarEntry(newName == null ? name : newName + ".class"); + } else if (name.endsWith(".DSA") || name.endsWith(".SF")) { + // skip signatures + continue; + } else { + // copy other resources + if (!this.copyResources) { + continue; // unless generating an API + } + if (!this.copyEmptyDirectories && name.endsWith("/")) { + continue; // Don't copy empty directories + } + entry = new JarEntry(name); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int n; + byte[] b = new byte[1 << 15]; // Max class file size + while ((n = is.read(b, 0, b.length)) != -1) { + buffer.write(b, 0, n); + } + buffer.flush(); + data = buffer.toByteArray(); + } + entry.setTime(0); + out.putNextEntry(entry); + out.write(data); + } + } + } + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DelayedJar.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DelayedJar.java new file mode 100644 index 00000000..3dcb5087 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DelayedJar.java @@ -0,0 +1,20 @@ +package io.github.cruciblemc.forgegradle.tasks; + +import groovy.lang.Closure; +import org.gradle.api.tasks.bundling.Jar; + +public class DelayedJar extends Jar { + private Closure closure = null; + + @Override + public void copy() { + if (closure != null) { + super.manifest(closure); + } + super.copy(); + } + + public void setManifest(Closure closure) { + this.closure = closure; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DeterministicDecompileTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DeterministicDecompileTask.java new file mode 100644 index 00000000..597a4652 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/DeterministicDecompileTask.java @@ -0,0 +1,538 @@ +package io.github.cruciblemc.forgegradle.tasks; + +import com.github.abrarsyed.jastyle.ASFormatter; +import com.github.abrarsyed.jastyle.FileWildcardFilter; +import com.github.abrarsyed.jastyle.OptParser; +import com.google.common.base.Joiner; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import groovy.lang.Closure; +import io.github.cruciblemc.forgegradle.DevConstants; +import net.minecraftforge.gradle.common.BaseExtension; +import net.minecraftforge.gradle.common.Constants; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.extrastuff.FFPatcher; +import net.minecraftforge.gradle.extrastuff.FmlCleanup; +import net.minecraftforge.gradle.extrastuff.GLConstantFixer; +import net.minecraftforge.gradle.extrastuff.McpCleanup; +import net.minecraftforge.gradle.patching.ContextualPatch; +import net.minecraftforge.gradle.patching.ContextualPatch.HunkReport; +import net.minecraftforge.gradle.patching.ContextualPatch.PatchReport; +import net.minecraftforge.gradle.patching.ContextualPatch.PatchStatus; +import net.minecraftforge.gradle.tasks.abstractutil.CachedTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.tasks.*; +import org.gradle.process.JavaExecSpec; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static net.minecraftforge.gradle.common.Constants.EXT_NAME_MC; + +public class DeterministicDecompileTask extends CachedTask { + private static final Pattern BEFORE = Pattern.compile("(?m)((case|default).+(?:\\r\\n|\\r|\\n))(?:\\r\\n|\\r|\\n)"); + private static final Pattern AFTER = Pattern.compile("(?m)(?:\\r\\n|\\r|\\n)((?:\\r\\n|\\r|\\n)[ \\t]+(case|default))"); + @InputFile + public DelayedFile inJar; + @InputFile + private DelayedFile fernFlower; + @Internal + private DelayedFile patch; + @InputFile + private DelayedFile astyleConfig; + @Cached + @OutputFile + private DelayedFile outJar; + @Internal + private HashMap sourceMap = new HashMap<>(); + @Internal + private HashMap resourceMap = new HashMap<>(); + + /** + * This method outputs to the cleanSrc + * + * @throws Throwable Let em throw anything.. I dont care. + */ + @TaskAction + protected void doMCPStuff() throws Throwable { + // define files. + File temp = new File(this.getTemporaryDir(), this.getInJar().getName()); + + InputStream forgeflower = this.getClass().getResourceAsStream("/crucible/forgeflower.jar"); + if (forgeflower == null) + throw new IOException("Embedded forgeflower is missing and cannot be extracted!"); + + File forgeFlower = new File(this.getTemporaryDir(), "forgeFlower.jar"); + java.nio.file.Files.copy(forgeflower, forgeFlower.toPath(), StandardCopyOption.REPLACE_EXISTING); + + this.getLogger().info("Decompiling Jar"); + this.decompile(this.getInJar(), this.getTemporaryDir(), forgeFlower); + + this.getLogger().info("Applying crucible fixes"); + InputStream patches = this.getClass().getResourceAsStream("/crucible/crucible-patches.zip"); + if (patches == null) + throw new IOException("Embedded crucible-patches is missing and cannot be extracted!"); + File patchFiles = new File(this.getTemporaryDir(), "crucible-patches"); + safeUnzip(patches, patchFiles.toPath()); + this.crucibleFixJar(patchFiles, temp); + + this.getLogger().info("Loading decompiled jar"); + this.readJarAndFix(temp); + + this.saveJar(new File(this.getTemporaryDir(), this.getInJar().getName() + ".fixed.jar")); + + this.getLogger().info("Applying MCP patches"); + if (this.getPatch().isFile()) { + this.applySingleMcpPatch(this.getPatch()); + } else { + this.applyPatchDirectory(this.getPatch()); + } + + this.saveJar(new File(this.getTemporaryDir(), this.getInJar().getName() + ".patched.jar")); + + this.getLogger().info("Cleaning source"); + this.applyMcpCleanup(this.getAstyleConfig()); + + this.getLogger().info("Saving Jar"); + this.saveJar(this.getOutJar()); + } + + // this ugly horrible terrible method does the magic of patching the 1.12 forgeflower output to the 1.7.10 equivalent + @SuppressWarnings({"IOStreamConstructor", "deprecation"}) + private void crucibleFixJar(File patchFiles, File jar) throws Throwable { + HashMap sourceMap = new HashMap<>(); + HashMap resourceMap = new HashMap<>(); + + final ZipInputStream zin = new ZipInputStream(new FileInputStream(jar)); + ZipEntry entry; + String fileStr; + while ((entry = zin.getNextEntry()) != null) { + if (entry.getName().contains("META-INF")) { + continue; + } + + if (entry.isDirectory() || !entry.getName().endsWith(".java")) { + resourceMap.put(entry.getName(), ByteStreams.toByteArray(zin)); + } else { + fileStr = new String(ByteStreams.toByteArray(zin), Charset.defaultCharset()); + sourceMap.put(entry.getName(), fileStr); + } + } + zin.close(); + try (Stream pathStream = java.nio.file.Files.walk(patchFiles.toPath())) { + for (Path f : pathStream.filter(java.nio.file.Files::isRegularFile).collect(Collectors.toList())) { + ContextualPatch patch = ContextualPatch.create(Files.toString(f.toFile(), + Charset.defaultCharset()), new SrcContextProvider(sourceMap)); + this.printPatchErrors(patch.patch(false)); + } + } + + ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(jar)); + for (Map.Entry entry1 : resourceMap.entrySet()) { + zout.putNextEntry(new ZipEntry(entry1.getKey())); + zout.write(entry1.getValue()); + zout.closeEntry(); + } + + for (Map.Entry entry1 : sourceMap.entrySet()) { + zout.putNextEntry(new ZipEntry(entry1.getKey())); + zout.write(entry1.getValue().getBytes()); + zout.closeEntry(); + } + + zout.close(); + } + + private void decompile(final File inJar, final File outJar, final File fernFlower) { + this.getProject().javaexec(new Closure(this) { + private static final long serialVersionUID = 4608694547855396167L; + + @Override + public JavaExecSpec call() { + JavaExecSpec exec = (JavaExecSpec) this.getDelegate(); + + exec.args( + fernFlower.getAbsolutePath(), + "-din=1", + "-rbr=0", + "-dgs=1", + "-asc=1", + "-log=ERROR", + inJar.getAbsolutePath(), + outJar.getAbsolutePath() + ); + + exec.getMainClass().set("-jar"); + //exec.jvmArgs("-Dfile.encoding=" + Charset.defaultCharset()); + exec.setWorkingDir(fernFlower.getParentFile()); + + exec.classpath(Constants.getClassPath()); + exec.setStandardOutput(DevConstants.getTaskLogStream(DeterministicDecompileTask.this.getProject(), DeterministicDecompileTask.this.getName() + ".log")); + + exec.setMaxHeapSize("512M"); + + return exec; + } + + @Override + public JavaExecSpec call(Object obj) { + return this.call(); + } + }); + } + + private void readJarAndFix(final File jar) throws IOException { + // begin reading jar + final ZipInputStream zin = new ZipInputStream(new FileInputStream(jar)); + ZipEntry entry; + String fileStr; + + BaseExtension exten = (BaseExtension) this.getProject().getExtensions().getByName(EXT_NAME_MC); + boolean fixInterfaces = !exten.getVersion().equals("1.7.2"); + + while ((entry = zin.getNextEntry()) != null) { + // no META or dirs. wel take care of dirs later. + if (entry.getName().contains("META-INF")) { + continue; + } + + // resources or directories. + if (entry.isDirectory() || !entry.getName().endsWith(".java")) { + this.resourceMap.put(entry.getName(), ByteStreams.toByteArray(zin)); + } else { + // source! + fileStr = new String(ByteStreams.toByteArray(zin), Charset.defaultCharset()); + + // fix + fileStr = FFPatcher.processFile(new File(entry.getName()).getName(), fileStr, fixInterfaces); + + this.sourceMap.put(entry.getName(), fileStr); + } + } + + zin.close(); + } + + private void applySingleMcpPatch(File patchFile) throws Throwable { + ContextualPatch patch = ContextualPatch.create(Files.asCharSource(patchFile, Charset.defaultCharset()).read(), new ContextProvider(this.sourceMap)); + this.printPatchErrors(patch.patch(false)); + } + + private void printPatchErrors(List errors) throws Throwable { + boolean fuzzed = false; + for (PatchReport report : errors) { + if (!report.getStatus().isSuccess()) { + this.getLogger().log(LogLevel.ERROR, "Patching failed: " + report.getTarget(), report.getFailure()); + + for (HunkReport hunk : report.getHunks()) { + if (!hunk.getStatus().isSuccess()) { + this.getLogger().error("Hunk " + hunk.getHunkID() + " failed!"); + } + } + + throw report.getFailure(); + } else if (report.getStatus() == PatchStatus.Fuzzed) // catch fuzzed patches + { + this.getLogger().log(LogLevel.INFO, "Patching fuzzed: " + report.getTarget(), report.getFailure()); + fuzzed = true; + + for (HunkReport hunk : report.getHunks()) { + if (!hunk.getStatus().isSuccess()) { + this.getLogger().info("Hunk " + hunk.getHunkID() + " fuzzed " + hunk.getFuzz() + "!"); + } + } + } else { + this.getLogger().debug("Patch succeeded: " + report.getTarget()); + } + } + if (fuzzed) { + this.getLogger().lifecycle("Patches Fuzzed!"); + } + } + + private void applyPatchDirectory(File patchDir) throws Throwable { + Multimap patches = ArrayListMultimap.create(); + for (File f : Objects.requireNonNull(patchDir.listFiles(new FileWildcardFilter("*.patch")))) { + String base = f.getName(); + patches.put(base, f); + for (File e : Objects.requireNonNull(patchDir.listFiles(new FileWildcardFilter(base + ".*")))) { + patches.put(base, e); + } + } + + for (String key : patches.keySet()) { + ContextualPatch patch = this.findPatch(patches.get(key)); + if (patch == null) { + this.getLogger().lifecycle("Patch not found for set: " + key); //This should never happen, but whatever + } else { + this.printPatchErrors(patch.patch(false)); + } + } + } + + private ContextualPatch findPatch(Collection files) throws Throwable { + ContextualPatch patch = null; + for (File f : files) { + patch = ContextualPatch.create(Files.asCharSource(f, Charset.defaultCharset()).read(), new ContextProvider(this.sourceMap)); + List errors = patch.patch(true); + + boolean success = true; + for (PatchReport rep : errors) { + if (!rep.getStatus().isSuccess()) { + success = false; + break; + } + } + if (success) { + break; + } + } + return patch; + } + + private void applyMcpCleanup(File conf) throws IOException { + ASFormatter formatter = new ASFormatter(); + OptParser parser = new OptParser(formatter); + parser.parseOptionFile(conf); + + Reader reader; + Writer writer; + + GLConstantFixer fixer = new GLConstantFixer(); + ArrayList files = new ArrayList<>(this.sourceMap.keySet()); + Collections.sort(files); // Just to make sure we have the same order.. shouldn't matter on anything but lets be careful. + + for (String file : files) { + String text = this.sourceMap.get(file); + + this.getLogger().debug("Processing file: " + file); + + this.getLogger().debug("processing comments"); + text = McpCleanup.stripComments(text); + + this.getLogger().debug("fixing imports comments"); + text = McpCleanup.fixImports(text); + + this.getLogger().debug("various other cleanup"); + text = McpCleanup.cleanup(text); + + this.getLogger().debug("fixing OGL constants"); + text = fixer.fixOGL(text); + + this.getLogger().debug("formatting source"); + reader = new StringReader(text); + writer = new StringWriter(); + formatter.format(reader, writer); + reader.close(); + writer.flush(); + writer.close(); + text = writer.toString(); + + this.getLogger().debug("applying FML transformations"); + text = BEFORE.matcher(text).replaceAll("$1"); + text = AFTER.matcher(text).replaceAll("$1"); + text = FmlCleanup.renameClass(text); + + this.sourceMap.put(file, text); + } + } + + private void saveJar(File output) throws IOException { + ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output)); + + // write in resources + for (Map.Entry entry : this.resourceMap.entrySet()) { + zout.putNextEntry(new ZipEntry(entry.getKey())); + zout.write(entry.getValue()); + zout.closeEntry(); + } + + // write in sources + for (Map.Entry entry : this.sourceMap.entrySet()) { + zout.putNextEntry(new ZipEntry(entry.getKey())); + zout.write(entry.getValue().getBytes()); + zout.closeEntry(); + } + + zout.close(); + } + + private static void safeUnzip(InputStream source, Path target) throws IOException { + try (ZipInputStream zip = new ZipInputStream(source)) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + + // Prevent against traversal path attacks + Path filePath = target.resolve(entry.getName()).normalize(); + if (!filePath.startsWith(target)) { + throw new IOException("Transversal path detected: " + entry.getName()); + } + + if (entry.isDirectory()) { + java.nio.file.Files.createDirectories(filePath); + } else { + if (filePath.getParent() != null && java.nio.file.Files.notExists(filePath.getParent())) { + java.nio.file.Files.createDirectories(filePath.getParent()); + } + java.nio.file.Files.copy(zip, filePath, StandardCopyOption.REPLACE_EXISTING); + } + } + } + } + + public HashMap getSourceMap() { + return this.sourceMap; + } + + public void setSourceMap(HashMap sourceMap) { + this.sourceMap = sourceMap; + } + + public File getAstyleConfig() { + return this.astyleConfig.call(); + } + + public void setAstyleConfig(DelayedFile astyleConfig) { + this.astyleConfig = astyleConfig; + } + + public File getFernFlower() { + return this.fernFlower.call(); + } + + public void setFernFlower(DelayedFile fernFlower) { + this.fernFlower = fernFlower; + } + + public File getInJar() { + return this.inJar.call(); + } + + public void setInJar(DelayedFile inJar) { + this.inJar = inJar; + } + + public File getOutJar() { + return this.outJar.call(); + } + + public void setOutJar(DelayedFile outJar) { + this.outJar = outJar; + } + + @InputFiles + public FileCollection getPatches() { + File patches = this.patch.call(); + if (patches.isDirectory()) + return this.getProject().fileTree(patches); + else + return this.getProject().files(patches); + } + + public File getPatch() { + return this.patch.call(); + } + + public void setPatch(DelayedFile patch) { + this.patch = patch; + } + + public HashMap getResourceMap() { + return this.resourceMap; + } + + public void setResourceMap(HashMap resourceMap) { + this.resourceMap = resourceMap; + } + + /** + * A private inner class to be used with the MCPPatches only. + */ + private static class ContextProvider implements ContextualPatch.IContextProvider { + private final int STRIP = 1; + private final Map fileMap; + + public ContextProvider(Map fileMap) { + this.fileMap = fileMap; + } + + private String strip(String target) { + target = target.replace('\\', '/'); + int index = 0; + for (int x = 0; x < this.STRIP; x++) { + index = target.indexOf('/', index) + 1; + } + return target.substring(index); + } + + @Override + public List getData(String target) { + target = this.strip(target); + + if (this.fileMap.containsKey(target)) { + String[] lines = this.fileMap.get(target).split("\r\n|\r|\n"); + List ret = new ArrayList(); + Collections.addAll(ret, lines); + return ret; + } + + return null; + } + + @Override + public void setData(String target, List data) { + this.fileMap.put(this.strip(target), Joiner.on(Constants.NEWLINE).join(data)); + } + } + + public static class SrcContextProvider implements ContextualPatch.IContextProvider { + private final Map fileMap; + + private final int STRIP = 3; + + public SrcContextProvider(Map fileMap) { + this.fileMap = fileMap; + } + + public String strip(String target) { + target = target.replace('\\', '/'); + int index = 0; + for (int x = 0; x < STRIP; x++) { + index = target.indexOf('/', index) + 1; + } + return target.substring(index); + } + + @Override + public List getData(String target) { + target = strip(target); + + if (fileMap.containsKey(target)) { + String[] lines = fileMap.get(target).split("\r\n|\r|\n"); + List ret = new ArrayList(); + Collections.addAll(ret, lines); + return ret; + } + + return null; + } + + @Override + public void setData(String target, List data) { + target = strip(target); + fileMap.put(target, Joiner.on(Constants.NEWLINE).join(data)); + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/ExtractS2SRangeTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/ExtractS2SRangeTask.java new file mode 100644 index 00000000..a8e8eba2 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/ExtractS2SRangeTask.java @@ -0,0 +1,458 @@ +package io.github.cruciblemc.forgegradle.tasks; + +import com.google.code.regexp.Pattern; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import net.minecraftforge.gradle.PredefInputSupplier; +import net.minecraftforge.gradle.SequencedInputSupplier; +import net.minecraftforge.gradle.common.Constants; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.srg2source.ast.RangeExtractor; +import net.minecraftforge.srg2source.util.io.FolderSupplier; +import net.minecraftforge.srg2source.util.io.InputSupplier; +import net.minecraftforge.srg2source.util.io.ZipInputSupplier; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.*; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.*; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ExtractS2SRangeTask extends DefaultTask { + @InputFiles + private FileCollection libs; + private DelayedFile projectFile; // to get classpath from a subproject + private String projectConfig; // Also for a subProject + private boolean includeJar = false; //Include the 'jar' task for subproject. + + @Optional + @OutputFile + private DelayedFile excOutput; + + // stuff defined on the tasks.. + @Internal + private final List in = new LinkedList(); + + @OutputFile + private DelayedFile rangeMap; + + private boolean allCached = false; + private static final Pattern FILE_FROM = Pattern.compile("\\s+@\\|([\\w\\d/.]+)\\|.*$"); + private static final Pattern FILE_START = Pattern.compile("\\s*Class Start\\: ([\\w\\d.]+)$"); + + @TaskAction + public void doTask() throws IOException { + List ins = getIn(); + File rangemap = getRangeMap(); + + InputSupplier inSup; + + if (ins.size() == 0) + return; // no input. + else if (ins.size() == 1) { + // just 1 supplier. + inSup = getInput(ins.get(0)); + } else { + // multinput + inSup = new SequencedInputSupplier(); + for (File f : ins) { + ((SequencedInputSupplier) inSup).add(getInput(f)); + } + } + + // cache + inSup = cacheInputs(inSup, rangemap); + + if (rangemap.exists()) { + if (allCached) { + return; + } + + List files = inSup.gatherAll(".java"); + + // read rangemap + List lines = Files.readLines(rangemap, Charsets.UTF_8); + { + Iterator it = lines.iterator(); + while (it.hasNext()) { + String line = it.next(); + + com.google.code.regexp.Matcher match; + String fileMatch = null; + if (line.trim().startsWith("@")) { + match = FILE_FROM.matcher(line); + if (match.matches()) { + fileMatch = match.group(1).replace('\\', '/'); + } + } else { + match = FILE_START.matcher(line); + if (match.matches()) { + fileMatch = match.group(1).replace('.', '/') + ".java"; + } + } + + if (fileMatch != null && files.contains(fileMatch)) { + it.remove(); + } + } + } + + generateRangeMap(inSup, rangemap); + + lines.addAll(Files.readLines(rangemap, Charsets.UTF_8)); + Files.write(Joiner.on(Constants.NEWLINE).join(lines), rangemap, Charsets.UTF_8); + } else { + generateRangeMap(inSup, rangemap); + } + } + + private InputSupplier cacheInputs(InputSupplier input, File out) throws IOException { + boolean outExists = out.exists(); + + // read the cache + File cacheFile = new File(out + ".inputCache"); + HashSet cache = readCache(cacheFile); + + // generate the cache + List strings = input.gatherAll(".java"); + HashSet genCache = Sets.newHashSetWithExpectedSize(strings.size()); + PredefInputSupplier predef = new PredefInputSupplier(); + for (String rel : strings) { + File root = new File(input.getRoot(rel)).getCanonicalFile(); + + InputStream fis = input.getInput(rel); + byte[] array = ByteStreams.toByteArray(fis); + fis.close(); + + CacheEntry entry = new CacheEntry(rel, root, Constants.hash(array)); + genCache.add(entry); + + if (!outExists || !cache.contains(entry)) { + predef.addFile(rel, root, array); + } + } + + if (!predef.isEmpty()) { + writeCache(cacheFile, genCache); + } else { + allCached = true; + } + + return predef; + } + + private HashSet readCache(File cacheFile) throws IOException { + if (!cacheFile.exists()) + return Sets.newHashSetWithExpectedSize(0); + + List lines = Files.readLines(cacheFile, Charsets.UTF_8); + HashSet cache = Sets.newHashSetWithExpectedSize(lines.size()); + + for (String s : lines) { + String[] tokens = s.split(";"); + if (tokens.length != 3) { + getLogger().warn("Corrupted input cache! {}", cacheFile); + break; + } + cache.add(new CacheEntry(tokens[0], new File(tokens[1]), tokens[2])); + } + + return cache; + } + + private void writeCache(File cacheFile, Collection cache) throws IOException { + if (cacheFile.exists()) + cacheFile.delete(); + + cacheFile.getParentFile().mkdirs(); + cacheFile.createNewFile(); + + BufferedWriter writer = Files.newWriter(cacheFile, Charsets.UTF_8); + for (CacheEntry e : cache) { + writer.write(e.toString()); + writer.newLine(); + } + + writer.close(); + } + + private void generateRangeMap(InputSupplier inSup, File rangeMap) { + RangeExtractor extractor = new RangeExtractor(); + extractor.addLibs(getLibs().getAsPath()).setSrc(inSup); + + PrintStream stream = new PrintStream(Constants.getTaskLogStream(getProject(), this.getName() + ".log")); + extractor.setOutLogger(stream); + + boolean worked = extractor.generateRangeMap(rangeMap); + + stream.close(); + + if (!worked) + throw new RuntimeException("RangeMap generation Failed!!!"); + } + + private InputSupplier getInput(File f) throws IOException { + if (f.isDirectory()) + return new FolderSupplier(f); + else if (f.getPath().endsWith(".jar") || f.getPath().endsWith(".zip")) { + ZipInputSupplier supp = new ZipInputSupplier(); + supp.readZip(f); + return supp; + } else + throw new IllegalArgumentException("Can only make suppliers out of directories and zips right now!"); + } + + public File getRangeMap() { + return rangeMap.call(); + } + + public void setRangeMap(DelayedFile out) { + this.rangeMap = out; + } + + public File getExcOutput() { + return excOutput == null ? null : excOutput.call(); + } + + public void setExcOutput(DelayedFile out) { + this.excOutput = out; + } + + @InputFiles + public FileCollection getIns() { + return getProject().files(in); + } + + public List getIn() { + List files = new LinkedList(); + for (DelayedFile f : in) + files.add(f.call()); + return files; + } + + public void addIn(DelayedFile in) { + this.in.add(in); + } + +// public FileCollection getLibs() { +// if (projectFile != null && libs == null) // libs == null to avoid doing this any more than necessary.. +// { +// File buildscript = projectFile.call(); +// if (!buildscript.exists()) +// return null; +// +// Project proj = BasePlugin.getProject(buildscript, getProject()); +// libs = proj.getConfigurations().getByName(projectConfig); +// +// if (includeJar) { +// AbstractTask jarTask = (AbstractTask) proj.getTasks().getByName("jar"); +// executeTask(jarTask); +// File compiled = (File) jarTask.property("archivePath"); +// libs = getProject().files(compiled, libs); +// +// if (getExcOutput() != null) { +// extractExcInfo(compiled, getExcOutput()); +// } +// } +// } +// +// return libs; +// } + //replaced with FG3 method + + public FileCollection getLibs() { + FileCollection collection = null; + + for (Object o : libs) { + FileCollection col; + if (o instanceof FileCollection) { + col = (FileCollection) o; + } else { + col = getProject().files(o); + } + + if (collection == null) + collection = col; + else + collection = collection.plus(col); + } + + return collection; + } + +// private void executeTask(AbstractTask task) { +// for (Object dep : task.getTaskDependencies().getDependencies(task)) { +// executeTask((AbstractTask) dep); +// } +// +// if (!task.getState().getExecuted()) { +// getLogger().lifecycle(task.getPath()); +// task.execute(); +// } +// } + + public void setLibs(FileCollection libs) { + this.libs = libs; + } + + public void setLibsFromProject(DelayedFile buildscript, String config, boolean includeJar) { + this.projectFile = buildscript; + this.projectConfig = config; + this.includeJar = includeJar; + } + + private static class CacheEntry { + public final String path, hash; + public final File root; + + public CacheEntry(String path, File root, String hash) throws IOException { + this.path = path.replace('\\', '/'); + this.hash = hash; + this.root = root.getCanonicalFile(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((hash == null) ? 0 : hash.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((root == null) ? 0 : root.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheEntry other = (CacheEntry) obj; + if (hash == null) { + if (other.hash != null) + return false; + } else if (!hash.equals(other.hash)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + if (root == null) { + return other.root == null; + } else return root.getAbsolutePath().equals(other.root.getAbsolutePath()); + } + + @Override + public String toString() { + return path + ";" + root + ";" + hash; + } + } + + + private void extractExcInfo(File compiled, File output) { + try { + if (output.exists()) + output.delete(); + + output.getParentFile().mkdirs(); + output.createNewFile(); + + BufferedWriter writer = Files.newWriter(output, Charsets.UTF_8); + ZipInputStream inJar = null; + try { + inJar = new ZipInputStream(new BufferedInputStream(new FileInputStream(compiled))); + + while (true) { + ZipEntry entry = inJar.getNextEntry(); + + if (entry == null) break; + + if (entry.isDirectory()) continue; + + String entryName = entry.getName(); + if (!entryName.endsWith(".class") || !entryName.startsWith("net/minecraft/")) + continue; + + getProject().getLogger().debug("Processing " + entryName); + byte[] data = new byte[4096]; + ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream(); + + int len; + do { + len = inJar.read(data); + if (len > 0) { + entryBuffer.write(data, 0, len); + } + } while (len != -1); + + byte[] entryData = entryBuffer.toByteArray(); + + ClassReader cr = new ClassReader(entryData); + ClassVisitor ca = new GenerateMapClassAdapter(writer); + cr.accept(ca, 0); + } + } finally { + if (inJar != null) { + try { + inJar.close(); + } catch (IOException e) { + // ignore + } + } + } + + writer.close(); + } catch (IOException e) { + Throwables.propagate(e); + } + } + + public class GenerateMapClassAdapter extends ClassVisitor { + String className; + BufferedWriter writer; + + public GenerateMapClassAdapter(BufferedWriter writer) { + super(Opcodes.ASM5); + this.writer = writer; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.className = name; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + if (name.equals("")) + return super.visitMethod(access, name, desc, signature, exceptions); + + String clsSig = this.className + "/" + name + desc; + + try { + if ((access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC) { + writer.write(clsSig); + writer.write("=static"); + writer.newLine(); + } + } catch (IOException e) { + Throwables.propagate(e); + } + return super.visitMethod(access, name, desc, signature, exceptions); + } + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/MakeTrueSources.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/MakeTrueSources.java new file mode 100644 index 00000000..19e11bc6 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/MakeTrueSources.java @@ -0,0 +1,254 @@ +package io.github.cruciblemc.forgegradle.tasks; + +import com.github.abrarsyed.jastyle.ASFormatter; +import com.github.abrarsyed.jastyle.constants.EnumFormatStyle; +import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.tasks.abstractutil.CachedTask; +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 java.io.*; +import java.nio.charset.Charset; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class MakeTrueSources extends CachedTask { + // Before all gods I swear, this is the last time I touch upon regex + private static final String NEWLINE_REGEX = "[\\s|\\t|\\r\\n]+"; + private static final String SPACE_REGEX = "[\\s]+"; + private static final Pattern COMMENT_LINE = Pattern.compile("[^\\n]*//.*" + NEWLINE_REGEX + "\\{"); + private static final Pattern COMMENT = Pattern.compile("//.*"); + private static final Pattern FORMAT_LINE = Pattern.compile(Pattern.quote(System.lineSeparator()) + "[\\s]*\\{"); + private static final Pattern ELSEIF_LINE = Pattern.compile("\\}[\\s]*else[\\s]+if[\\s]*\\(.*\\)[\\s]*\\{"); + private static final Pattern ELSE_LINE = Pattern.compile("\\}[\\s]*else[\\s]*\\{"); + private static final Pattern CONDITION = Pattern.compile("\\(.*\\)"); + private static final Pattern CLASS = Pattern.compile(".*[\\s]+class[\\s]+[^\\n]*\\{.*\\}", Pattern.DOTALL); + + @InputFile + private DelayedFile inJar; + + @InputFile + private DelayedFile astyleConfig; + + @OutputFile + @Cached + private DelayedFile outJar; + + @Internal + private HashMap sourceMap = new HashMap(); + + @TaskAction + protected void doMCPStuff() throws Throwable { + this.getLogger().lifecycle("Loading untrue sources: " + this.getInJar().getCanonicalPath()); + this.readJarAndFix(this.getInJar()); + + this.getLogger().lifecycle("Cleaning source"); + this.applyMcpCleanup(this.getAstyleConfig()); + + this.getLogger().lifecycle("Saving Jar: " + this.getOutJar().getCanonicalPath()); + this.saveJar(this.getOutJar()); + } + + private void readJarAndFix(final File jar) throws IOException { + this.getProject().getLogger().lifecycle("Begin reading jar..."); + // begin reading jar + final ZipInputStream zin = new ZipInputStream(new FileInputStream(jar)); + ZipEntry entry = null; + String fileStr; + + while ((entry = zin.getNextEntry()) != null) { + // no META or dirs. wel take care of dirs later. + if (entry.getName().contains("META-INF")) { + continue; + } + + // resources or directories. + if (entry.isDirectory() || !entry.getName().endsWith(".java")) { + // NO-OP + } else { + // source! + fileStr = new String(ByteStreams.toByteArray(zin), Charset.defaultCharset()); + + this.sourceMap.put(entry.getName(), fileStr); + } + } + + zin.close(); + } + + private void applyMcpCleanup(File conf) throws IOException { + ASFormatter formatter = new ASFormatter(); + //OptParser parser = new OptParser(formatter); + //parser.parseOptionFile(conf); + + Reader reader; + Writer writer; + + formatter.setFormattingStyle(EnumFormatStyle.JAVA); + formatter.setBreakElseIfsMode(false); + formatter.setSpaceIndentation(4); + formatter.setClassIndent(false); + formatter.setNamespaceIndent(false); + formatter.setCaseIndent(true); + formatter.setBreakClosingHeaderBracketsMode(false); + formatter.setDeleteEmptyLinesMode(false); + formatter.setMaxInStatementIndentLength(40); + //TODO: Figure out this + //formatter.setUseProperInnerClassIndenting(true); + + ArrayList files = new ArrayList(this.sourceMap.keySet()); + Collections.sort(files); // Just to make sure we have the same order.. shouldn't matter on anything but lets be careful. + + for (String file : files) { + String text = this.sourceMap.get(file); + + this.getLogger().debug("Processing file: " + file); + + Matcher commentLineMatcher = COMMENT_LINE.matcher(text); + List lines = new ArrayList(); + + while (commentLineMatcher.find()) { + lines.add(commentLineMatcher.group()); + } + + for (String commentLine : lines) { + Matcher commentMatcher = COMMENT.matcher(commentLine); + + if (commentMatcher.find()) { + String comment = commentMatcher.group(); + String newCommentLine = commentLine.replace(comment, "") + " " + comment; + text = text.replace(commentLine, newCommentLine); + this.getLogger().lifecycle("Fixed comment: " + comment); + } + } + + lines.clear(); + + /* + Matcher elseMatcher = ELSE_LINE.matcher(text); + + while(elseMatcher.find()) { + lines.add(elseMatcher.group()); + } + + for (String elseLine : lines) { + String newElseLine = "} else {"; + text = text.replace(elseLine, newElseLine); + } + + lines.clear(); + + Matcher elseIfMatcher = ELSEIF_LINE.matcher(text); + + while(elseIfMatcher.find()) { + lines.add(elseIfMatcher.group()); + } + + for (String elseIfLine : lines) { + Matcher conditionMatcher = CONDITION.matcher(elseIfLine); + + if (conditionMatcher.find()) { + String condition = conditionMatcher.group(); + String newElseIf = "} else if " + condition + "{"; + text = text.replace(elseIfLine, newElseIf); + } + } + + lines.clear(); + + Matcher formatMatcher = FORMAT_LINE.matcher(text); + + while (formatMatcher.find()) { + lines.add(formatMatcher.group()); + } + + for (String formatLine : lines) { + String newFormat = " {"; + text = text.replace(formatLine, newFormat); + } + + lines.clear(); + */ + + // If this is anonymous subclass, give it some space + // text = text.replace("};", "};" + System.lineSeparator()); + + // If we have empty {} block, beautify it + // text = text.replace("{}", "{" + System.lineSeparator() + " System.out.println(\"NO-OP\");" + System.lineSeparator() + "}"); + + // Make it twice to be sure + for (int i = 0; i < 2; i++) { + reader = new StringReader(text); + writer = new StringWriter(); + formatter.format(reader, writer); + reader.close(); + writer.flush(); + writer.close(); + text = writer.toString(); + } + + //text = text.replace("System.out.println(\"NO-OP\");", "// NO-OP"); + + List textLines = Lists.newArrayList(text.split("\\r?\\n")); + textLines.removeIf(string -> string.contains("private static final String __OBFID")); + text = textLines.stream().collect(Collectors.joining(System.lineSeparator())); + + this.sourceMap.put(file, text); + } + } + + private void saveJar(File output) throws IOException { + ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output)); + + // write in sources + for (Map.Entry entry : this.sourceMap.entrySet()) { + zout.putNextEntry(new ZipEntry(entry.getKey())); + zout.write(entry.getValue().getBytes()); + zout.closeEntry(); + } + + zout.close(); + } + + public HashMap getSourceMap() { + return this.sourceMap; + } + + public void setSourceMap(HashMap sourceMap) { + this.sourceMap = sourceMap; + } + + public File getAstyleConfig() { + return this.astyleConfig.call(); + } + + public void setAstyleConfig(DelayedFile astyleConfig) { + this.astyleConfig = astyleConfig; + } + + public File getInJar() { + return this.inJar.call(); + } + + public void setInJar(DelayedFile inJar) { + this.inJar = inJar; + } + + public File getOutJar() { + return this.outJar.call(); + } + + public void setOutJar(DelayedFile outJar) { + this.outJar = outJar; + } + +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/UncodeJarTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/UncodeJarTask.java new file mode 100644 index 00000000..5eebd551 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/UncodeJarTask.java @@ -0,0 +1,27 @@ +package io.github.cruciblemc.forgegradle.tasks; + +import com.juanmuscaria.uncode.ASMCodeRemover; +import lombok.Getter; +import lombok.Setter; +import net.minecraftforge.gradle.delayed.DelayedFile; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import java.io.IOException; + +@Getter +@Setter +public class UncodeJarTask extends DefaultTask { + @InputFile + private DelayedFile inputJar; + + @OutputFile + private DelayedFile outJar; + + @TaskAction + public void doTask() throws IOException { + ASMCodeRemover.removeContent(getInputJar().call().toPath(), getOutJar().call().toPath(), true); + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GenDevProjectsTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GenDevProjectsTask.java new file mode 100644 index 00000000..535e5c37 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GenDevProjectsTask.java @@ -0,0 +1,334 @@ +package io.github.cruciblemc.forgegradle.tasks.dev; + +import com.google.common.io.Files; +import groovy.lang.Closure; +import io.github.cruciblemc.forgegradle.DevExtension; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.delayed.DelayedString; +import net.minecraftforge.gradle.json.JsonFactory; +import net.minecraftforge.gradle.json.version.Library; +import net.minecraftforge.gradle.json.version.Version; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.*; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static net.minecraftforge.gradle.common.Constants.EXT_NAME_MC; +import static net.minecraftforge.gradle.common.Constants.NEWLINE; + +public class GenDevProjectsTask extends DefaultTask { + private static final String TEMPLATE = """ + apply plugin: 'java-library' + apply plugin: 'eclipse' + + repositories { + mavenCentral() + %s + } + + dependencies { + %s + } + + jar { + exclude 'GradleStart*', 'net/minecraftforge/gradle/**' + } + + // source set definition + %s + + processResources { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + + def links = [] + def dupes = [] + eclipse.project.file.withXml { provider -> + def node = provider.asNode() + links = [] + dupes = [] + node.linkedResources.link.each { child -> + def path = child.location.text() + if (path in dupes) { + child.replaceNode {} + } else { + dupes.add(path) + def newName = path.split('/')[-2..-1].join('/') + links += newName + child.replaceNode { + link{ + name(newName) + type('2') + location(path) + } + } + } + } + } + + eclipse.classpath.file.withXml { + def node = it.asNode() + node.classpathentry.each { child -> + if (child.@kind == 'src' && !child.@path.contains('/')) child.replaceNode {} + if (child.@path in links) links.remove(child.@path) + } + links.each { link -> node.appendNode('classpathentry', [kind:'src', path:link]) } + } + tasks.eclipseClasspath.dependsOn 'eclipseProject' //Make them run in correct order" + """; + @Internal + protected DelayedFile targetDir; + + @InputFile + protected DelayedFile json; + + @Input + @Optional + protected String[] repos = new String[0]; + + @Input + protected boolean useLibrariesConfiguration = false; + + @Input + @Optional + private DelayedString mappingChannel, mappingVersion, mcVersion; + + private final List sources = new ArrayList<>(); + private final List resources = new ArrayList<>(); + private final List testSources = new ArrayList<>(); + private final List testResources = new ArrayList<>(); + + private final ArrayList deps = new ArrayList<>(); + + public GenDevProjectsTask() { + this.getOutputs().file(getTargetFile()); + } + + @TaskAction + public void doTask() throws IOException { + parseJson(); + writeFile(); + } + + private void parseJson() throws IOException { + Version version = JsonFactory.loadVersion(getJson(), getJson().getParentFile()); + + for (Library lib : version.getLibraries()) { + if (lib.name.contains("fixed") || lib.natives != null || lib.extract != null) { + continue; + } else { + deps.add(lib.getArtifactName()); + } + } + } + + private void writeFile() throws IOException { + File file = getProject().file(getTargetFile().call()); + file.getParentFile().mkdirs(); + Files.touch(file); + + StringBuilder o = new StringBuilder(); + + for (String repo : getRepos()) { + a(o, " maven {", + " url = '" + repo + "'", + " }" + ); + } + for (String repo : ((DevExtension) getProject().getExtensions().getByName(EXT_NAME_MC)).getRepos()) { + a(o, " maven {", + " url = '" + repo + "'", + " }" + ); + } + var repos = o.toString(); + o.setLength(0); + + if (useLibrariesConfiguration) { + var libraryOverwrite = getProject().getConfigurations().getByName("libraries").getDependencies() + .stream().map(dep -> dep.getGroup() + ':' + dep.getName() + ':' + dep.getVersion()) + .collect(Collectors.toList()); + + o.append(NEWLINE).append(" // Library overwrites").append(NEWLINE); + for (String dep : libraryOverwrite) { + o.append(" api '").append(dep).append('\'').append(NEWLINE); + } + + o.append(" // MC libraries").append(NEWLINE); + for (String dep : deps) { + o.append(" api '").append(dep).append('\'').append(NEWLINE); + } + } else { + for (String dep : deps) { + o.append(" api '").append(dep).append('\'').append(NEWLINE); + } + } + + String channel = getMappingChannel(); + String version = getMappingVersion(); + String mcversion = getMcVersion(); + if (version != null && channel != null) { + o.append(" api group: 'de.oceanlabs.mcp', name:'mcp_").append(channel).append("', version:'").append(version).append('-').append(mcversion).append("', ext:'zip'"); + } + o.append(" testImplementation 'junit:junit:4.5'"); + var deps = o.toString(); + o.setLength(0); + + URI base = targetDir.call().toURI(); + + if (resources.size() > 0 || sources.size() > 0 || testSources.size() > 0 || testResources.size() > 0) { + a(o, "sourceSets {"); + a(o, " main {"); + if (sources.size() > 0) { + a(o, " java {"); + for (DelayedFile src : sources) { + o.append(" srcDir '").append(relative(base, src)).append('\'').append(NEWLINE); + } + a(o, " }"); + } + if (resources.size() > 0) { + a(o, " resources {"); + for (DelayedFile src : resources) { + o.append(" srcDir '").append(relative(base, src)).append('\'').append(NEWLINE); + } + a(o, " }"); + } + a(o, " }"); + a(o, " test {"); + if (testSources.size() > 0) { + a(o, " java {"); + + for (DelayedFile src : testSources) { + o.append(" srcDir '").append(relative(base, src)).append('\'').append(NEWLINE); + } + a(o, " }"); + } + if (testResources.size() > 0) { + a(o, " resources {"); + for (DelayedFile src : testResources) { + o.append(" srcDir '").append(relative(base, src)).append('\'').append(NEWLINE); + } + a(o, " }"); + } + a(o, " }"); + a(o, "}"); + } + var srcSet = o.toString(); + o.setLength(0); + + String buildFile = String.format(TEMPLATE, repos, deps, srcSet); + Files.asCharSink(file, Charset.defaultCharset()).write(buildFile); + } + + private String relative(URI base, DelayedFile src) { + String relative = base.relativize(src.call().toURI()).getPath().replace('\\', '/'); + if (!relative.endsWith("/")) relative += "/"; + return relative; + } + + private void a(StringBuilder out, String... lines) { + for (String line : lines) { + out.append(line).append(NEWLINE); + } + } + + private Closure getTargetFile() { + return new Closure(this) { + private static final long serialVersionUID = -6333350974905684295L; + + @Override + public File call() { + return new File(getTargetDir(), "build.gradle"); + } + + @Override + public File call(Object obj) { + return new File(getTargetDir(), "build.gradle"); + } + }; + } + + public File getTargetDir() { + return targetDir.call(); + } + + public void setTargetDir(DelayedFile targetDir) { + this.targetDir = targetDir; + } + + public GenDevProjectsTask addSource(DelayedFile source) { + sources.add(source); + return this; + } + + public GenDevProjectsTask addResource(DelayedFile resource) { + resources.add(resource); + return this; + } + + public GenDevProjectsTask addTestSource(DelayedFile source) { + testSources.add(source); + return this; + } + + public GenDevProjectsTask addTestResource(DelayedFile resource) { + testResources.add(resource); + return this; + } + + public File getJson() { + return json.call(); + } + + public void setJson(DelayedFile json) { + this.json = json; + } + + public String getMappingChannel() { + String channel = mappingChannel.call(); + return channel.equals("{MAPPING_CHANNEL}") ? null : channel; + } + + public void setMappingChannel(DelayedString mChannel) { + this.mappingChannel = mChannel; + } + + public String getMappingVersion() { + String version = mappingVersion.call(); + return version.equals("{MAPPING_VERSION}") ? null : version; + } + + public void setMappingVersion(DelayedString mappingVersion) { + this.mappingVersion = mappingVersion; + } + + public String getMcVersion() { + return mcVersion.call(); + } + + public void setMcVersion(DelayedString mcVersion) { + this.mcVersion = mcVersion; + } + + public void setRepos(String[] repos) { + this.repos = repos; + } + + public String[] getRepos() { + return repos; + } + + public boolean isUseLibrariesConfiguration() { + return useLibrariesConfiguration; + } + + public void setUseLibrariesConfiguration(boolean useLibrariesConfiguration) { + this.useLibrariesConfiguration = useLibrariesConfiguration; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GeneratePatches.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GeneratePatches.java new file mode 100644 index 00000000..2870f20a --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/GeneratePatches.java @@ -0,0 +1,208 @@ +package io.github.cruciblemc.forgegradle.tasks.dev; + +import com.cloudbees.diff.Diff; +import com.cloudbees.diff.Hunk; +import com.cloudbees.diff.PatchException; +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.srg2source.util.io.FolderSupplier; +import net.minecraftforge.srg2source.util.io.InputSupplier; +import net.minecraftforge.srg2source.util.io.ZipInputSupplier; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.file.FileVisitDetails; +import org.gradle.api.file.FileVisitor; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.util.*; + +public class GeneratePatches extends DefaultTask { + @OutputDirectory + DelayedFile patchDir; + + @InputFiles + DelayedFile changed; + + @InputFiles + DelayedFile original; + + @Input + String originalPrefix = ""; + + @Input + String changedPrefix = ""; + + private final Set created = new HashSet(); + + @TaskAction + public void doTask() throws IOException, PatchException { + created.clear(); + getPatchDir().mkdirs(); + + // fix and create patches. + processFiles(getSupplier(original.call()), getSupplier(changed.call())); + + removeOld(getPatchDir()); + } + + private InputSupplier getSupplier(File file) throws IOException { + if (file.isDirectory()) + return new FolderSupplier(file); + + ZipInputSupplier ret = new ZipInputSupplier(); + ret.readZip(file); + return ret; + } + + private void removeOld(File dir) throws IOException { + final ArrayList directories = new ArrayList(); + FileTree tree = getProject().fileTree(dir); + + tree.visit(new FileVisitor() { + @Override + public void visitDir(FileVisitDetails dir) { + directories.add(dir.getFile()); + } + + @Override + public void visitFile(FileVisitDetails f) { + File file = f.getFile(); + if (!created.contains(file)) { + getLogger().debug("Removed patch: " + f.getRelativePath()); + file.delete(); + } + } + }); + + // We want things sorted in reverse order. Do that sub folders come before parents + Collections.sort(directories, new Comparator() { + @Override + public int compare(File o1, File o2) { + int r = o1.compareTo(o2); + if (r < 0) return 1; + if (r > 0) return -1; + return 0; + } + }); + + for (File f : directories) { + if (f.listFiles().length == 0) { + getLogger().debug("Removing empty dir: " + f); + f.delete(); + } + } + } + + public void processFiles(InputSupplier original, InputSupplier changed) throws IOException { + List paths = original.gatherAll(""); + for (String path : paths) { + path = path.replace('\\', '/'); + InputStream o = original.getInput(path); + InputStream c = changed.getInput(path); + try { + processFile(path, o, c); + } finally { + if (o != null) o.close(); + if (c != null) c.close(); + } + } + } + + public void processFile(String relative, InputStream original, InputStream changed) throws IOException { + getLogger().debug("Diffing: " + relative); + + File patchFile = new File(getPatchDir(), relative + ".patch"); + + if (changed == null) { + getLogger().debug(" Changed File does not exist"); + return; + } + + // We have to cache the bytes because diff reads the stream twice.. why.. who knows. + byte[] oData = ByteStreams.toByteArray(original); + byte[] cData = ByteStreams.toByteArray(changed); + + Diff diff = Diff.diff(new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8), new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), false); + + if (!relative.startsWith("/")) + relative = "/" + relative; + + if (!diff.isEmpty()) { + String unidiff = diff.toUnifiedDiff(originalPrefix + relative, changedPrefix + relative, + new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8), + new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), 3); + unidiff = unidiff.replace("\r\n", "\n"); //Normalize lines + unidiff = unidiff.replace("\n" + Hunk.ENDING_NEWLINE + "\n", "\n"); //We give 0 shits about this. + + String olddiff = ""; + if (patchFile.exists()) { + olddiff = Files.toString(patchFile, Charsets.UTF_8); + } + + if (!olddiff.equals(unidiff)) { + getLogger().debug("Writing patch: " + patchFile); + patchFile.getParentFile().mkdirs(); + Files.touch(patchFile); + Files.write(unidiff, patchFile, Charsets.UTF_8); + } else { + getLogger().debug("Patch did not change"); + } + created.add(patchFile); + } + } + + public FileCollection getChanged() { + File f = changed.call(); + if (f.isDirectory()) + return getProject().fileTree(f); + else + return getProject().files(f); + } + + public void setChanged(DelayedFile changed) { + this.changed = changed; + } + + public FileCollection getOriginal() { + File f = original.call(); + if (f.isDirectory()) + return getProject().fileTree(f); + else + return getProject().files(f); + } + + public void setOriginal(DelayedFile original) { + this.original = original; + } + + public File getPatchDir() { + return patchDir.call(); + } + + public void setPatchDir(DelayedFile patchDir) { + this.patchDir = patchDir; + } + + public String getOriginalPrefix() { + return originalPrefix; + } + + public void setOriginalPrefix(String originalPrefix) { + this.originalPrefix = originalPrefix; + } + + public String getChangedPrefix() { + return changedPrefix; + } + + public void setChangedPrefix(String changedPrefix) { + this.changedPrefix = changedPrefix; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/ObfuscateTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/ObfuscateTask.java new file mode 100644 index 00000000..7a2c2ee3 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/ObfuscateTask.java @@ -0,0 +1,234 @@ +package io.github.cruciblemc.forgegradle.tasks.dev; + +import com.google.common.io.Files; +import io.github.cruciblemc.forgegradle.reobf.JarRemapperWrapper; +import net.md_5.specialsource.Jar; +import net.md_5.specialsource.JarMapping; +import net.md_5.specialsource.JarRemapper; +import net.md_5.specialsource.provider.ClassLoaderProvider; +import net.md_5.specialsource.provider.JarProvider; +import net.md_5.specialsource.provider.JointProvider; +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.extrastuff.ReobfExceptor; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.LinkedList; + +public class ObfuscateTask extends DefaultTask { + @Internal + private DelayedFile outJar; + @Internal + private DelayedFile preFFJar; + @Internal + private DelayedFile srg; + @Internal + private DelayedFile exc; + @Internal + private boolean reverse; + @Internal + private String projectName; + private final LinkedList> configureProject = new LinkedList>(); + @Internal + private DelayedFile methodsCsv; + @Internal + private DelayedFile fieldsCsv; + @Internal + private String subTask = "jar"; + @Internal + private LinkedList extraSrg = new LinkedList(); + + @TaskAction + public void doTask() throws IOException { + getLogger().debug("Building child project model..."); + Project childProj = getProject().project(projectName); + for (Action act : configureProject) { + if (act != null) + act.execute(childProj); + } + + DefaultTask compileTask = (DefaultTask) childProj.getTasks().getByName("compileJava"); + + File inJar = new File(childProj.getBuildDir(), "libs" + File.separator + "cauldron.jar"); + + File srg = getSrg(); + + if (getExc() != null) { + ReobfExceptor exceptor = new ReobfExceptor(); + exceptor.toReobfJar = inJar; + exceptor.deobfJar = getPreFFJar(); + exceptor.excConfig = getExc(); + exceptor.fieldCSV = getFieldsCsv(); + exceptor.methodCSV = getMethodsCsv(); + + File outSrg = new File(this.getTemporaryDir(), "reobf_cls.srg"); + + exceptor.doFirstThings(); + exceptor.buildSrg(srg, outSrg); + + srg = outSrg; + } + + // append SRG + BufferedWriter writer = new BufferedWriter(new FileWriter(srg, true)); + for (String line : extraSrg) { + writer.write(line); + writer.newLine(); + } + writer.flush(); + writer.close(); + + getLogger().debug("Obfuscating jar..."); + obfuscate(inJar, (FileCollection) compileTask.property("classpath"), srg); + } + + private void executeTask(final DefaultTask task) { + for (Object dep : task.getTaskDependencies().getDependencies(task)) { + executeTask((DefaultTask) dep); + } + if (!task.getState().getExecuted()) { + getLogger().lifecycle(task.getPath()); + for (Action action : task.getActions()) { + action.execute(task); + } + } + } + + private void obfuscate(File inJar, FileCollection classpath, File srg) throws IOException { + // load mapping + JarMapping mapping = new JarMapping(); + mapping.loadMappings(Files.newReader(srg, Charset.defaultCharset()), null, null, reverse); + + // make remapper + JarRemapper remapper = new JarRemapperWrapper(null, mapping); + + // load jar + Jar input = Jar.init(inJar); + + // ensure that inheritance provider is used + JointProvider inheritanceProviders = new JointProvider(); + inheritanceProviders.add(new JarProvider(input)); + + if (classpath != null) + inheritanceProviders.add(new ClassLoaderProvider(new URLClassLoader(toUrls(classpath)))); + + mapping.setFallbackInheritanceProvider(inheritanceProviders); + + File out = getOutJar(); + if (!out.getParentFile().exists()) //Needed because SS doesn't create it. + { + out.getParentFile().mkdirs(); + } + + // remap jar + remapper.remapJar(input, getOutJar()); + } + + public static URL[] toUrls(FileCollection collection) throws MalformedURLException { + ArrayList urls = new ArrayList(); + + for (File file : collection.getFiles()) + urls.add(file.toURI().toURL()); + + return urls.toArray(new URL[urls.size()]); + } + + public File getOutJar() { + return outJar.call(); + } + + public void setOutJar(DelayedFile outJar) { + this.outJar = outJar; + } + + public File getPreFFJar() { + return preFFJar.call(); + } + + public void setPreFFJar(DelayedFile preFFJar) { + this.preFFJar = preFFJar; + } + + public File getSrg() { + return srg.call(); + } + + public void setSrg(DelayedFile srg) { + this.srg = srg; + } + + public File getExc() { + return exc.call(); + } + + public void setExc(DelayedFile exc) { + this.exc = exc; + } + + public boolean isReverse() { + return reverse; + } + + public void setReverse(boolean reverse) { + this.reverse = reverse; + } + + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + + public File getMethodsCsv() { + return methodsCsv.call(); + } + + public void setMethodsCsv(DelayedFile methodsCsv) { + this.methodsCsv = methodsCsv; + } + + public File getFieldsCsv() { + return fieldsCsv.call(); + } + + public void setFieldsCsv(DelayedFile fieldsCsv) { + this.fieldsCsv = fieldsCsv; + } + + public void configureProject(Action action) { + configureProject.add(action); + } + + public LinkedList getExtraSrg() { + return extraSrg; + } + + public void setExtraSrg(LinkedList extraSrg) { + this.extraSrg = extraSrg; + } + + public String getSubTask() { + return subTask; + } + + public void setSubTask(String subTask) { + this.subTask = subTask; + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/SubprojectTask.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/SubprojectTask.java new file mode 100644 index 00000000..0c07f160 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/dev/SubprojectTask.java @@ -0,0 +1,84 @@ +package io.github.cruciblemc.forgegradle.tasks.dev; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; + +import java.util.LinkedList; +import java.util.Set; + +public class SubprojectTask extends DefaultTask { + @Internal + private String projectName; + @Internal + private String tasks; + private final LinkedList> configureProject = new LinkedList<>(); + @Internal + private Action configureTask; + + @TaskAction + public void doTask() { + Project childProj = getProject().project(projectName); + + // configure the project + for (Action act : configureProject) { + if (act != null) + act.execute(childProj); + } + + for (String task : tasks.split(" ")) { + Set list = childProj.getTasksByName(task, false); + for (Task t : list) { + if (configureTask != null) + configureTask.execute(t); + executeTask((DefaultTask) t); + } + } + + System.gc(); + } + + private void executeTask(final DefaultTask task) { + for (Object dep : task.getTaskDependencies().getDependencies(task)) { + executeTask((DefaultTask) dep); + } + + if (!task.getState().getExecuted()) { + getLogger().lifecycle(task.getPath()); + for (Action action : task.getActions()) { + action.execute(task); + } + } + } + + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public String getTasks() { + return tasks; + } + + public void setTasks(String tasks) { + this.tasks = tasks; + } + + public Action getConfigureTask() { + return configureTask; + } + + public void setConfigureTask(Action configureTask) { + this.configureTask = configureTask; + } + + public void configureProject(Action action) { + configureProject.add(action); + } +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/ClassCollection.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/ClassCollection.java new file mode 100644 index 00000000..20df9960 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/ClassCollection.java @@ -0,0 +1,56 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import org.objectweb.asm.tree.ClassNode; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.jar.Manifest; + +class ClassCollection { + private final List classes; + private final Manifest manifest; + private final Map extraFiles; + + public ClassCollection(List classes, Manifest manifest, Map extraFiles) { + this.classes = classes; + this.manifest = manifest; + this.extraFiles = extraFiles; + } + + public List getClasses() { + return this.classes; + } + + public Manifest getManifest() { + return this.manifest; + } + + public Map getExtraFiles() { + return this.extraFiles; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + ClassCollection that = (ClassCollection) o; + + if (!Objects.equals(this.classes, that.classes)) + return false; + if (!Objects.equals(this.extraFiles, that.extraFiles)) + return false; + return Objects.equals(this.manifest, that.manifest); + } + + @Override + public int hashCode() { + int result = this.classes != null ? this.classes.hashCode() : 0; + result = 31 * result + (this.manifest != null ? this.manifest.hashCode() : 0); + result = 31 * result + (this.extraFiles != null ? this.extraFiles.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/FixedJarInputStream.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/FixedJarInputStream.java new file mode 100644 index 00000000..1f7aa64e --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/FixedJarInputStream.java @@ -0,0 +1,36 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +/** + * Fixes bug with JRE that expects the Manifest to always be the first entry in META-INF, when in some cases, it isn't.
+ * Credit to clienthax for finding this: http://bugs.java.com/view_bug.do?bug_id=4338238 + */ + +class FixedJarInputStream extends JarInputStream { + private Manifest manifest; + + public FixedJarInputStream(File file, boolean verify) throws IOException { + super(new FileInputStream(file), verify); + JarFile jar = new JarFile(file); + JarEntry manifestEntry = jar.getJarEntry(JarFile.MANIFEST_NAME); + try { + if (manifestEntry != null) { + this.manifest = new Manifest(jar.getInputStream(manifestEntry)); + } + } finally { + jar.close(); + } + } + + @Override + public Manifest getManifest() { + return this.manifest; + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/IOUtils.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/IOUtils.java new file mode 100644 index 00000000..74c246bc --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/IOUtils.java @@ -0,0 +1,41 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +class IOUtils { + + public static byte[] readStreamFully(InputStream is) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(8192, is.available())); + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) >= 0) { + baos.write(buffer, 0, read); + } + return baos.toByteArray(); + } + + public static ClassNode readClassFromBytes(byte[] bytes) { + ClassNode classNode = new ClassNode(); + ClassReader classReader = new ClassReader(bytes); + classReader.accept(classNode, 0); + return classNode; + } + + public static byte[] writeClassToBytes(ClassNode classNode) { + ClassWriter classWriter = new ClassWriter(0); + classNode.accept(classWriter); + return classWriter.toByteArray(); + } + + public static int getSecondToLastIndexOf(String string, char character) { + String temp = string.substring(0, string.lastIndexOf(character)); + return temp.lastIndexOf(character); + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/JarUtils.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/JarUtils.java new file mode 100644 index 00000000..eec0273f --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/JarUtils.java @@ -0,0 +1,129 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import org.objectweb.asm.tree.ClassNode; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.jar.*; + +class JarUtils { + + public static ClassCollection readFromJar(File file) throws IOException { + List classes = new ArrayList<>(); + Map extraFiles = new HashMap<>(); + Manifest manifest = null; + FixedJarInputStream jin = null; + long fileSize = file.length(); + long currentProgress = 0; + System.out.println("Loading Input JAR: " + file); + try { + jin = new FixedJarInputStream(file, false); + JarEntry entry; + while ((entry = jin.getNextJarEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String name = entry.getName(); + if (name.endsWith(".class")) { + byte[] bytes = IOUtils.readStreamFully(jin); + if (bytes.length > 0) { + ClassNode cn = null; + try { + cn = IOUtils.readClassFromBytes(bytes); + + if (!name.equals(cn.name + ".class")) { + System.err.println("There was an error in reading a class. Corrupted JAR maybe?\n" + name + " != " + cn.name + ".class"); + } else { + classes.add(cn); + } + } catch (Exception e) { + System.err.println("There was an unexpected error while reading class data. Corrupted JAR maybe?\n" + name); + } + } else { + System.err.println("Found a class with no content. Corrupted JAR maybe?\nClass was:" + name + "\nThe class will be skipped."); + } + } else { + String upperCaseName = name.toUpperCase(Locale.ROOT); + // Skip MANIFEST, since it's handled specially, and any signature files as they will be invalid after modifying binaries + if (!upperCaseName.startsWith("META-INF/") || (!upperCaseName.endsWith("MANIFEST.MF") && !upperCaseName.endsWith(".SF") && !upperCaseName.endsWith(".RSA"))) { + extraFiles.put(name, IOUtils.readStreamFully(jin)); + } + } + } + manifest = stripManifest(jin.getManifest()); + } finally { + if (jin != null) { + jin.close(); + } + } + return new ClassCollection(classes, manifest, extraFiles); + } + + public static void writeToJar(ClassCollection cc, File file) throws IOException { + if (file.exists()) { + file.delete(); + } + int classesWritten = 0; + Set dirs = new HashSet<>(); + JarOutputStream jout = null; + + try { + jout = new JarOutputStream(new FileOutputStream(file)); + addDirectories(JarFile.MANIFEST_NAME, dirs); + jout.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); + Manifest manifest = cc.getManifest(); + if (manifest != null) { + manifest.write(jout); + } + jout.closeEntry(); + + for (ClassNode classNode : cc.getClasses()) { + addDirectories(classNode.name, dirs); + jout.putNextEntry(new JarEntry(classNode.name + ".class")); + jout.write(IOUtils.writeClassToBytes(classNode)); + jout.closeEntry(); + } + for (Map.Entry entry : cc.getExtraFiles().entrySet()) { + addDirectories(entry.getKey(), dirs); + jout.putNextEntry(new JarEntry(entry.getKey())); + jout.write(entry.getValue()); + jout.closeEntry(); + } + for (String dirPath : dirs) { + jout.putNextEntry(new JarEntry(dirPath + "/")); + jout.closeEntry(); + } + jout.flush(); + } finally { + if (jout != null) { + jout.close(); + } + } + + if (jout != null) { + jout.close(); + } + } + + private static void addDirectories(String filePath, Set dirs) { + int i = filePath.lastIndexOf('/'); + if (i >= 0) { + String dirPath = filePath.substring(0, i); + if (dirs.add(dirPath)) { + addDirectories(dirPath, dirs); + } + } + } + + private static Manifest stripManifest(Manifest manifestIn) { + if (manifestIn == null) + return manifestIn; + Manifest manifestOut = new Manifest(manifestIn); + for (Map.Entry entry : manifestIn.getEntries().entrySet()) { + manifestOut.getEntries().remove(entry.getKey()); + } + return manifestOut; + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/RestoreGenericSignatures.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/RestoreGenericSignatures.java new file mode 100644 index 00000000..a1c71086 --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/RestoreGenericSignatures.java @@ -0,0 +1,115 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import net.minecraftforge.gradle.delayed.DelayedFile; +import net.minecraftforge.gradle.tasks.abstractutil.CachedTask; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Map; + +public class RestoreGenericSignatures extends CachedTask { + private static final Map SIGNATURE_MAP = new HashMap<>(); + + @InputFile + private DelayedFile inJar; + + @Cached + @OutputFile + public DelayedFile outJar; + + public RestoreGenericSignatures() { + this.getInputs().property("signatureMap", SIGNATURE_MAP); + } + + @TaskAction + public void restore() throws Throwable { + File input = this.inJar.call().getCanonicalFile(); + + if (!input.exists()) + throw new FileNotFoundException("Could not locate file " + input); + + ClassCollection collection = JarUtils.readFromJar(input); + + for (ClassNode node : collection.getClasses()) { + if (node.fields != null) { + for (FieldNode field : node.fields) { + String signature = SIGNATURE_MAP.get(field.name); + + if (signature != null) { + field.signature = signature; + } + } + } + + if (node.methods != null) { + for (MethodNode method : node.methods) { + String signature = SIGNATURE_MAP.get(method.name); + + if (signature != null) { + method.signature = signature; + } + } + } + } + + File output = this.outJar.call().getCanonicalFile(); + + if (output.exists()) { + output.delete(); + } + + JarUtils.writeToJar(collection, output); + } + + public File getInJar() { + return this.inJar.call(); + } + + public void setInJar(DelayedFile inJar) { + this.inJar = inJar; + } + + public File getOutJar() { + return this.outJar.call(); + } + + public void setOutJar(DelayedFile outJar) { + this.outJar = outJar; + } + + public static Map getSignatureMap() { + return SIGNATURE_MAP; + } + + static { + // buttonList + SIGNATURE_MAP.put("field_146292_n", "Ljava/util/List;"); + // labelList + SIGNATURE_MAP.put("field_146293_o", "Ljava/util/List;"); + // playerEntityList + SIGNATURE_MAP.put("field_72404_b", "Ljava/util/List;"); + // stringToClassMapping + SIGNATURE_MAP.put("field_75625_b", "Ljava/util/Map;>;"); + // classToStringMapping + SIGNATURE_MAP.put("field_75626_c", "Ljava/util/Map;Ljava/lang/String;>;"); + // IDtoClassMapping + SIGNATURE_MAP.put("field_75623_d", "Ljava/util/Map;>;"); + // classToIDMapping + SIGNATURE_MAP.put("field_75624_e", "Ljava/util/Map;Ljava/lang/Integer;>;"); + // stringToIDMapping + SIGNATURE_MAP.put("field_75622_f", "Ljava/util/Map;"); + // entityEggs + SIGNATURE_MAP.put("field_75627_a", "Ljava/util/HashMap;"); + + // addInformation + SIGNATURE_MAP.put("func_77624_a", "(Lnet/minecraft/item/ItemStack;Lnet/minecraft/entity/player/EntityPlayer;Ljava/util/List;Z)V"); + } + +} diff --git a/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/SafeClassWriter.java b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/SafeClassWriter.java new file mode 100644 index 00000000..8f494f4e --- /dev/null +++ b/buildSrc/src/main/java/io/github/cruciblemc/forgegradle/tasks/signum/SafeClassWriter.java @@ -0,0 +1,143 @@ +package io.github.cruciblemc.forgegradle.tasks.signum; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A ClassWriter that computes the common super class of two classes without + * actually loading them with a ClassLoader. + * + * @author Eric Bruneton + */ + +class SafeClassWriter extends ClassWriter { + private final ClassLoader loader; + + public SafeClassWriter(ClassLoader loader, final int flags) { + super(flags); + this.loader = loader != null ? loader : ClassLoader.getSystemClassLoader(); + } + + public SafeClassWriter(ClassReader cr, ClassLoader loader, final int flags) { + super(cr, flags); + this.loader = loader != null ? loader : ClassLoader.getSystemClassLoader(); + } + + @Override + protected String getCommonSuperClass(final String type1, final String type2) { + try { + ClassReader info1 = this.typeInfo(type1); + ClassReader info2 = this.typeInfo(type2); + if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (this.typeImplements(type2, info2, type1)) + return type1; + else + return "java/lang/Object"; + } + if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (this.typeImplements(type1, info1, type2)) + return type2; + else + return "java/lang/Object"; + } + StringBuilder b1 = this.typeAncestors(type1, info1); + StringBuilder b2 = this.typeAncestors(type2, info2); + String result = "java/lang/Object"; + int end1 = b1.length(); + int end2 = b2.length(); + while (true) { + int start1 = b1.lastIndexOf(";", end1 - 1); + int start2 = b2.lastIndexOf(";", end2 - 1); + if (start1 != -1 && start2 != -1 + && end1 - start1 == end2 - start2) { + String p1 = b1.substring(start1 + 1, end1); + String p2 = b2.substring(start2 + 1, end2); + if (p1.equals(p2)) { + result = p1; + end1 = start1; + end2 = start2; + } else + return result; + } else + return result; + } + } catch (IOException e) { + throw new RuntimeException(e.toString()); + } + } + + /** + * Returns the internal names of the ancestor classes of the given type. + * + * @param type the internal name of a class or interface. + * @param info the ClassReader corresponding to 'type'. + * @return a StringBuilder containing the ancestor classes of 'type', + * separated by ';'. The returned string has the following format: + * ";type1;type2 ... ;typeN", where type1 is 'type', and typeN is a + * direct subclass of Object. If 'type' is Object, the returned + * string is empty. + * @throws IOException if the bytecode of 'type' or of some of its ancestor class + * cannot be loaded. + */ + private StringBuilder typeAncestors(String type, ClassReader info) + throws IOException { + StringBuilder b = new StringBuilder(); + while (!"java/lang/Object".equals(type)) { + b.append(';').append(type); + type = info.getSuperName(); + info = this.typeInfo(type); + } + return b; + } + + /** + * Returns true if the given type implements the given interface. + * + * @param type the internal name of a class or interface. + * @param info the ClassReader corresponding to 'type'. + * @param itf the internal name of a interface. + * @return true if 'type' implements directly or indirectly 'itf' + * @throws IOException if the bytecode of 'type' or of some of its ancestor class + * cannot be loaded. + */ + private boolean typeImplements(String type, ClassReader info, String itf) + throws IOException { + while (!"java/lang/Object".equals(type)) { + String[] itfs = info.getInterfaces(); + for (String itf2 : itfs) { + if (itf2.equals(itf)) + return true; + } + for (String itf2 : itfs) { + if (this.typeImplements(itf2, this.typeInfo(itf2), itf)) + return true; + } + type = info.getSuperName(); + info = this.typeInfo(type); + } + return false; + } + + /** + * Returns a ClassReader corresponding to the given class or interface. + * + * @param type the internal name of a class or interface. + * @return the ClassReader corresponding to 'type'. + * @throws IOException if the bytecode of 'type' cannot be loaded. + */ + private ClassReader typeInfo(final String type) throws IOException { + String resource = type + ".class"; + InputStream is = this.loader.getResourceAsStream(resource); + if (is == null) + throw new IOException("Cannot create ClassReader for type " + type); + try { + return new ClassReader(is); + } finally { + is.close(); + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/crucible/crucible-patches.zip b/buildSrc/src/main/resources/crucible/crucible-patches.zip new file mode 100644 index 00000000..c8489b76 Binary files /dev/null and b/buildSrc/src/main/resources/crucible/crucible-patches.zip differ diff --git a/buildSrc/src/main/resources/crucible/forgeflower.jar b/buildSrc/src/main/resources/crucible/forgeflower.jar new file mode 100644 index 00000000..ecec1a92 Binary files /dev/null and b/buildSrc/src/main/resources/crucible/forgeflower.jar differ