diff --git a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java index 976517bb..332325f2 100644 --- a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java +++ b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java @@ -14,7 +14,7 @@ import org.gradle.util.GradleVersion; */ @SuppressWarnings("unused") public class LoomGradlePluginBootstrap implements Plugin { - private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.8"; + private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.10"; private static final int MIN_SUPPORTED_MAJOR_JAVA_VERSION = 17; private static final int MIN_SUPPORTED_MAJOR_IDEA_VERSION = 2021; diff --git a/build.gradle b/build.gradle index 7ef3536c..495d3061 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,11 @@ if (!isSnapshot) { logger.lifecycle(":building plugin v${version}") // We must build against the version of Kotlin Gradle ships with. -def kotlinVersion = KotlinDslVersion.current().getKotlinVersion() +def props = new Properties() +Project.class.getClassLoader().getResource("gradle-kotlin-dsl-versions.properties").openStream().withCloseable { + props.load(it) +} +def kotlinVersion = props.getProperty("kotlin") if (libs.versions.kotlin.get() != kotlinVersion) { throw new IllegalStateException("Requires Kotlin version: ${kotlinVersion}") } @@ -334,7 +338,6 @@ tasks.withType(Test).configureEach { import org.gradle.api.internal.artifacts.configurations.ConfigurationRoles -import org.gradle.launcher.cli.KotlinDslVersion import org.gradle.util.GradleVersion import org.w3c.dom.Document import org.w3c.dom.Element diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f855da16..88e22bda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "1.9.22" +kotlin = "1.9.24" asm = "9.6" commons-io = "2.15.1" gson = "2.10.1" diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 33eaef18..1dfbf935 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -6,7 +6,7 @@ mockito = "5.12.0" java-debug = "0.52.0" mixin = "0.12.5+mixin.0.8.5" -gradle-nightly = "8.10-20240613003017+0000" +gradle-nightly = "8.11-20240814172604+0000" fabric-loader = "0.15.11" fabric-installer = "1.0.1" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f7a6eb3..66cd5a0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf13..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30db..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index bfb2f8b8..a1f12033 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -141,14 +141,6 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { void setRefreshDeps(boolean refreshDeps); - /** - * If true, multi-project optimisation mode is enabled. This mode makes builds with many Loom projects - * much faster by increasing sharing and disabling some functionality. - * - *

You can enable it by setting the Gradle property {@code fabric.loom.multiProjectOptimisation} to {@code true}. - */ - boolean multiProjectOptimisation(); - ListProperty getLibraryProcessors(); ListProperty getRemapperExtensions(); diff --git a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java index 984fc6eb..4429ac6e 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java @@ -47,7 +47,6 @@ import net.fabricmc.loom.build.IntermediaryNamespaces; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.extension.MixinExtension; -import net.fabricmc.loom.task.PrepareJarRemapTask; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.LoomVersions; @@ -124,11 +123,6 @@ public abstract class AnnotationProcessorInvoker { args.put("MSG_" + key, value); }); - if (loomExtension.multiProjectOptimisation()) { - // Ensure that all of the mixin mappings have been generated before we create the mixin mappings. - runBeforePrepare(project, task); - } - project.getLogger().debug("Outputting refmap to dir: " + getRefmapDestinationDir(task) + " for compile task: " + task); args.forEach((k, v) -> passArgument(task, k, v)); } catch (IOException e) { @@ -160,12 +154,6 @@ public abstract class AnnotationProcessorInvoker { } } - private void runBeforePrepare(Project project, Task compileTask) { - project.getGradle().allprojects(otherProject -> { - otherProject.getTasks().withType(PrepareJarRemapTask.class, prepareRemapTask -> prepareRemapTask.mustRunAfter(compileTask)); - }); - } - private static void checkPattern(String input, Pattern pattern) { final Matcher matcher = pattern.matcher(input); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 27be5353..0e247360 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -41,7 +41,6 @@ import org.gradle.api.configuration.BuildFeatures; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.ListProperty; -import org.gradle.api.provider.Provider; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.ForgeExtensionAPI; @@ -63,11 +62,9 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMi import net.fabricmc.loom.configuration.providers.minecraft.mapped.MojangMappedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider; -import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ModPlatform; import net.fabricmc.loom.util.download.Download; import net.fabricmc.loom.util.download.DownloadBuilder; -import net.fabricmc.loom.util.gradle.GradleUtils; public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implements LoomGradleExtension { private final Project project; @@ -86,7 +83,6 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl private MojangMappedMinecraftProvider mojangMappedMinecraftProvider; private InstallerData installerData; private boolean refreshDeps; - private final Provider multiProjectOptimisation; private final ListProperty libraryProcessorFactories; private final LoomProblemReporter problemReporter; private final boolean configurationCacheActive; @@ -124,7 +120,6 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl }); refreshDeps = manualRefreshDeps(); - multiProjectOptimisation = GradleUtils.getBooleanPropertyProvider(project, Constants.Properties.MULTI_PROJECT_OPTIMISATION); libraryProcessorFactories = project.getObjects().listProperty(LibraryProcessorManager.LibraryProcessorFactory.class); libraryProcessorFactories.addAll(LibraryProcessorManager.DEFAULT_LIBRARY_PROCESSORS); libraryProcessorFactories.finalizeValueOnRead(); @@ -132,15 +127,6 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl configurationCacheActive = getBuildFeatures().getConfigurationCache().getActive().get(); isolatedProjectsActive = getBuildFeatures().getIsolatedProjects().getActive().get(); - // Fundamentally impossible to support multi-project optimisation with the configuration cache and/or isolated projects. - if (multiProjectOptimisation.get() && configurationCacheActive) { - throw new UnsupportedOperationException("Multi-project optimisation is not supported with the configuration cache"); - } - - if (multiProjectOptimisation.get() && isolatedProjectsActive) { - throw new UnsupportedOperationException("Isolated projects are not supported with multi-project optimisation"); - } - if (configurationCacheActive) { project.getLogger().warn("Loom support for the Gradle configuration cache is highly experimental and may not work as expected. Please report any issues you encounter."); } @@ -315,11 +301,6 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl this.refreshDeps = refreshDeps; } - @Override - public boolean multiProjectOptimisation() { - return multiProjectOptimisation.getOrElse(false); - } - @Override public ListProperty getLibraryProcessors() { return libraryProcessorFactories; diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index 3cb42ee3..2aeedd79 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -84,12 +84,6 @@ public abstract class AbstractRemapJarTask extends Jar { @Input public abstract Property getTargetNamespace(); - /** - * When enabled the TinyRemapperService will not be shared across sub projects. - */ - @Input - public abstract Property getRemapperIsolation(); - @Inject protected abstract WorkerExecutor getWorkerExecutor(); @@ -117,7 +111,6 @@ public abstract class AbstractRemapJarTask extends Jar { public AbstractRemapJarTask() { getSourceNamespace().convention(MappingsNamespace.NAMED.toString()).finalizeValueOnRead(); getTargetNamespace().convention(getProject().provider(() -> IntermediaryNamespaces.runtimeIntermediary(getProject()))).finalizeValueOnRead(); - getRemapperIsolation().convention(true).finalizeValueOnRead(); getIncludesClientOnlyClasses().convention(false).finalizeValueOnRead(); getJarType().finalizeValueOnRead(); diff --git a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java b/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java deleted file mode 100644 index a774ac25..00000000 --- a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2022 FabricMC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package net.fabricmc.loom.task; - -import java.nio.file.Path; - -import javax.inject.Inject; - -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.TaskAction; -import org.gradle.workers.WorkAction; -import org.gradle.workers.WorkParameters; -import org.gradle.workers.WorkQueue; -import org.gradle.workers.WorkerExecutor; - -import net.fabricmc.loom.task.service.TinyRemapperService; -import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; - -/** - * The prepare remap task runs before all other jar remap tasks, should be used to setup tiny remapper. - */ -public abstract class PrepareJarRemapTask extends AbstractLoomTask { - private final RemapJarTask remapJarTask; - @InputFile - public abstract RegularFileProperty getInputFile(); - - @Inject - public PrepareJarRemapTask(RemapJarTask remapJarTask) { - this.remapJarTask = remapJarTask; - - getInputFile().set(remapJarTask.getInputFile()); - // TODO can this be up-to-date when the main task is up-to date? - getOutputs().upToDateWhen((o) -> false); - - getProject().getGradle().allprojects(project -> { - project.getTasks().withType(PrepareJarRemapTask.class, otherTask -> { - if (otherTask == this) return; - - // Ensure that all other prepare tasks inputs have completed - dependsOn(otherTask.getInputs()); - mustRunAfter(otherTask.getInputs()); - }); - }); - } - - @Inject - protected abstract WorkerExecutor getWorkerExecutor(); - - @TaskAction - public void run() { - final WorkQueue workQueue = getWorkerExecutor().noIsolation(); - - workQueue.submit(ReadInputsAction.class, params -> { - params.getTinyRemapperBuildServiceUuid().set(UnsafeWorkQueueHelper.create(remapJarTask.getTinyRemapperService())); - params.getInputFile().set(getInputFile()); - }); - } - - public interface ReadInputsParams extends WorkParameters { - Property getTinyRemapperBuildServiceUuid(); - RegularFileProperty getInputFile(); - } - - public abstract static class ReadInputsAction implements WorkAction { - private final TinyRemapperService tinyRemapperService; - - public ReadInputsAction() { - this.tinyRemapperService = UnsafeWorkQueueHelper.get(getParameters().getTinyRemapperBuildServiceUuid(), TinyRemapperService.class); - } - - @Override - public void execute() { - final Path inputFile = getParameters().getInputFile().getAsFile().get().toPath(); - prepare(tinyRemapperService, inputFile); - } - } - - static void prepare(TinyRemapperService tinyRemapperService, Path inputFile) { - tinyRemapperService.getTinyRemapperForInputs().readInputsAsync(tinyRemapperService.getOrCreateTag(inputFile), inputFile); - } -} diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index 946c01ec..cc83ab23 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -157,10 +157,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { getUseMixinAP().set(LoomGradleExtension.get(getProject()).getMixin().getUseLegacyMixinAp()); - if (getLoomExtension().multiProjectOptimisation()) { - setupPreparationTask(); - } - // Make outputs reproducible by default setReproducibleFileOrder(true); setPreserveFileTimestamps(false); @@ -168,19 +164,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { getJarType().set("classes"); } - private void setupPreparationTask() { - PrepareJarRemapTask prepareJarTask = getProject().getTasks().create("prepare" + getName().substring(0, 1).toUpperCase() + getName().substring(1), PrepareJarRemapTask.class, this); - - dependsOn(prepareJarTask); - mustRunAfter(prepareJarTask); - - getProject().getGradle().allprojects(project -> { - project.getTasks() - .withType(PrepareJarRemapTask.class) - .configureEach(this::mustRunAfter); - }); - } - @TaskAction public void run() { final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); @@ -194,8 +177,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { params.getTinyRemapperBuildServiceUuid().set(UnsafeWorkQueueHelper.create(getTinyRemapperService())); params.getRemapClasspath().from(getClasspath()); - params.getMultiProjectOptimisation().set(getLoomExtension().multiProjectOptimisation()); - final boolean mixinAp = getUseMixinAP().get(); params.getUseMixinExtension().set(!mixinAp); @@ -280,7 +261,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { SetProperty getAtAccessWideners(); Property getUseMixinExtension(); - Property getMultiProjectOptimisation(); Property getOptimizeFmj(); record RefmapData(List mixinConfigs, String refmapName) implements Serializable { } @@ -307,9 +287,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { try { LOGGER.info("Remapping {} to {}", inputFile, outputFile); - if (!getParameters().getMultiProjectOptimisation().getOrElse(false)) { - prepare(); - } + prepare(); if (tinyRemapperService != null) { tinyRemapper = tinyRemapperService.getTinyRemapperForRemapping(); @@ -341,7 +319,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { optimizeFMJ(); } - if (tinyRemapperService != null && !getParameters().getMultiProjectOptimisation().get()) { + if (tinyRemapperService != null) { tinyRemapperService.close(); } @@ -361,7 +339,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { final Path inputFile = getParameters().getInputFile().getAsFile().get().toPath(); if (tinyRemapperService != null) { - PrepareJarRemapTask.prepare(tinyRemapperService, inputFile); + tinyRemapperService.getTinyRemapperForInputs().readInputsAsync(tinyRemapperService.getOrCreateTag(inputFile), inputFile); } } diff --git a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java index f0aa6d89..6bdecf65 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java @@ -33,34 +33,33 @@ import javax.inject.Inject; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskAction; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.fabricmc.loom.task.service.SourceRemapperService; -import net.fabricmc.loom.util.service.BuildSharedServiceManager; -import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; +import net.fabricmc.loom.util.newService.ScopedServiceFactory; public abstract class RemapSourcesJarTask extends AbstractRemapJarTask { - private final Provider serviceManagerProvider; + @Nested + abstract Property getSourcesRemapperServiceOptions(); @Inject public RemapSourcesJarTask() { super(); - serviceManagerProvider = BuildSharedServiceManager.createForTask(this, getBuildEventsListenerRegistry()); - getClasspath().from(getProject().getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); getJarType().set("sources"); + + getSourcesRemapperServiceOptions().set(SourceRemapperService.createOptions(this)); } @TaskAction public void run() { submitWork(RemapSourcesAction.class, params -> { if (!params.namespacesMatch()) { - params.getSourcesRemapperServiceUuid().set(UnsafeWorkQueueHelper.create(SourceRemapperService.create(serviceManagerProvider.get().get(), this))); + params.getSourcesRemapperServiceOptions().set(getSourcesRemapperServiceOptions()); } }); } @@ -73,27 +72,24 @@ public abstract class RemapSourcesJarTask extends AbstractRemapJarTask { } public interface RemapSourcesParams extends AbstractRemapParams { - Property getSourcesRemapperServiceUuid(); + Property getSourcesRemapperServiceOptions(); } public abstract static class RemapSourcesAction extends AbstractRemapAction { private static final Logger LOGGER = LoggerFactory.getLogger(RemapSourcesAction.class); - private final @Nullable SourceRemapperService sourceRemapperService; - public RemapSourcesAction() { super(); - - sourceRemapperService = getParameters().getSourcesRemapperServiceUuid().isPresent() - ? UnsafeWorkQueueHelper.get(getParameters().getSourcesRemapperServiceUuid(), SourceRemapperService.class) - : null; } @Override public void execute() { try { - if (sourceRemapperService != null) { - sourceRemapperService.remapSourcesJar(inputFile, outputFile); + if (!getParameters().namespacesMatch()) { + try (var serviceFactory = new ScopedServiceFactory()) { + SourceRemapperService sourceRemapperService = serviceFactory.get(getParameters().getSourcesRemapperServiceOptions()); + sourceRemapperService.remapSourcesJar(inputFile, outputFile); + } } else { Files.copy(inputFile, outputFile, StandardCopyOption.REPLACE_EXISTING); } diff --git a/src/main/java/net/fabricmc/loom/task/service/NewMappingsService.java b/src/main/java/net/fabricmc/loom/task/service/NewMappingsService.java new file mode 100644 index 00000000..2e90f40e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/service/NewMappingsService.java @@ -0,0 +1,140 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task.service; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; + +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; +import net.fabricmc.loom.util.TinyRemapperHelper; +import net.fabricmc.loom.util.newService.Service; +import net.fabricmc.loom.util.newService.ServiceFactory; +import net.fabricmc.loom.util.newService.ServiceType; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.fabricmc.tinyremapper.IMappingProvider; + +/** + * A service that provides mappings for remapping. + */ +public final class NewMappingsService extends Service implements Closeable { + public static ServiceType TYPE = new ServiceType<>(Options.class, NewMappingsService.class); + + public interface Options extends Service.Options { + @InputFile + RegularFileProperty getMappingsFile(); + @Input + Property getFrom(); + @Input + Property getTo(); + @Input + Property getRemapLocals(); + } + + /** + * Returns options for creating a new mappings service, with a given mappings file. + */ + public static Provider createOptions(Project project, Path mappingsFile, String from, String to, boolean remapLocals) { + return TYPE.create(project, o -> { + o.getMappingsFile().set(project.file(mappingsFile)); + o.getFrom().set(from); + o.getTo().set(to); + o.getRemapLocals().set(remapLocals); + }); + } + + /** + * Returns options for creating a new mappings service, using the mappings as specified in the project's mapping configuration. + */ + public static Provider createOptionsWithProjectMappings(Project project, String from, String to) { + final MappingConfiguration mappingConfiguration = LoomGradleExtension.get(project).getMappingConfiguration(); + return createOptions(project, mappingConfiguration.tinyMappings, from, to, false); + } + + public NewMappingsService(Options options, ServiceFactory serviceFactory) { + super(options, serviceFactory); + } + + private IMappingProvider mappingProvider = null; + private MemoryMappingTree memoryMappingTree = null; + + public IMappingProvider getMappingsProvider() { + if (mappingProvider == null) { + try { + mappingProvider = TinyRemapperHelper.create( + getMappingsPath(), + getFrom(), + getTo(), + getOptions().getRemapLocals().get() + ); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read mappings from: " + getMappingsPath(), e); + } + } + + return mappingProvider; + } + + public MemoryMappingTree getMemoryMappingTree() { + if (memoryMappingTree == null) { + memoryMappingTree = new MemoryMappingTree(); + + try { + MappingReader.read(getMappingsPath(), memoryMappingTree); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read mappings from: " + getMappingsPath(), e); + } + } + + return memoryMappingTree; + } + + public String getFrom() { + return getOptions().getFrom().get(); + } + + public String getTo() { + return getOptions().getTo().get(); + } + + public Path getMappingsPath() { + return getOptions().getMappingsFile().get().getAsFile().toPath(); + } + + @Override + public void close() { + mappingProvider = null; + } +} diff --git a/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java b/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java index b3f5456c..cf1b2c9b 100644 --- a/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java +++ b/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java @@ -26,55 +26,58 @@ package net.fabricmc.loom.task.service; import java.io.File; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Supplier; -import com.google.common.base.Suppliers; -import org.cadixdev.lorenz.MappingSet; import org.cadixdev.mercury.Mercury; import org.cadixdev.mercury.remapper.MercuryRemapper; -import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.task.RemapSourcesJarTask; import net.fabricmc.loom.util.DeletingFileVisitor; import net.fabricmc.loom.util.FileSystemUtil; import net.fabricmc.loom.util.SourceRemapper; import net.fabricmc.loom.util.ZipUtils; -import net.fabricmc.loom.util.service.SharedService; -import net.fabricmc.loom.util.service.SharedServiceManager; +import net.fabricmc.loom.util.newService.Service; +import net.fabricmc.loom.util.newService.ServiceFactory; +import net.fabricmc.loom.util.newService.ServiceType; import net.fabricmc.lorenztiny.TinyMappingsReader; -public final class SourceRemapperService implements SharedService { - public static synchronized SourceRemapperService create(SharedServiceManager serviceManager, RemapSourcesJarTask task) { - final Project project = task.getProject(); - final String to = task.getTargetNamespace().get(); - final String from = task.getSourceNamespace().get(); - final LoomGradleExtension extension = LoomGradleExtension.get(project); - final String id = extension.getMappingConfiguration().getBuildServiceName("sourceremapper", from, to); - final int javaCompileRelease = SourceRemapper.getJavaCompileRelease(project); +public final class SourceRemapperService extends Service { + public static ServiceType TYPE = new ServiceType<>(Options.class, SourceRemapperService.class); - return serviceManager.getOrCreateService(id, () -> - new SourceRemapperService(MappingsService.createDefault(project, serviceManager, from, to), task.getClasspath(), javaCompileRelease)); + public interface Options extends Service.Options { + @Nested + Property getMappings(); + @Input + Property getJavaCompileRelease(); + @InputFiles + ConfigurableFileCollection getClasspath(); + } + + public static Provider createOptions(RemapSourcesJarTask task) { + return TYPE.create(task.getProject(), o -> { + o.getMappings().set(NewMappingsService.createOptionsWithProjectMappings( + task.getProject(), + task.getSourceNamespace().get(), + task.getTargetNamespace().get() + )); + o.getJavaCompileRelease().set(SourceRemapper.getJavaCompileRelease(task.getProject())); + o.getClasspath().from(task.getClasspath()); + }); } private static final Logger LOGGER = LoggerFactory.getLogger(SourceRemapperService.class); - private final MappingsService mappingsService; - private final ConfigurableFileCollection classpath; - private final int javaCompileRelease; - - private final Supplier mercury = Suppliers.memoize(this::createMercury); - - private SourceRemapperService(MappingsService mappingsService, ConfigurableFileCollection classpath, int javaCompileRelease) { - this.mappingsService = mappingsService; - this.classpath = classpath; - this.javaCompileRelease = javaCompileRelease; + public SourceRemapperService(Options options, ServiceFactory serviceFactory) { + super(options, serviceFactory); } public void remapSourcesJar(Path source, Path destination) throws IOException { @@ -96,10 +99,17 @@ public final class SourceRemapperService implements SharedService { Files.delete(destination); } + Mercury mercury = createMercury(); + try (FileSystemUtil.Delegate dstFs = Files.isDirectory(destination) ? null : FileSystemUtil.getJarFileSystem(destination, true)) { Path dstPath = dstFs != null ? dstFs.get().getPath("/") : destination; - doRemap(srcPath, dstPath, source); + try { + mercury.rewrite(srcPath, dstPath); + } catch (Exception e) { + LOGGER.warn("Could not remap " + source + " fully!", e); + } + SourceRemapper.copyNonJavaFiles(srcPath, dstPath, LOGGER, source); } finally { if (isSrcTmp) { @@ -108,30 +118,16 @@ public final class SourceRemapperService implements SharedService { } } - private synchronized void doRemap(Path srcPath, Path dstPath, Path source) { - try { - mercury.get().rewrite(srcPath, dstPath); - } catch (Exception e) { - LOGGER.warn("Could not remap " + source + " fully!", e); - } - } - - private MappingSet getMappings() throws IOException { - return new TinyMappingsReader(mappingsService.getMemoryMappingTree(), mappingsService.getFromNamespace(), mappingsService.getToNamespace()).read(); - } - - private Mercury createMercury() { + private Mercury createMercury() throws IOException { var mercury = new Mercury(); mercury.setGracefulClasspathChecks(true); - mercury.setSourceCompatibilityFromRelease(javaCompileRelease); + mercury.setSourceCompatibilityFromRelease(getOptions().getJavaCompileRelease().get()); - try { - mercury.getProcessors().add(MercuryRemapper.create(getMappings())); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read mercury mappings", e); - } + NewMappingsService mappingsService = getServiceFactory().get(getOptions().getMappings()); + var tinyMappingsReader = new TinyMappingsReader(mappingsService.getMemoryMappingTree(), mappingsService.getFrom(), mappingsService.getTo()).read(); + mercury.getProcessors().add(MercuryRemapper.create(tinyMappingsReader)); - for (File file : classpath.getFiles()) { + for (File file : getOptions().getClasspath().getFiles()) { if (file.exists()) { mercury.getClassPath().add(file.toPath()); } diff --git a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java index 16484d10..f2e23058 100644 --- a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java +++ b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java @@ -46,7 +46,6 @@ import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.build.IntermediaryNamespaces; import net.fabricmc.loom.build.mixin.AnnotationProcessorInvoker; import net.fabricmc.loom.extension.RemapperExtensionHolder; @@ -71,7 +70,6 @@ public class TinyRemapperService implements SharedService { final LoomGradleExtension extension = LoomGradleExtension.get(project); final boolean legacyMixin = extension.getMixin().getUseLegacyMixinAp().get(); final @Nullable KotlinClasspathService kotlinClasspathService = KotlinClasspathService.getOrCreateIfRequired(serviceManager, project); - boolean multiProjectOptimisation = extension.multiProjectOptimisation(); // Generates an id that is used to share the remapper across projects. This tasks in the remap jar task name to handle custom remap jar tasks separately. final var joiner = new StringJoiner(":"); @@ -82,9 +80,8 @@ public class TinyRemapperService implements SharedService { joiner.add("kotlin-" + kotlinClasspathService.version()); } - if (remapJarTask.getRemapperIsolation().get() || !multiProjectOptimisation) { - joiner.add(project.getPath()); - } + // TODO remove this when removing shared service manager. + joiner.add(project.getPath()); extension.getKnownIndyBsms().get().stream().sorted().forEach(joiner::add); @@ -108,20 +105,6 @@ public class TinyRemapperService implements SharedService { final ConfigurationContainer configurations = project.getConfigurations(); ConfigurableFileCollection excludedMinecraftJars = project.files(); - // Exclude none root minecraft jars. - if (multiProjectOptimisation && !extension.isRootProject()) { - MappingsNamespace mappingsNamespace = MappingsNamespace.of(from); - - if (mappingsNamespace != null) { - for (Path minecraftJar : extension.getMinecraftJars(mappingsNamespace)) { - excludedMinecraftJars.from(minecraftJar.toFile()); - } - } else { - // None fatal as this is a performance optimisation. - project.getLogger().warn("Unable to find minecraft jar for namespace {}", from); - } - } - List classPath = remapJarTask.getClasspath() .minus(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES)) .minus(configurations.getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES)) diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index f23e1a07..01107a19 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -155,7 +155,6 @@ public class Constants { } public static final class Properties { - public static final String MULTI_PROJECT_OPTIMISATION = "fabric.loom.multiProjectOptimisation"; public static final String DONT_REMAP = "fabric.loom.dontRemap"; public static final String DISABLE_REMAPPED_VARIANTS = "fabric.loom.disableRemappedVariants"; public static final String DISABLE_PROJECT_DEPENDENT_MODS = "fabric.loom.disableProjectDependentMods"; diff --git a/src/main/java/net/fabricmc/loom/util/gradle/GradleTypeAdapter.java b/src/main/java/net/fabricmc/loom/util/gradle/GradleTypeAdapter.java new file mode 100644 index 00000000..90e7173d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/GradleTypeAdapter.java @@ -0,0 +1,165 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.gradle; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class GradleTypeAdapter implements TypeAdapterFactory { + public static final Gson GSON = new Gson().newBuilder() + .registerTypeAdapterFactory(new GradleTypeAdapter()) + .create(); + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + final Class rawClass = type.getRawType(); + + if (FileCollection.class.isAssignableFrom(rawClass)) { + return new FileCollectionTypeAdapter(); + } else if (RegularFileProperty.class.isAssignableFrom(rawClass)) { + return new RegularFilePropertyTypeAdapter(); + } else if (ListProperty.class.isAssignableFrom(rawClass)) { + return new ListPropertyTypeAdapter(gson); + } else if (MapProperty.class.isAssignableFrom(rawClass)) { + return new MapPropertyTypeAdapter(gson); + } else if (Property.class.isAssignableFrom(rawClass)) { + return new PropertyTypeAdapter(gson); + } + + return null; + } + + private static final class PropertyTypeAdapter> extends WriteOnlyTypeAdapter { + private final Gson gson; + + private PropertyTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter out, T property) throws IOException { + final Object o = property.get(); + final TypeAdapter adapter = gson.getAdapter(o.getClass()); + adapter.write(out, o); + } + } + + private static final class FileCollectionTypeAdapter extends WriteOnlyTypeAdapter { + @Override + public void write(JsonWriter out, T fileCollection) throws IOException { + out.beginArray(); + + final List files = fileCollection.getFiles().stream() + .map(File::getAbsolutePath) + .sorted() + .toList(); + + for (String file : files) { + out.value(file); + } + + out.endArray(); + } + } + + private static final class RegularFilePropertyTypeAdapter extends WriteOnlyTypeAdapter { + @Override + public void write(JsonWriter out, T property) throws IOException { + final File file = property.get().getAsFile(); + out.value(file.getAbsolutePath()); + } + } + + private static final class ListPropertyTypeAdapter> extends WriteOnlyTypeAdapter { + private final Gson gson; + + private ListPropertyTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter out, T property) throws IOException { + List objects = property.get(); + out.beginArray(); + + for (Object o : objects) { + final TypeAdapter adapter = gson.getAdapter(o.getClass()); + adapter.write(out, o); + } + + out.endArray(); + } + } + + private static final class MapPropertyTypeAdapter> extends WriteOnlyTypeAdapter { + private final Gson gson; + + private MapPropertyTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter out, T property) throws IOException { + out.beginObject(); + + for (Map.Entry entry : property.get().entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + + if (!(key instanceof String)) { + throw new UnsupportedOperationException("Map keys must be strings"); + } + + out.name(entry.getKey().toString()); + final TypeAdapter adapter = gson.getAdapter(value.getClass()); + adapter.write(out, value); + } + + out.endObject(); + } + } + + private abstract static class WriteOnlyTypeAdapter extends TypeAdapter { + @Override + public final T read(JsonReader in) { + throw new UnsupportedOperationException("This type adapter is write-only"); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java index d17b3cbc..209703d6 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2022 FabricMC + * Copyright (c) 2022-2024 FabricMC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -41,6 +41,7 @@ import javax.xml.xpath.XPathFactory; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -53,6 +54,7 @@ import org.xml.sax.InputSource; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.ModSettings; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; +import net.fabricmc.loom.util.Constants; public final class SourceSetHelper { @VisibleForTesting @@ -123,7 +125,7 @@ public final class SourceSetHelper { } public static List getClasspath(SourceSetReference reference, Project project) { - final List classpath = getGradleClasspath(reference); + final List classpath = getGradleClasspath(reference, project); classpath.addAll(getIdeaClasspath(reference, project)); classpath.addAll(getEclipseClasspath(reference, project)); @@ -132,7 +134,7 @@ public final class SourceSetHelper { return classpath; } - private static List getGradleClasspath(SourceSetReference reference) { + private static List getGradleClasspath(SourceSetReference reference, Project project) { final SourceSetOutput output = reference.sourceSet().getOutput(); final File resources = output.getResourcesDir(); @@ -144,6 +146,19 @@ public final class SourceSetHelper { classpath.add(resources); } + // Add dev jars from dependency projects if the source set is "main". + if (SourceSet.MAIN_SOURCE_SET_NAME.equals(reference.sourceSet().getName()) && !reference.project().getPath().equals(project.getPath()) + && GradleUtils.isLoomProject(reference.project())) { + final Configuration namedElements = reference.project().getConfigurations().getByName(Constants.Configurations.NAMED_ELEMENTS); + + // Note: We're not looking at the artifacts from configuration variants. It's probably not needed + // (certainly not with Loom's setup), but technically someone could add child variants that add additional + // dev jars that wouldn't be picked up by this. + for (File artifact : namedElements.getOutgoing().getArtifacts().getFiles()) { + classpath.add(artifact); + } + } + return classpath; } diff --git a/src/main/java/net/fabricmc/loom/util/newService/ExampleService.java b/src/main/java/net/fabricmc/loom/util/newService/ExampleService.java new file mode 100644 index 00000000..7417221c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/newService/ExampleService.java @@ -0,0 +1,102 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.newService; + +import java.io.Closeable; +import java.io.IOException; + +import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; + +// Note: This file mostly acts as documentation for the new service system. +// Services are classes that wrap expensive to create objects, such as mappings or tiny remapper. +// Services can be reused multiple times within a given scope, such as configuration or a single task action. +// Services need to have serializable inputs, used as a cache key and serialised to be passed between Gradle contexts. E.g task inputs, or work action params. +public final class ExampleService extends Service implements Closeable { + public static ServiceType TYPE = new ServiceType<>(Options.class, ExampleService.class); + + // Options use Gradle's Property's thus can be used in task inputs. + public interface Options extends Service.Options { + @Nested + Property getNested(); + } + + // Options can be created using data from the Project + static Provider createOptions(Project project) { + return TYPE.create(project, o -> { + o.getNested().set(AnotherService.createOptions(project, "example")); + }); + } + + // An example of how a service could be used, this could be within a task action. + // ServiceFactory would be similar to the existing ScopedSharedServiceManager + // Thus if a service with the same options has previously been created it will be reused. + static void howToUse(Options options, ServiceFactory factory) { + ExampleService exampleService = factory.get(options); + exampleService.doSomething(); + } + + public ExampleService(Options options, ServiceFactory serviceFactory) { + super(options, serviceFactory); + } + + public void doSomething() { + // The service factory used to the creation the current service can be used to get or create other services based on the current service's options. + AnotherService another = getServiceFactory().get(getOptions().getNested()); + System.out.println("ExampleService: " + another.getExample()); + } + + @Override + public void close() throws IOException { + // Anything that needs to be cleaned up when the service is no longer needed. + } + + public static final class AnotherService extends Service { + public static ServiceType TYPE = new ServiceType<>(Options.class, AnotherService.class); + + public interface Options extends Service.Options { + @Input + Property getExample(); + } + + static Provider createOptions(Project project, String example) { + return TYPE.create(project, o -> { + o.getExample().set(example); + }); + } + + public AnotherService(Options options, ServiceFactory serviceFactory) { + super(options, serviceFactory); + } + + // Services can expose any methods they wish, either to return data or do a job. + public String getExample() { + return getOptions().getExample().get(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/newService/ScopedServiceFactory.java b/src/main/java/net/fabricmc/loom/util/newService/ScopedServiceFactory.java new file mode 100644 index 00000000..1648a346 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/newService/ScopedServiceFactory.java @@ -0,0 +1,120 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.newService; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; + +import net.fabricmc.loom.util.gradle.GradleTypeAdapter; + +/** + * An implementation of {@link ServiceFactory} that creates services scoped to the factory instance. + * When the factory is closed, all services created by it are closed and discarded. + */ +public final class ScopedServiceFactory implements ServiceFactory, Closeable { + private final Map> servicesIdentityMap = new IdentityHashMap<>(); + private final Map> servicesJsonMap = new HashMap<>(); + + @Override + public > S get(O options) { + // First check if the service is already created, using the identity map saving the need to serialize the options + //noinspection unchecked + S service = (S) servicesIdentityMap.get(options); + + if (service != null) { + return service; + } + + // TODO skip serialization if we know there is no service with the same type + + // If the service is not already created, serialize the options and check the json map as it may be an equivalent service + String key = getOptionsCacheKey(options); + //noinspection unchecked + service = (S) servicesJsonMap.get(key); + + if (service != null) { + return service; + } + + service = createService(options, this); + + servicesIdentityMap.put(options, service); + servicesJsonMap.put(key, service); + + return service; + } + + private static > S createService(O options, ServiceFactory serviceFactory) { + // We need to create the service from the provided options + final Class serviceClass; + + // Find the service class + try { + //noinspection unchecked + serviceClass = (Class) Class.forName(options.getServiceClass().get()); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to find service class: " + options.getServiceClass().get(), e); + } + + try { + // Check there is only 1 constructor + if (serviceClass.getDeclaredConstructors().length != 1) { + throw new RuntimeException("Service class must have exactly 1 constructor"); + } + + // Check the constructor takes the correct types, the options class and a ScopedServiceFactory + Class[] parameterTypes = serviceClass.getDeclaredConstructors()[0].getParameterTypes(); + + if (parameterTypes.length != 2 || !parameterTypes[0].isAssignableFrom(options.getClass()) || !parameterTypes[1].isAssignableFrom(ServiceFactory.class)) { + throw new RuntimeException("Service class" + serviceClass.getName() + " constructor must take the options class and a ScopedServiceFactory"); + } + + //noinspection unchecked + return (S) serviceClass.getDeclaredConstructors()[0].newInstance(options, serviceFactory); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Failed to create service instance", e); + } + } + + private String getOptionsCacheKey(Service.Options options) { + return GradleTypeAdapter.GSON.toJson(options); + } + + @Override + public void close() throws IOException { + for (Service service : servicesIdentityMap.values()) { + if (service instanceof Closeable closeable) { + closeable.close(); + } + } + + servicesIdentityMap.clear(); + servicesJsonMap.clear(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/newService/Service.java b/src/main/java/net/fabricmc/loom/util/newService/Service.java new file mode 100644 index 00000000..0e6ab54c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/newService/Service.java @@ -0,0 +1,71 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.newService; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.jetbrains.annotations.ApiStatus; + +/** + * A service is used to manage a set of data or a task that may be reused multiple times. + * + * @param The options type. + */ +public abstract class Service { + private final O options; + private final ServiceFactory serviceFactory; + + public Service(O options, ServiceFactory serviceFactory) { + this.options = options; + this.serviceFactory = serviceFactory; + } + + /** + * Gets the options for this service. + * + * @return The options. + */ + protected final O getOptions() { + return options; + } + + /** + * Return the factory that created this service, this can be used to get nested services. + * + * @return The {@link ServiceFactory} instance. + */ + protected ServiceFactory getServiceFactory() { + return serviceFactory; + } + + /** + * The base type of options class for a service. + */ + public interface Options { + @Input + @ApiStatus.Internal + Property getServiceClass(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/newService/ServiceFactory.java b/src/main/java/net/fabricmc/loom/util/newService/ServiceFactory.java new file mode 100644 index 00000000..e6fcdf6e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/newService/ServiceFactory.java @@ -0,0 +1,54 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.newService; + +import org.gradle.api.provider.Property; + +/** + * A factory for creating {@link Service} instances. + */ +public interface ServiceFactory { + /** + * Gets or creates a service instance with the given options. + * + * @param options The options to use. + * @param The options type. + * @param The service type. + * @return The service instance. + */ + default > S get(Property options) { + return get(options.get()); + } + + /** + * Gets or creates a service instance with the given options. + * + * @param options The options to use. + * @param The options type. + * @param The service type. + * @return The service instance. + */ + > S get(O options); +} diff --git a/src/main/java/net/fabricmc/loom/util/newService/ServiceType.java b/src/main/java/net/fabricmc/loom/util/newService/ServiceType.java new file mode 100644 index 00000000..13f47a18 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/newService/ServiceType.java @@ -0,0 +1,52 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.newService; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.provider.Provider; + +/** + * A record to hold the options and service class for a service. + * @param optionsClass The options class for the service. + * @param serviceClass The service class. + */ +public record ServiceType>(Class optionsClass, Class serviceClass) { + /** + * Create an instance of the options class for the given service class. + * @param project The {@link Project} to create the options for. + * @param action An action to configure the options. + * @return The created options instance. + */ + public Provider create(Project project, Action action) { + return project.provider(() -> { + O options = project.getObjects().newInstance(optionsClass); + options.getServiceClass().set(serviceClass.getName()); + options.getServiceClass().finalizeValue(); + action.execute(options); + return options; + }); + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy b/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy index 6857224d..9be4cdbb 100644 --- a/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy @@ -48,8 +48,6 @@ class FabricAPIBenchmark implements GradleProjectTestTrait { patch: "fabric_api" ) - gradle.enableMultiProjectOptimisation() - if (!gradle.buildGradle.text.contains("loom.mixin.useLegacyMixinAp")) { gradle.buildGradle << """ allprojects { diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy index bcd9ea88..44d70bdd 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy @@ -60,6 +60,7 @@ class ConfigurationCacheTest extends Specification implements GradleProjectTestT "configureClientLaunch" | _ "jar" | _ "check" | _ + "remapSourcesJar" | _ } // Test GradleUtils.configurationInputFile invalidates the cache when the file changes diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy index 3b84c8cd..3c1b9ef5 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy @@ -49,8 +49,6 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { patch: "fabric_api" ) - gradle.enableMultiProjectOptimisation() - // Disable the mixin ap if needed. Fabric API is a large enough test project to see if something breaks. if (disableMixinAp) { gradle.buildGradle << """ @@ -108,7 +106,6 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { then: result.task(":build").outcome == SUCCESS - result.task(":prepareRemapJar").outcome == SUCCESS def biomeApiJar = new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/999.0.0/fabric-biome-api-v1-999.0.0.jar") new File(gradle.mavenLocalDir, "net/fabricmc/fabric-api/fabric-biome-api-v1/999.0.0/fabric-biome-api-v1-999.0.0-sources.jar").exists() diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy index 143138f1..0c885d3c 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy @@ -39,7 +39,7 @@ import org.gradle.internal.remote.internal.inet.InetAddressFactory import org.gradle.internal.service.ServiceRegistry import org.gradle.invocation.DefaultGradle import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.gradle.launcher.daemon.configuration.DaemonParameters +import org.gradle.launcher.daemon.configuration.DaemonPriority import org.gradle.launcher.daemon.context.DefaultDaemonContext import org.gradle.launcher.daemon.protocol.DaemonMessageSerializer import org.gradle.launcher.daemon.protocol.Finished @@ -52,7 +52,7 @@ import org.gradle.launcher.daemon.server.DaemonTcpServerConnector import org.gradle.launcher.daemon.server.DefaultDaemonConnection import org.gradle.launcher.daemon.server.IncomingConnectionHandler import org.gradle.launcher.daemon.server.SynchronizedDispatchConnection -import org.gradle.launcher.daemon.server.api.DaemonStateControl +import org.gradle.launcher.daemon.server.api.DaemonState import org.gradle.util.GradleVersion import net.fabricmc.loom.util.gradle.daemon.DaemonUtils @@ -78,7 +78,7 @@ class TestPlugin implements Plugin { // Write it in the registry def registry = new PersistentDaemonRegistry(registryBin.toFile(), services.get(FileLockManager.class), services.get(Chmod.class)) - def daemonInfo = new DaemonInfo(address, createDaemonContext(), "token".bytes, DaemonStateControl.State.Busy) + def daemonInfo = new DaemonInfo(address, createDaemonContext(), "token".bytes, DaemonState.Busy) registry.store(daemonInfo) // When we get a connection, wait for a stop message and process it by responding with a success message @@ -99,36 +99,19 @@ class TestPlugin implements Plugin { } } - // Thanks groovy for allowing me to do this :D static DefaultDaemonContext createDaemonContext() { - int constructorArgsCount = DefaultDaemonContext.class.getConstructors()[0].getParameterCount() - - if (constructorArgsCount == 10) { - // Gradle 8.9+ adds a JavaVersion and NativeServicesMode parameter to the constructor - //noinspection GroovyAssignabilityCheck - return new DefaultDaemonContext( - UUID.randomUUID().toString(), - new File("."), - JavaLanguageVersion.current(), - new File("."), - ProcessHandle.current().pid(), - 0, - List.of(), - false, - NativeServices.NativeServicesMode.NOT_SET, - DaemonParameters.Priority.NORMAL - ) - } - return new DefaultDaemonContext( UUID.randomUUID().toString(), new File("."), + JavaLanguageVersion.current(), + "", new File("."), ProcessHandle.current().pid(), 0, List.of(), false, - DaemonParameters.Priority.NORMAL + NativeServices.NativeServicesMode.NOT_SET, + DaemonPriority.NORMAL ) } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/GradleTypeAdapterTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/GradleTypeAdapterTest.groovy new file mode 100644 index 00000000..5efed261 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/GradleTypeAdapterTest.groovy @@ -0,0 +1,107 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit + +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import spock.lang.IgnoreIf +import spock.lang.Specification + +import net.fabricmc.loom.util.gradle.GradleTypeAdapter + +class GradleTypeAdapterTest extends Specification { + def "Property"() { + given: + def property = Mock(Property) + + when: + def json = GradleTypeAdapter.GSON.toJson(property) + + then: + 1 * property.get() >> "value" + json == "\"value\"" + } + + @IgnoreIf({ os.windows }) + def "FileCollection"() { + given: + def file1 = new File("file1") + def file2 = new File("file2") + def fileCollection = Mock(FileCollection) + + when: + def json = GradleTypeAdapter.GSON.toJson(fileCollection) + + then: + 1 * fileCollection.getFiles() >> [file1, file2].shuffled() + json == "[\"${file1.getAbsolutePath()}\",\"${file2.getAbsolutePath()}\"]" + } + + @IgnoreIf({ os.windows }) + def "RegularFileProperty"() { + given: + def file = new File("file") + def regularFileProperty = Mock(RegularFileProperty) + def regularFile = Mock(RegularFile) + + when: + def json = GradleTypeAdapter.GSON.toJson(regularFileProperty) + + then: + 1 * regularFileProperty.get() >> regularFile + 1 * regularFile.getAsFile() >> file + json == "\"${file.getAbsolutePath()}\"" + } + + def "ListProperty"() { + given: + def listProperty = Mock(ListProperty) + def list = ["value1", "value2"] + + when: + def json = GradleTypeAdapter.GSON.toJson(listProperty) + + then: + 1 * listProperty.get() >> list + json == "[\"value1\",\"value2\"]" + } + + def "MapProperty"() { + given: + def mapProperty = Mock(MapProperty) + def map = ["key1": "value1", "key2": "value2"] + + when: + def json = GradleTypeAdapter.GSON.toJson(mapProperty) + + then: + 1 * mapProperty.get() >> map + json == "{\"key1\":\"value1\",\"key2\":\"value2\"}" + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/service/MappingsServiceTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/service/MappingsServiceTest.groovy new file mode 100644 index 00000000..97232f09 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/service/MappingsServiceTest.groovy @@ -0,0 +1,59 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.service + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property + +import net.fabricmc.loom.task.service.NewMappingsService +import net.fabricmc.loom.test.util.GradleTestUtil + +class MappingsServiceTest extends ServiceTestBase { + def "get mapping tree"() { + given: + NewMappingsService service = factory.get(new TestOptions( + mappingsFile: GradleTestUtil.mockRegularFileProperty(new File("src/test/resources/mappings/PosInChunk.mappings")), + from: GradleTestUtil.mockProperty("intermediary"), + to: GradleTestUtil.mockProperty("named"), + )) + + when: + def mappingTree = service.memoryMappingTree + + then: + mappingTree.getClasses().size() == 2 + + service.from == "intermediary" + service.to == "named" + } + + static class TestOptions implements NewMappingsService.Options { + RegularFileProperty mappingsFile + Property from + Property to + Property remapLocals = GradleTestUtil.mockProperty(false) + Property serviceClass = serviceClassProperty(NewMappingsService.TYPE) + } +} \ No newline at end of file diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/service/ScopedServiceFactoryTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/service/ScopedServiceFactoryTest.groovy new file mode 100644 index 00000000..0b8907e2 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/service/ScopedServiceFactoryTest.groovy @@ -0,0 +1,145 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.service + +import groovy.transform.InheritConstructors +import groovy.transform.TupleConstructor +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import spock.lang.Specification + +import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.newService.ScopedServiceFactory +import net.fabricmc.loom.util.newService.Service +import net.fabricmc.loom.util.newService.ServiceType + +class ScopedServiceFactoryTest extends Specification { + def "create service"() { + given: + def options = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def factory = new ScopedServiceFactory() + + when: + TestService service = factory.get(options) + + then: + service.getExample() == "hello" + + cleanup: + factory.close() + } + + def "reuse service"() { + given: + def options = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def factory = new ScopedServiceFactory() + + when: + TestService service = factory.get(options) + TestService service2 = factory.get(options) + + then: + service === service2 + + cleanup: + factory.close() + } + + def "reuse service different options instance"() { + given: + def options = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def options2 = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def factory = new ScopedServiceFactory() + + when: + TestService service = factory.get(options) + TestService service2 = factory.get(options2) + + then: + service === service2 + + cleanup: + factory.close() + } + + def "Separate instances"() { + given: + def options = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def options2 = new TestServiceOptions(GradleTestUtil.mockProperty("world")) + def factory = new ScopedServiceFactory() + + when: + TestService service = factory.get(options) + TestService service2 = factory.get(options2) + + then: + service !== service2 + service.example == "hello" + service2.example == "world" + + cleanup: + factory.close() + } + + def "close service"() { + given: + def options = new TestServiceOptions(GradleTestUtil.mockProperty("hello")) + def factory = new ScopedServiceFactory() + + when: + TestService service = factory.get(options) + factory.close() + + then: + service.closed + } + + @InheritConstructors + static class TestService extends Service implements Closeable { + static ServiceType TYPE = new ServiceType(TestService.Options.class, TestService.class) + + interface Options extends Service.Options { + @Input + Property getExample(); + } + + boolean closed = false + + String getExample() { + return options.example.get() + } + + @Override + void close() throws Exception { + closed = true + } + } + + @TupleConstructor + static class TestServiceOptions implements TestService.Options { + Property example + Property serviceClass = ServiceTestBase.serviceClassProperty(TestService.TYPE) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/service/ServiceTestBase.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/service/ServiceTestBase.groovy new file mode 100644 index 00000000..7eba4b3f --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/service/ServiceTestBase.groovy @@ -0,0 +1,50 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.service + +import org.gradle.api.provider.Property +import spock.lang.Specification + +import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.newService.ScopedServiceFactory +import net.fabricmc.loom.util.newService.Service +import net.fabricmc.loom.util.newService.ServiceType + +abstract class ServiceTestBase extends Specification { + ScopedServiceFactory factory + + def setup() { + factory = new ScopedServiceFactory() + } + + def cleanup() { + factory.close() + factory = null + } + + static Property serviceClassProperty(ServiceType type) { + return GradleTestUtil.mockProperty(type.serviceClass().name) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/service/SourceRemapperServiceTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/service/SourceRemapperServiceTest.groovy new file mode 100644 index 00000000..51bfb230 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/service/SourceRemapperServiceTest.groovy @@ -0,0 +1,95 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.service + +import java.nio.file.Files +import java.nio.file.Path + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.intellij.lang.annotations.Language + +import net.fabricmc.loom.task.service.NewMappingsService +import net.fabricmc.loom.task.service.SourceRemapperService +import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.DeletingFileVisitor + +class SourceRemapperServiceTest extends ServiceTestBase { + def "remap sources"() { + given: + Path tempDirectory = Files.createTempDirectory("test") + Path sourceDirectory = tempDirectory.resolve("source") + Path destDirectory = tempDirectory.resolve("dst") + Path mappings = tempDirectory.resolve("mappings.tiny") + + Files.createDirectories(sourceDirectory) + Files.createDirectories(destDirectory) + Files.writeString(sourceDirectory.resolve("Source.java"), SOURCE) + Files.writeString(mappings, MAPPINGS) + + SourceRemapperService service = factory.get(new TestOptions( + mappings: GradleTestUtil.mockProperty( + new MappingsServiceTest.TestOptions( + mappingsFile: GradleTestUtil.mockRegularFileProperty(mappings.toFile()), + from: GradleTestUtil.mockProperty("named"), + to: GradleTestUtil.mockProperty("intermediary"), + ) + ), + )) + + when: + service.remapSourcesJar(sourceDirectory, destDirectory) + + then: + // This isn't actually remapping, as we dont have the classpath setup at all. But that's not what we're testing here. + !Files.readString(destDirectory.resolve("Source.java")).isEmpty() + + cleanup: + Files.walkFileTree(tempDirectory, new DeletingFileVisitor()) + } + + @Language("java") + static String SOURCE = """ + class Source { + public void test() { + System.out.println("Hello"); + } + } + """.trim() + + // Tiny v2 mappings to rename println + static String MAPPINGS = """ + tiny 2 0 intermediary named + c Source Source + m ()V println test + """.trim() + + static class TestOptions implements SourceRemapperService.Options { + Property mappings + Property javaCompileRelease = GradleTestUtil.mockProperty(17) + ConfigurableFileCollection classpath = GradleTestUtil.mockConfigurableFileCollection() + Property serviceClass = serviceClassProperty(SourceRemapperService.TYPE) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy index 9cf2e7b9..9356a8a5 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy @@ -33,7 +33,6 @@ import org.gradle.util.GradleVersion import spock.lang.Shared import net.fabricmc.loom.test.LoomTestConstants -import net.fabricmc.loom.util.Constants import net.fabricmc.loom.util.ZipUtils trait GradleProjectTestTrait { @@ -332,9 +331,5 @@ trait GradleProjectTestTrait { } """ } - - void enableMultiProjectOptimisation() { - getGradleProperties() << "\n${Constants.Properties.MULTI_PROJECT_OPTIMISATION}=true" - } } } \ No newline at end of file diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy index 49448cf0..c98e8486 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy @@ -26,6 +26,7 @@ package net.fabricmc.loom.test.util import org.gradle.api.Project import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.SourceDirectorySet @@ -132,6 +133,12 @@ class GradleTestUtil { return mock } + static ConfigurableFileCollection mockConfigurableFileCollection(File... files) { + def mock = mock(ConfigurableFileCollection.class) + when(mock.getFiles()).thenReturn(Set.of(files)) + return mock + } + static RepositoryHandler mockRepositoryHandler() { def mock = mock(RepositoryHandler.class) return mock