diff --git a/build.gradle b/build.gradle index b2e2dcc..ff7317e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,10 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform plugins { id 'application' - //id 'org.jetbrains.gradle.upx' version '1.6.0-RC.5' - id 'de.undercouch.download' version '5.4.0' id 'com.github.johnrengelman.shadow' version '8.1.1' id 'io.micronaut.application' version '4.2.1' + id 'com.juanmuscaria.tooling.dmm' /* because sometimes we need to make our own tools */ + id 'de.undercouch.download' /* version set in the plugin above */ } ext.gitInfoCached = null diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..164f8d5 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +dependencies { + annotationProcessor("org.projectlombok:lombok:1.18.28") + implementation("de.undercouch:gradle-download-task:5.6.0") + implementation("org.tukaani:xz:1.9") + implementation("commons-io:commons-io:2.16.1") +} + + +gradlePlugin { + plugins { + toolingPlugin { + id = "com.juanmuscaria.tooling.dmm" + implementationClass = "com.juanmuscaria.tooling.dmm.DmmPlugin" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/DmmPlugin.groovy b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/DmmPlugin.groovy new file mode 100644 index 0000000..909e146 --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/DmmPlugin.groovy @@ -0,0 +1,87 @@ +package com.juanmuscaria.tooling.dmm + +import com.juanmuscaria.tooling.dmm.file.XZArchiver +import com.juanmuscaria.tooling.dmm.task.UpxTask +import de.undercouch.gradle.tasks.download.Download +import org.gradle.api.Named +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.RelativePath +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.resources.ReadableResource +import org.gradle.api.tasks.Sync +import org.gradle.internal.os.OperatingSystem + +import java.nio.file.Path + +class DmmPlugin implements Plugin { + static class Extension implements Named { + Property version + Property localUpxPath + Provider upxExecutableProvider + + Extension(Property version, Property localUpxPath, Provider upxExecutableProvider) { + this.version = version + this.localUpxPath = localUpxPath + this.upxExecutableProvider = upxExecutableProvider + } + + @Override + String getName() { + return "dmm" + } + } + + @Override + void apply(Project target) { + target.with { + plugins.withId("de.undercouch.download") { + var os = OperatingSystem.current() + var version = objects.property(String).convention("4.0.2") + var localUpxPath = objects.property(Path) + + var downloadUpx = tasks.register("downloadUpx", Download) { + group = "upx" + var platform = UpxSupportedOperatingSystems.current() + src("https://github.com/upx/upx/releases/download/v${version.get()}/upx-${version.get()}-${platform.fileSuffix}.${platform.extension}") + dest(layout.buildDirectory.file("upx/downloads/${os.isWindows() ? "upx.zip" : "upx.tar.xz"}")) + overwrite(false) + } + + var unzipUpx = tasks.register("unzipUpx", Sync) { + group = "upx" + from(downloadUpx.map { + os.isWindows() ? zipTree(it.dest) : tarTree(XZArchiver.xz(resources, it.dest) as ReadableResource) + }) + eachFile { + relativePath(new RelativePath(true, name)) + } + into(layout.buildDirectory.dir("upx/exec")) + } + + var upxExecutable = localUpxPath.orElse(unzipUpx.map { it.destinationDir.toPath().resolve("upx${os.isWindows() ? ".exe" : ""}") }) + + var extension = extensions.create("upx", Extension, version, localUpxPath, upxExecutable) + var compress = tasks.register("compress") { + group = "upx" + } + + plugins.withId("org.graalvm.buildtools.native") { + tasks.getByName("nativeCompile") { nativeBuild -> + var compressTask = tasks.register("compress${nativeBuild.name.capitalize()}", UpxTask) { + it.dependsOn(nativeBuild) + it.inputExecutable.set(nativeBuild.outputDirectory.flatMap { it.file(nativeBuild.executableName) }) + it.upxExecutableFile.set(upxExecutable.get()) + } + + compress.configure { + it.dependsOn(compressTask) + } + } + } + } + } + } +} diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UnsafeJvm.java b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UnsafeJvm.java new file mode 100644 index 0000000..1f71f05 --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UnsafeJvm.java @@ -0,0 +1,30 @@ +package com.juanmuscaria.tooling.dmm; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +public class UnsafeJvm { + public static final MethodHandles.Lookup theLookup; + + static { + try { + var unsafeClass = Class.forName("sun.misc.Unsafe"); + Object unsafe = null; + for (Field field : unsafeClass.getDeclaredFields()) { + if (field.getType().equals(unsafeClass)) { + field.setAccessible(true); + unsafe = field.get(null); + } + } + MethodHandles.lookup(); + var lookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + var lookupFieldOffset = (long) unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class) + .invoke(unsafe, lookupField); + + theLookup = (MethodHandles.Lookup) unsafeClass.getDeclaredMethod("getObject", Object.class, long.class) + .invoke(unsafe, MethodHandles.Lookup.class, lookupFieldOffset); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UpxSupportedOperatingSystems.groovy b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UpxSupportedOperatingSystems.groovy new file mode 100644 index 0000000..0a800d2 --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/UpxSupportedOperatingSystems.groovy @@ -0,0 +1,49 @@ +package com.juanmuscaria.tooling.dmm + + +import org.gradle.internal.os.OperatingSystem +import org.jetbrains.annotations.NotNull + +enum UpxSupportedOperatingSystems { + WINDOWS_x86("win32", "zip"), + WINDOWS_x64("win64", "zip"), + LINUX_x64("amd64_linux", "tar.xz"), + LINUX_ARM64("arm64_linux", "tar.xz"), + LINUX_ARM("amd_linux", "tar.xz"), + LINUX_x86("i386_linux", "tar.xz"); + + @NotNull + private final String fileSuffix + @NotNull + private final String extension + + private UpxSupportedOperatingSystems(String fileSuffix, String extension) { + this.fileSuffix = fileSuffix + this.extension = extension + } + + @NotNull + final String getFileSuffix() { + return this.fileSuffix + } + + @NotNull + final String getExtension() { + return this.extension + } + + @NotNull + static UpxSupportedOperatingSystems current() { + var os = OperatingSystem.current() + var arch = System.getProperty("os.arch", "") + boolean is64 = arch.contains("64") + if (os.isWindows()) { + return is64 ? WINDOWS_x64 : WINDOWS_x86 + } else if (os.isLinux()) { + var isArm = arch.contains("arm") || arch.contains("aarch") + return is64 && isArm ? LINUX_ARM64 : (!is64 && isArm ? LINUX_ARM : (is64 ? LINUX_x64 : LINUX_x86)) + } else { + throw new UnsupportedOperationException("Current OS '$os' is not supported.") + } + } +} diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/AbstractArchiver.java b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/AbstractArchiver.java new file mode 100644 index 0000000..f9ec69e --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/AbstractArchiver.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.juanmuscaria.tooling.dmm.file; + +import org.gradle.api.internal.file.archive.compression.CompressedReadableResource; +import org.gradle.api.internal.file.archive.compression.URIBuilder; +import org.gradle.api.resources.internal.ReadableResourceInternal; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.InputStream; +import java.net.URI; + +public abstract class AbstractArchiver implements CompressedReadableResource { + protected final ReadableResourceInternal resource; + protected final URI uri; + + public AbstractArchiver(ReadableResourceInternal resource) { + assert resource != null; + this.uri = new URIBuilder(resource.getURI()).schemePrefix(getSchemePrefix()).build(); + this.resource = resource; + } + + abstract protected String getSchemePrefix(); + + @Override + public abstract @NotNull InputStream read(); + + @Override + public @NotNull String getDisplayName() { + return resource.getDisplayName(); + } + + @Override + public @NotNull URI getURI() { + return uri; + } + + @Override + public @NotNull String getBaseName() { + return resource.getBaseName(); + } + + @Override + public File getBackingFile() { + return resource.getBackingFile(); + } +} diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/XZArchiver.groovy b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/XZArchiver.groovy new file mode 100644 index 0000000..b650928 --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/file/XZArchiver.groovy @@ -0,0 +1,39 @@ +package com.juanmuscaria.tooling.dmm.file + +import com.juanmuscaria.tooling.dmm.UnsafeJvm +import org.gradle.api.internal.resources.DefaultResourceHandler +import org.gradle.api.internal.resources.ResourceResolver +import org.gradle.api.resources.MissingResourceException +import org.gradle.api.resources.ResourceException +import org.gradle.api.resources.ResourceHandler +import org.gradle.api.resources.internal.ReadableResourceInternal +import org.jetbrains.annotations.NotNull +import org.tukaani.xz.SeekableFileInputStream +import org.tukaani.xz.SeekableXZInputStream + +import java.lang.invoke.MethodHandle + +class XZArchiver extends AbstractArchiver { + static MethodHandle resourceResolver = UnsafeJvm.theLookup + .unreflectGetter(DefaultResourceHandler.class.getDeclaredField("resourceResolver")) + + static ReadableResourceInternal xz(ResourceHandler handle, Object path) { + var resolver = resourceResolver.invokeWithArguments(handle) as ResourceResolver + return new XZArchiver(resolver.resolveResource(path)) + } + + XZArchiver(ReadableResourceInternal resource) { + super(resource) + } + + @Override + protected String getSchemePrefix() { + return "xz:" + } + + @NotNull + @Override + InputStream read() throws MissingResourceException, ResourceException { + return new SeekableXZInputStream(new SeekableFileInputStream(resource.backingFile)) + } +} diff --git a/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/task/UpxTask.groovy b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/task/UpxTask.groovy new file mode 100644 index 0000000..59c5ed2 --- /dev/null +++ b/buildSrc/src/main/groovy/com/juanmuscaria/tooling/dmm/task/UpxTask.groovy @@ -0,0 +1,140 @@ +package com.juanmuscaria.tooling.dmm.task + +import org.apache.commons.io.FilenameUtils +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.internal.os.OperatingSystem + +import java.nio.file.Path + +class UpxTask extends DefaultTask { + enum Command { + COMPRESS(null), + DECOMPRESS("-d"), + TEST("-t"), + LIST("-l"); + public final String command + + Command(String command) { + this.command = command + } + } + + enum LogLevel { + NORMAL(null), + NO_WARNINGS("-q"), + NO_ERRORS("-qq"), + OFF("-qqq") + public final String command + + LogLevel(String command) { + this.command = command + } + } + + enum Overlay { + COPY, STRIP, SKIP + } + + abstract static class CompressionLevel implements Serializable { + public static final CompressionLevel BEST = new CompressionLevel() { + @Override + String command() { + return "--best" + } + } + + abstract String command(); + + static class Number extends CompressionLevel { + private final int amount + + Number(int amount) { + this.amount = amount + } + + @Override + String command() { + return "-{$amount}" + } + } + } + + enum BruteLevel { + BRUTE("--brute"), ULTRA_BRUTE("--ultra-brute") + public final String command + + BruteLevel(String command) { + this.command = command + } + } + + @InputFile + final RegularFileProperty inputExecutable = project.objects.fileProperty() + + @OutputFile + @Optional + final RegularFileProperty outputExecutable = project.objects.fileProperty() + .convention(inputExecutable.flatMap { + var name = "${FilenameUtils.removeExtension(it.asFile.name)}-compressed${OperatingSystem.current().isWindows() ? ".exe" : ""}" + var pathString = it.asFile.toPath().parent.resolve(name).toAbsolutePath().toString() + return project.layout.buildDirectory.file(pathString) + }) + + @Input + final Property command = project.objects.property(Command) + .convention(Command.COMPRESS) + + @Input + final Property logLevel = project.objects.property(LogLevel) + .convention(LogLevel.NORMAL) + + @Input + final Property exact = project.objects.property(Boolean) + .convention(false) + + @Input + final Property overlay = project.objects.property(Overlay) + .convention(Overlay.COPY) + + @Input + final Property compressionLevel = project.objects.property(CompressionLevel) + .convention(CompressionLevel.BEST) + + @Input + @Optional + final Property bruteLevel = project.objects.property(BruteLevel) + + @Input + final ListProperty additionalOptions = project.objects.listProperty(String) + .convention(Collections.emptyList()) + + @InputFile + final Property upxExecutableFile = project.objects.property(Path) + + @TaskAction + void execute() { + outputExecutable.get().asFile.delete() + project.exec { + executable = upxExecutableFile.get().toAbsolutePath().toString() + var upxArgs = [] + command.get().command?.with { upxArgs << it } + upxArgs << "-o" << outputExecutable.get().asFile.absolutePath + logLevel.get().command?.with { upxArgs << it } + if (exact.get()) upxArgs << "--exact" + upxArgs << "--overlay=${overlay.get().name().toLowerCase()}".toString() + upxArgs << compressionLevel.get().command() + bruteLevel.orNull?.with { upxArgs << it.command } + upxArgs.addAll(additionalOptions.get() ) + upxArgs << inputExecutable.get().asFile.absolutePath + args(upxArgs) + } + } +} diff --git a/modloader/Patcher.cs b/modloader/Patcher.cs index 8a476fd..9fb59fd 100644 --- a/modloader/Patcher.cs +++ b/modloader/Patcher.cs @@ -41,7 +41,7 @@ public static void Patch(ref AssemblyDefinition assembly) } else { - Console.WriteLine("Replacment path is gone??? No game Assembles will be patched!"); + Console.WriteLine("Replacement path is gone??? No game Assembles will be patched!"); } } }