diff --git a/src/main/java/com/modrinth/minotaur/Minotaur.java b/src/main/java/com/modrinth/minotaur/Minotaur.java index 21fc291..d064587 100644 --- a/src/main/java/com/modrinth/minotaur/Minotaur.java +++ b/src/main/java/com/modrinth/minotaur/Minotaur.java @@ -1,12 +1,20 @@ package com.modrinth.minotaur; +import com.modrinth.minotaur.dependencies.container.NamedDependency; +import com.modrinth.minotaur.request.ModrinthApiSettings; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskContainer; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.api.tasks.bundling.AbstractArchiveTask; -import static com.modrinth.minotaur.Util.ext; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.modrinth.minotaur.gameversion.GameVersionDetection.detectGameVersions; +import static com.modrinth.minotaur.loader.LoaderDetection.detectLoaders; /** * The main class for Minotaur. @@ -20,55 +28,81 @@ public class Minotaur implements Plugin { */ @Override public void apply(final Project project) { - project.getExtensions().create("modrinth", ModrinthExtension.class, project); + ModrinthExtension ext = project.getExtensions().create("modrinth", ModrinthExtension.class); project.getLogger().debug("Created the `modrinth` extension."); + ListProperty defaultLoaders = project.getObjects().listProperty(String.class).empty(); + ListProperty defaultGameVersions = project.getObjects().listProperty(String.class).empty(); + + // Some of the plugins we inspect register their extensions *super* late + // and this is the only thing that's late enough, as far as I can tell. + project.getGradle().projectsEvaluated(g -> { + defaultLoaders.set(detectLoaders(project)); + defaultGameVersions.set(detectGameVersions(project)); + }); + TaskContainer tasks = project.getTasks(); tasks.register("modrinth", TaskModrinthUpload.class, task -> { task.setGroup("publishing"); task.setDescription("Upload project to Modrinth"); task.dependsOn(tasks.named("assemble")); task.mustRunAfter(tasks.named("build")); - task.notCompatibleWithConfigurationCache("Fundamentally incompatible with configuration cache"); + + task.getFile().set(ext.getFile()); + task.getAdditionalFiles().set(ext.getAdditionalFileDsl().getAdditionalFiles()); + task.getUntypedAdditionalFiles().from(ext.getAdditionalFiles()); + task.getChangelog().set(ext.getChangelog()); + task.getFailSilently().set(ext.getFailSilently()); + Provider resolvedVersion = makeResolvedVersion(project, ext); + task.getProjectId().set(ext.getProjectId()); + task.getVersionNumber().set(resolvedVersion); + task.getVersionName().set(ext.getVersionName().orElse(task.getVersionNumber())); + wireUpApiSettings(task.getApiSettings(), ext, resolvedVersion); + task.getIsDryRun().set(ext.getDebugMode()); + task.getLoaders().set(getOrDefaultLoaders(ext, defaultLoaders)); + task.getGameVersions().set(getOrDefaultGameVersions(ext, defaultGameVersions)); + task.getDependencies().set(ext.getDependencies().zip(ext.getNamedDependencies(), (deps, named) -> + Stream.concat( + named.stream().map(NamedDependency::getDependency), + deps.stream() + ).collect(Collectors.toList()) + )); + task.getVersionType().set(ext.getVersionType()); }); project.getLogger().debug("Registered the `modrinth` task."); tasks.register("modrinthSyncBody", TaskModrinthSyncBody.class, task -> { task.setGroup("publishing"); task.setDescription("Sync project description to Modrinth"); - task.notCompatibleWithConfigurationCache("Fundamentally incompatible with configuration cache"); + + wireUpApiSettings(task.getApiSettings(), ext, makeResolvedVersion(project, ext)); + task.getProjectId().set(ext.getProjectId()); + task.getSyncBodyFrom().set(ext.getSyncBodyFrom()); + task.getIsDryRun().set(ext.getDebugMode()); + task.getFailSilently().set(ext.getFailSilently()); }); project.getLogger().debug("Registered the `modrinthSyncBody` task."); + project.getLogger().debug("Successfully applied the Modrinth plugin!"); + } - project.afterEvaluate(evaluatedProject -> { - ModrinthExtension ext = ext(evaluatedProject); - - if (!ext.getAutoAddDependsOn().getOrElse(true)) { - return; - } - - evaluatedProject.getTasks().named("modrinth", TaskModrinthUpload.class).configure(task -> { - task.getWiredInputFiles().from(ext.getFile()); - task.getInputs().property("changelog", ext.getChangelog()).optional(true); - - ext.getAdditionalFiles().get().forEach(file -> { - if (file == null) { - return; - } + private static Provider makeResolvedVersion(Project project, ModrinthExtension ext) { + return ext.getVersionNumber().orElse(project.getVersion().toString()); + } - // Try to get an AbstractArchiveTask from the input file by whatever means possible. - if (file instanceof AbstractArchiveTask) { - task.dependsOn(file); - } else if (file instanceof TaskProvider && - ((TaskProvider) file).get() instanceof AbstractArchiveTask) { - task.dependsOn(((TaskProvider) file).get()); - } - }); + private static void wireUpApiSettings(ModrinthApiSettings settings, ModrinthExtension ext, Provider resolvedVersion) { + settings.getApiUrl().set(ext.getApiUrl()); + settings.getToken().set(ext.getToken().orElse(ext.getDebugMode().map(d -> d ? "mrp-meow" : null))); + settings.getProjectId().set(ext.getProjectId()); + settings.getVersionNumber().set(resolvedVersion); + } - evaluatedProject.getLogger().debug("Made the `modrinth` task depend on the upload file and additional files."); - }); - }); + private static Provider> getOrDefaultLoaders(ModrinthExtension ext, Provider> defaultLoaders) { + Provider> fallback = ext.getDetectLoaders() + .zip(defaultLoaders, (detect, loaders) -> detect ? loaders : Collections.emptyList()); + return ext.getLoaders().map(l -> l.isEmpty() ? null : l).orElse(fallback); + } - project.getLogger().debug("Successfully applied the Modrinth plugin!"); + private static Provider> getOrDefaultGameVersions(ModrinthExtension ext, Provider> detectedVersions) { + return ext.getGameVersions().zip(detectedVersions, (v, def) -> v.isEmpty() ? def : v); } } diff --git a/src/main/java/com/modrinth/minotaur/ModrinthExtension.java b/src/main/java/com/modrinth/minotaur/ModrinthExtension.java index c00267c..5c72c22 100644 --- a/src/main/java/com/modrinth/minotaur/ModrinthExtension.java +++ b/src/main/java/com/modrinth/minotaur/ModrinthExtension.java @@ -5,10 +5,19 @@ import com.modrinth.minotaur.dependencies.container.DependencyDSL; import masecla.modrinth4j.model.version.ProjectVersion.VersionType; import org.gradle.api.Action; -import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; + +import javax.inject.Inject; +import java.io.File; +import java.util.concurrent.Callable; /** * Class defining the extension used for configuring {@link TaskModrinthUpload}. This is done via the {@code modrinth @@ -46,30 +55,28 @@ public class ModrinthExtension extends DependencyDSL { */ public static final String DEFAULT_VERSION_TYPE = "release"; - /** - * @param project The Gradle project that the extension is applied to - */ - public ModrinthExtension(Project project) { - super(project.getObjects()); - additionalFileDsl = project.getObjects().newInstance(AdditionalFileDSL.class); - apiUrl = project.getObjects().property(String.class).convention(DEFAULT_API_URL); - token = project.getObjects().property(String.class).convention(project.getProviders().environmentVariable("MODRINTH_TOKEN")); - projectId = project.getObjects().property(String.class); - versionNumber = project.getObjects().property(String.class); - versionName = project.getObjects().property(String.class); - changelog = project.getObjects().property(String.class).convention(DEFAULT_CHANGELOG); - legacyUploadFile = project.getObjects().property(Object.class); - file = project.getObjects().fileProperty().convention(legacyUploadFile.flatMap(o -> Util.resolveFileProperty(project, o))); - additionalFiles = project.getObjects().listProperty(Object.class).empty(); - versionType = project.getObjects().property(String.class).convention(DEFAULT_VERSION_TYPE); - gameVersions = project.getObjects().listProperty(String.class).empty(); - loaders = project.getObjects().listProperty(String.class).empty(); - dependencies = project.getObjects().listProperty(Dependency.class).empty(); - failSilently = project.getObjects().property(Boolean.class).convention(false); - detectLoaders = project.getObjects().property(Boolean.class).convention(true); - debugMode = project.getObjects().property(Boolean.class).convention(false); - syncBodyFrom = project.getObjects().property(String.class); - autoAddDependsOn = project.getObjects().property(Boolean.class).convention(true); + @Inject + public ModrinthExtension(ProviderFactory providers, ObjectFactory objects, ProjectLayout layout) { + super(objects); + additionalFileDsl = objects.newInstance(AdditionalFileDSL.class); + apiUrl = objects.property(String.class).convention(DEFAULT_API_URL); + token = objects.property(String.class).convention(providers.environmentVariable("MODRINTH_TOKEN")); + projectId = objects.property(String.class); + versionNumber = objects.property(String.class); + versionName = objects.property(String.class); + changelog = objects.property(String.class).convention(DEFAULT_CHANGELOG); + legacyUploadFile = objects.property(Object.class); + file = objects.fileProperty().convention(resolveLegacyFile(objects, layout, legacyUploadFile)); + additionalFiles = objects.listProperty(Object.class).empty(); + versionType = objects.property(String.class).convention(DEFAULT_VERSION_TYPE); + gameVersions = objects.listProperty(String.class).empty(); + loaders = objects.listProperty(String.class).empty(); + dependencies = objects.listProperty(Dependency.class).empty(); + failSilently = objects.property(Boolean.class).convention(false); + detectLoaders = objects.property(Boolean.class).convention(true); + debugMode = objects.property(Boolean.class).convention(false); + syncBodyFrom = objects.property(String.class); + autoAddDependsOn = objects.property(Boolean.class).convention(true); } public void additionalFiles(Action action) { @@ -128,9 +135,10 @@ public Property getChangelog() { } /** - * @return The upload artifact file. This can be any object type that is resolvable by - * {@link Util#resolveFile(Project, Object)}. + * @return The upload artifact file + * @deprecated Use {@link #getFile()} instead. */ + @Deprecated public Property getUploadFile() { return this.legacyUploadFile; } @@ -209,8 +217,19 @@ public Property getSyncBodyFrom() { /** * @return Whether to automatically add the `dependsOn` information for upload files + * @deprecated No longer does anything; Gradle does this for us. */ + @Deprecated public Property getAutoAddDependsOn() { return autoAddDependsOn; } + + private static Provider resolveLegacyFile(ObjectFactory objects, ProjectLayout layout, Property legacyFile) { + ConfigurableFileCollection legacyFiles = objects.fileCollection(); + legacyFiles.from((Callable) legacyFile::getOrNull); + + Provider singleLegacyFile = legacyFiles.getElements() + .map(files -> files.isEmpty() ? null : files.iterator().next().getAsFile()); + return layout.file(singleLegacyFile); + } } diff --git a/src/main/java/com/modrinth/minotaur/TaskModrinthSyncBody.java b/src/main/java/com/modrinth/minotaur/TaskModrinthSyncBody.java index baebccd..e1bef6c 100644 --- a/src/main/java/com/modrinth/minotaur/TaskModrinthSyncBody.java +++ b/src/main/java/com/modrinth/minotaur/TaskModrinthSyncBody.java @@ -1,47 +1,76 @@ package com.modrinth.minotaur; import com.google.gson.JsonObject; +import com.modrinth.minotaur.request.ModrinthApiSettings; import masecla.modrinth4j.endpoints.project.ModifyProject.ProjectModifications; import masecla.modrinth4j.main.ModrinthAPI; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.UntrackedTask; import java.util.Objects; import java.util.regex.Pattern; -import static com.modrinth.minotaur.Util.*; - /** * A task used to communicate with Modrinth for the purpose of syncing project body with, for example, a README. */ -public class TaskModrinthSyncBody extends DefaultTask { +@UntrackedTask(because = "edits data remotely on Modrinth") +public abstract class TaskModrinthSyncBody extends DefaultTask { + /** + * @return the Modrinth API settings + */ + @Nested + public abstract ModrinthApiSettings getApiSettings(); + + /** + * @return The ID of the project to upload the file to. + */ + @Input + public abstract Property getProjectId(); + + /** + * @return the file to sync the project's description from + */ + @Input + public abstract Property getSyncBodyFrom(); + + /** + * @return whether the task should only simulate the changes without actually performing them + */ + @Input + public abstract Property getIsDryRun(); + + /** + * @return whether the build should continue even if the operation failed + */ + @Input + public abstract Property getFailSilently(); + /** * Uploads a body to a project, both of which are specified in {@link ModrinthExtension}. */ @TaskAction public void apply() { getLogger().lifecycle("Minotaur: {}", getClass().getPackage().getImplementationVersion()); - ModrinthExtension ext = ext(getProject()); try { - if (ext.getSyncBodyFrom() == null) { - throw new GradleException("Sync project body task was called, but `syncBodyFrom` was null!"); - } - - ModrinthAPI api = api(getProject()); + ModrinthAPI api = Util.api(getLogger(), getApiSettings()); // This isn't used until later, but resolve it early anyway to throw invalid IDs early String id = Objects.requireNonNull( - api.projects().getProjectIdBySlug(ext.getProjectId().get()).join(), - "Failed to resolve project ID: " + ext.getProjectId().get() + api.projects().getProjectIdBySlug(getProjectId().get()).join(), + "Failed to resolve project ID: " + getProjectId().get() ); getLogger().debug("Syncing body to project {}", id); Pattern excludeRegex = Pattern.compile(".*?", Pattern.DOTALL); - String body = ext.getSyncBodyFrom().get().replaceAll("\r\n", "\n"); + String body = getSyncBodyFrom().get().replace("\r\n", "\n"); body = excludeRegex.matcher(body).replaceAll(""); - if (ext.getDebugMode().get()) { + if (getIsDryRun().get()) { JsonObject data = new JsonObject(); data.addProperty("body", body); getLogger().lifecycle("Full data to be sent for upload: {}", data); @@ -50,11 +79,13 @@ public void apply() { } api.projects().modify(id, ProjectModifications.builder().body(body).build()).join(); - getLogger().lifecycle("Successfully synced body to project {}.", ext.getProjectId().get()); + getLogger().lifecycle("Successfully synced body to project {}.", getProjectId().get()); } catch (Exception e) { - if (ext.getFailSilently().get()) { + if (getFailSilently().get()) { getLogger().info("Failed to sync body to Modrinth. Check logs for more info."); getLogger().error("Modrinth body sync failed silently.", e); + } else if (e instanceof GradleException) { + throw (GradleException) e; } else { throw new GradleException("Failed to sync project body! " + e.getMessage(), e); } diff --git a/src/main/java/com/modrinth/minotaur/TaskModrinthUpload.java b/src/main/java/com/modrinth/minotaur/TaskModrinthUpload.java index c4210e7..ca027e5 100644 --- a/src/main/java/com/modrinth/minotaur/TaskModrinthUpload.java +++ b/src/main/java/com/modrinth/minotaur/TaskModrinthUpload.java @@ -2,34 +2,40 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.modrinth.minotaur.additionalfiles.TypedFileCollection; import com.modrinth.minotaur.dependencies.Dependency; -import com.modrinth.minotaur.responses.ResponseUpload; -import io.papermc.paperweight.userdev.PaperweightUserExtension; import com.modrinth.minotaur.masecla.modrinth4j.endpoints.version.TemporaryCreateVersion; import com.modrinth.minotaur.masecla.modrinth4j.endpoints.version.TemporaryCreateVersion.TemporaryCreateVersionRequest; +import com.modrinth.minotaur.request.ModrinthApiSettings; +import com.modrinth.minotaur.responses.ResponseUpload; import masecla.modrinth4j.main.ModrinthAPI; import masecla.modrinth4j.model.version.ProjectVersion; import masecla.modrinth4j.model.version.ProjectVersion.ProjectDependency; import masecla.modrinth4j.model.version.ProjectVersion.VersionType; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.plugins.PluginManager; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.Optional; -import org.gradle.api.tasks.TaskAction; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.*; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.io.File; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; -import static com.modrinth.minotaur.Util.*; +import static com.modrinth.minotaur.Util.api; /** * A task used to communicate with Modrinth for the purpose of uploading build artifacts. */ +@UntrackedTask(because = "uploads to Modrinth") public abstract class TaskModrinthUpload extends DefaultTask { /** * The response from the API when the file was uploaded successfully. @@ -62,14 +68,94 @@ public boolean wasUploadSuccessful() { } /** - * Input property used to add automatic task dependencies. + * @return the main file to upload + */ + @InputFile + public abstract RegularFileProperty getFile(); + + /** + * @return additional files to upload alongside the main file + * @see #getUntypedAdditionalFiles() + */ + @Nested + public abstract ListProperty getAdditionalFiles(); + + /** + * Gets a collection of additional files to upload alongside the main file. + * Similar to {@link #getAdditionalFiles()}, but does not specify the type of the files, and instead relies on + * the file name to determine the type. * - * @return property + * @return additional files to upload alongside the main file */ @InputFiles - @Optional - @ApiStatus.Internal - public abstract ConfigurableFileCollection getWiredInputFiles(); + public abstract ConfigurableFileCollection getUntypedAdditionalFiles(); + + /** + * @return the changelog text + */ + @Input + public abstract Property getChangelog(); + + /** + * @return whether the build should continue even if the upload failed + */ + @Input + public abstract Property getFailSilently(); + + /** + * @return The ID of the project to upload the file to. + */ + @Input + public abstract Property getProjectId(); + + /** + * @return the version number of the build + */ + @Input + public abstract Property getVersionNumber(); + + /** + * @return the version name of the build + */ + @Input + public abstract Property getVersionName(); + + /** + * @return the Modrinth API settings + */ + @Nested + public abstract ModrinthApiSettings getApiSettings(); + + /** + * @return whether the task should only simulate the upload without actually performing it + */ + @Input + public abstract Property getIsDryRun(); + + /** + * @return the mod loaders which this build supports + */ + @Input + public abstract ListProperty getLoaders(); + + /** + * @return the game versions which this build supports + */ + @Input + public abstract ListProperty getGameVersions(); + + /** + * @return the Modrinth project dependencies of this build + */ + @Input + public abstract ListProperty getDependencies(); + + /** + * @return the release type for the project + * @see VersionType + */ + @Input + public abstract Property getVersionType(); /** * Defines what to do when the Modrinth upload task is invoked. @@ -79,182 +165,54 @@ public boolean wasUploadSuccessful() { *
  • Resolves each file or task to be uploaded, ensuring they're all valid
  • *
  • Uploads these files to the Modrinth API under a new version
  • * - * This is all in a try/catch block so that, if {@link ModrinthExtension#getFailSilently()} is enabled, it won't + * This is all in a try/catch block so that, if {@link #getFailSilently()} is enabled, it won't * fail the build if it fails to upload the version to Modrinth. */ @TaskAction public void apply() { - getLogger().lifecycle("Minotaur: {}", getClass().getPackage().getImplementationVersion()); - ModrinthExtension ext = ext(getProject()); - PluginManager pluginManager = getProject().getPluginManager(); try { - ModrinthAPI api = api(getProject()); - - String slug = ext.getProjectId().get(); - String id = api.projects().getProjectIdBySlug(slug).join(); - if (id == null) { - if (ext.getDebugMode().get()) { - getLogger().error("Cannot find project with id '{}'.", slug); - id = ""; - } else { - throw new GradleException(String.format("Cannot find project with id '%s'", slug)); - } - } - getLogger().debug("Uploading version to project {}", id); + getLogger().lifecycle("Minotaur: {}", getClass().getPackage().getImplementationVersion()); - // Add version name if it's null - String versionNumber = resolveVersionNumber(getProject()); - if (ext.getVersionName().getOrNull() == null) { - ext.getVersionName().set(versionNumber); - } - - // Attempt to automatically resolve the loader if none were specified. - if (ext.getLoaders().get().isEmpty() && ext.getDetectLoaders().get()) { - Map pluginLoaderMap = new HashMap<>(); - pluginLoaderMap.put("net.minecraftforge.gradle", "forge"); - pluginLoaderMap.put("net.neoforged.gradle", "neoforge"); - pluginLoaderMap.put("net.neoforged.gradle.userdev", "neoforge"); - pluginLoaderMap.put("net.neoforged.moddev", "neoforge"); - pluginLoaderMap.put("net.neoforged.moddev.legacyforge", "forge"); - pluginLoaderMap.put("org.quiltmc.loom", "quilt"); - pluginLoaderMap.put("org.spongepowered.gradle.plugin", "sponge"); - pluginLoaderMap.put("io.papermc.paperweight.userdev", "paper"); - pluginLoaderMap.put("xyz.jpenilla.run-paper", "paper"); - pluginLoaderMap.put("xyz.jpenilla.run-waterfall", "waterfall"); - pluginLoaderMap.put("xyz.jpenilla.run-velocity", "velocity"); - - pluginLoaderMap.forEach((plugin, loader) -> { - if (pluginManager.hasPlugin(plugin)) { - getLogger().debug("Adding loader '{}' because plugin '{}' was found.", loader, plugin); - add(ext.getLoaders(), loader); - } - }); - - if (!ext.getLoaders().get().contains("quilt") // don't count quilt-loom twice - && getProject().getExtensions().findByName("loom") != null) { - Object loomPlatform = getProject().findProperty("loom.platform"); - if (loomPlatform != null) { - getLogger().debug("Adding loader '{}' because 'loom' extension was found and loom.platform={}.", loomPlatform, loomPlatform); - add(ext.getLoaders(), (String) loomPlatform); - } else { - getLogger().debug("Adding loader 'fabric' because 'loom' extension was found."); - add(ext.getLoaders(), "fabric"); - } - } - } - - if (ext.getLoaders().get().isEmpty()) { - throw new GradleException("Cannot upload to Modrinth: no loaders specified!"); - } + validateInputs(); + VersionType versionType = getAndValidateVersionType(); - // Attempt to automatically resolve the game version if none were specified. - if (ext.getGameVersions().get().isEmpty()) { - if (pluginManager.hasPlugin("net.minecraftforge.gradle") || - pluginManager.hasPlugin("net.neoforged.gradle") || - pluginManager.hasPlugin("net.neoforged.gradle.userdev")) { - - String[] props = {"MC_VERSION", "minecraftVersion"}; - - for (String prop : props) { - try { - String version = (String) getProject().getExtensions().getExtraProperties().get(prop); - if (version != null) { - getLogger().debug("Adding fallback game version {} from ForgeGradle/NeoGradle.", version); - add(ext.getGameVersions(), version); - break; - } - } catch (Exception e) { - getLogger().debug("Could not find property {}", prop); - } - } - } - - if (getProject().getExtensions().findByName("loom") != null) { - // Use the same method Loom uses to get the version. - // https://github.com/FabricMC/fabric-loom/blob/97f594da8e132c3d33cf39fe8d7cc0e76d84aeb6/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java#LL60C26-L60C56 - String version = getProject().getConfigurations().getByName("minecraft") - .getDependencies().iterator().next().getVersion(); - - if (version != null) { - getLogger().debug("Adding fallback game version {} from Loom.", version); - add(ext.getGameVersions(), version); - } - } - - if (getProject().getExtensions().findByName("paperweight") != null) { - String mcVer = getProject().getExtensions().getByType(PaperweightUserExtension.class).getMinecraftVersion().get(); - getLogger().debug("Adding fallback game version {} from paperweight-userdev.", mcVer); - add(ext.getGameVersions(), mcVer); - } - } + Map files = gatherFilesToUpload(); + ModrinthAPI api = api(getLogger(), getApiSettings()); - if (ext.getGameVersions().get().isEmpty()) { - throw new GradleException("Cannot upload to Modrinth: no game versions specified!"); - } + String slug = getProjectId().get(); + String id = getMyIdFromApi(api, slug); + getLogger().debug("Uploading version to project {}", id); // Convert each of our proto-dependencies to a proper Modrinth4J ProjectDependency - List protoDependencies = new ArrayList<>(); - List dependencies = new ArrayList<>(); - protoDependencies.addAll(ext.getNamedDependenciesAsList()); - protoDependencies.addAll(ext.getDependencies().get()); - protoDependencies.stream().map(dependency -> dependency.toNew(api)).forEach(dependencies::add); - - // Get each of the files, starting with the primary file - Map files = new LinkedHashMap<>(); - files.put(ext.getFile().get().getAsFile(), "primary"); - - // Convert each of the Object files from the extension to a proper File - ext.getAdditionalFiles().get().forEach(file -> { - File resolvedFile = resolveFile(getProject(), file); - - // Ensure the file actually exists before trying to upload it. - if (resolvedFile == null || !resolvedFile.exists()) { - throw new GradleException("The upload file is missing or null. " + file); - } - - String fileName = resolvedFile.getName(); - String fileType = null; - - // No switches in Java 8 :( - if (fileName.contains("-dev.jar")) { - fileType = "dev-jar"; - } else if (fileName.contains("-sources.jar")) { - fileType = "sources-jar"; - } else if (fileName.contains("-javadoc.jar")) { - fileType = "javadoc-jar"; - } else if (fileName.contains("asc") || fileName.contains("gpg") || fileName.contains("sig")) { - fileType = "signature"; - } - - files.put(resolvedFile, fileType); - }); - - ext.getAdditionalFileDsl().getNamedAdditionalFilesAsList().forEach(file -> - files.put(file.getFile().getAsFile(), file.getAdditionalFileType().toString())); + List dependencies = getDependencies().get().stream() + .map(dependency -> dependency.toNew(api)) + .collect(Collectors.toList()); // Start construction of the actual request! TemporaryCreateVersionRequest data = TemporaryCreateVersionRequest.builder() .projectId(id) - .versionNumber(versionNumber) - .name(ext.getVersionName().get()) - .changelog(ext.getChangelog().get().replaceAll("\r\n", "\n")) - .versionType(VersionType.valueOf(ext.getVersionType().get().toUpperCase(Locale.ROOT))) - .gameVersions(ext.getGameVersions().get()) - .loaders(ext.getLoaders().get()) + .versionNumber(getVersionNumber().get()) + .name(getVersionName().get()) + .changelog(getChangelog().get().replace("\r\n", "\n")) + .versionType(versionType) + .gameVersions(getGameVersions().get()) + .loaders(getLoaders().get()) .dependencies(dependencies) .files(files) .build(); // Return early in debug mode - if (ext.getDebugMode().get()) { + if (getIsDryRun().get()) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); getLogger().lifecycle("Full data to be sent for upload: {}", gson.toJson(data)); + getLogger().lifecycle("Files to be uploaded: {}", data.getFileNames().stream().collect(Collectors.joining(", "))); getLogger().lifecycle("Minotaur debug mode is enabled. Not going to upload this version."); return; } // Execute the request - ProjectVersion version = new TemporaryCreateVersion(getProject()).sendRequest(data).join(); + ProjectVersion version = new TemporaryCreateVersion(getLogger(), getApiSettings()) + .sendRequest(data).join(); //ProjectVersion version = api.versions().createProjectVersion(data).join(); newVersion = version; //noinspection deprecation @@ -268,25 +226,94 @@ && getProject().getExtensions().findByName("loom") != null) { newVersion.getId(), String.format( "%s/project/%s/version/%s", - ext.getApiUrl().get().replaceFirst("-?api", "").replaceFirst("/?v2/?", "").replaceFirst("//\\.", "//"), + getApiSettings().getApiUrl().get().replaceFirst("-?api", "").replaceFirst("/?v2/?", "").replaceFirst("//\\.", "//"), id, newVersion.getId() ) ); } catch (Exception e) { - if (ext.getFailSilently().get()) { + if (getFailSilently().get()) { getLogger().info("Failed to upload to Modrinth. Check logs for more info."); getLogger().error("Modrinth upload failed silently.", e); + } else if (e instanceof GradleException) { + throw (GradleException) e; } else { throw new GradleException("Failed to upload file to Modrinth! " + e.getMessage(), e); } } } - // avoid adding duplicates to `ListProperty`s - private static void add(final ListProperty list, final T element) { - if (!list.get().contains(element)) { - list.add(element); + private void validateInputs() { + if (getLoaders().get().isEmpty()) { + throw new InvalidUserDataException("Cannot upload to Modrinth: no loaders specified!"); + } + + if (getGameVersions().get().isEmpty()) { + throw new InvalidUserDataException("Cannot upload to Modrinth: no game versions specified!"); + } + } + + private VersionType getAndValidateVersionType() { + VersionType versionType; + try { + versionType = VersionType.valueOf(getVersionType().get().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new InvalidUserDataException("Cannot upload to Modrinth: invalid version type specified: " + getVersionType().get(), e); + } + return versionType; + } + + private String getMyIdFromApi(ModrinthAPI api, String slug) { + String id = api.projects().getProjectIdBySlug(slug).join(); + if (id == null) { + if (getIsDryRun().get()) { + getLogger().error("Cannot find project with id '{}'.", slug); + id = ""; + } else { + throw new GradleException(String.format("Cannot find project with id '%s'", slug)); + } + } + return id; + } + + private Map gatherFilesToUpload() { + // Get each of the files, starting with the primary file + Map files = new LinkedHashMap<>(); + files.put(getFile().get().getAsFile(), "primary"); + + // Convert each of the Object files from the extension to a proper File + getUntypedAdditionalFiles().forEach(resolvedFile -> { + String fileType = guessUploadFileType(resolvedFile.getName()); + files.put(resolvedFile, fileType); + }); + + getAdditionalFiles().get().forEach(typedFiles -> { + String type = typedFiles.getType().get().toString(); + typedFiles.getFiles().forEach(file -> files.put(file, type)); + }); + + List missingFiles = files.keySet().stream() + .filter(file -> !file.isFile()) + .collect(Collectors.toList()); + if (!missingFiles.isEmpty()) { + throw new GradleException("Missing some of the files we need to upload: " + missingFiles); + } + return files; + } + + private static @Nullable String guessUploadFileType(String fileName) { + String fileType = null; + + // No switches in Java 8 :( + if (fileName.contains("-dev.jar")) { + fileType = "dev-jar"; + } else if (fileName.contains("-sources.jar")) { + fileType = "sources-jar"; + } else if (fileName.contains("-javadoc.jar")) { + fileType = "javadoc-jar"; + } else if (fileName.contains("asc") || fileName.contains("gpg") || fileName.contains("sig")) { + fileType = "signature"; } + return fileType; } } diff --git a/src/main/java/com/modrinth/minotaur/Util.java b/src/main/java/com/modrinth/minotaur/Util.java index d30a30c..01ad5e3 100644 --- a/src/main/java/com/modrinth/minotaur/Util.java +++ b/src/main/java/com/modrinth/minotaur/Util.java @@ -1,16 +1,10 @@ package com.modrinth.minotaur; +import com.modrinth.minotaur.request.ModrinthApiSettings; import masecla.modrinth4j.client.agent.UserAgent; import masecla.modrinth4j.main.ModrinthAPI; -import org.gradle.api.Project; -import org.gradle.api.file.RegularFile; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.File; +import org.slf4j.Logger; /** * Internal utility methods to make things easier and deduplicated @@ -18,113 +12,37 @@ @ApiStatus.Internal public class Util { /** - * @param project Gradle project for getting various info from * @return A valid {@link ModrinthAPI} instance */ - static ModrinthAPI api(Project project) { - ModrinthExtension ext = ext(project); - String url = ext.getApiUrl().get(); + static ModrinthAPI api(Logger logger, ModrinthApiSettings settings) { + validateToken(logger, settings.getToken().get()); + return ModrinthAPI.rateLimited( + buildUserAgent(settings), + stripTrailingSlash(settings.getApiUrl().get()), + settings.getToken().get()); + } + + public static String stripTrailingSlash(String url) { if (url.endsWith("/")) { url = url.substring(0, url.length() - 1); } + return url; + } - UserAgent agent = UserAgent.builder() + public static UserAgent buildUserAgent(ModrinthApiSettings settings) { + return UserAgent.builder() .authorUsername("modrinth") .projectName("minotaur") .projectVersion(Util.class.getPackage().getImplementationVersion()) - .contact(ext.getProjectId().get() + "/" + resolveVersionNumber(project)) + .contact(settings.getProjectId().get() + "/" + settings.getVersionNumber().get()) .build(); + } - String token = ext.getToken().get(); + public static void validateToken(Logger logger, String token) { if (token.startsWith("mra")) { throw new RuntimeException("Token must be a personal-access token, not a session token!"); } else if (!token.startsWith("mrp")) { - project.getLogger().warn("Using GitHub tokens for authentication is deprecated. Please begin to use personal-access tokens."); + logger.warn("Using GitHub tokens for authentication is deprecated. Please begin to use personal-access tokens."); } - - return ModrinthAPI.rateLimited(agent, url, token); - } - - /** - * @param project Gradle project for getting various info from - * @return The {@link ModrinthExtension} for the project - */ - public static ModrinthExtension ext(Project project) { - return project.getExtensions().getByType(ModrinthExtension.class); - } - - /** - * Safely resolves the version number. - * - * @param project The Gradle project to resolve the extension and version from - * @return The extension version number if set; otherwise, the Gradle project version. - */ - public static String resolveVersionNumber(Project project) { - ModrinthExtension ext = ext(project); - if (ext.getVersionNumber().getOrNull() == null) { - ext.getVersionNumber().set(project.getVersion().toString()); - } - return ext.getVersionNumber().get(); - } - - /** - * Attempts to resolve a file using an arbitrary object provided by a user defined gradle - * task. - * - * @param in The arbitrary input object from the user. - * @return A file handle for the resolved input. If the input can not be resolved this will be null or the fallback. - */ - @Nullable - static File resolveFile(Project project, Object in) { - if (in == null) { - // If input is null we can't really do anything... - return null; - } else if (in instanceof File) { - // If the file is a Java file handle no additional handling is needed. - return (File) in; - } else if (in instanceof AbstractArchiveTask) { - // Grabs the file from an archive task. Allows build scripts to do things like the jar task directly. - return ((AbstractArchiveTask) in).getArchiveFile().get().getAsFile(); - } else if (in instanceof TaskProvider) { - // Grabs the file from an archive task wrapped in a provider. Allows Kotlin DSL buildscripts to also specify - // the jar task directly, rather than having to call #get() before running. - Object provided = ((TaskProvider) in).get(); - - // Check to see if the task provided is actually an AbstractArchiveTask. - if (provided instanceof AbstractArchiveTask) { - return ((AbstractArchiveTask) provided).getArchiveFile().get().getAsFile(); - } - } - - // None of the previous checks worked. Fall back to Gradle's built-in file resolution mechanics. - return project.file(in); - } - - public static Provider resolveFileProperty(Project project, Object in) { - if (in == null) { - // If input is null we can't really do anything... - return project.getObjects().fileProperty(); - } else if (in instanceof File) { - // If the file is a Java file handle no additional handling is needed. - return project.getLayout().file(project.provider(() -> (File) in)); - } else if (in instanceof AbstractArchiveTask) { - // Grabs the file from an archive task. Allows build scripts to do things like the jar task directly. - return ((AbstractArchiveTask) in).getArchiveFile(); - } else if (in instanceof TaskProvider) { - // Grabs the file from an archive task wrapped in a provider. Allows Kotlin DSL buildscripts to also specify - // the jar task directly, rather than having to call #get() before running. - Object provided = ((TaskProvider) in).get(); - - return ((TaskProvider) in).flatMap(task -> { - // Check to see if the task provided is actually an AbstractArchiveTask. - if (provided instanceof AbstractArchiveTask) { - return ((AbstractArchiveTask) provided).getArchiveFile(); - } - return project.getLayout().file(project.provider(() -> project.file(in))); - }); - } - - // None of the previous checks worked. Fall back to Gradle's built-in file resolution mechanics. - return project.getLayout().file(project.provider(() -> project.file(in))); } } diff --git a/src/main/java/com/modrinth/minotaur/additionalfiles/AdditionalFileDSL.java b/src/main/java/com/modrinth/minotaur/additionalfiles/AdditionalFileDSL.java index 57524ec..00707e7 100644 --- a/src/main/java/com/modrinth/minotaur/additionalfiles/AdditionalFileDSL.java +++ b/src/main/java/com/modrinth/minotaur/additionalfiles/AdditionalFileDSL.java @@ -1,39 +1,21 @@ package com.modrinth.minotaur.additionalfiles; -import com.modrinth.minotaur.Util; -import org.gradle.api.NamedDomainObjectContainer; -import org.gradle.api.Project; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; /** * the Nested AdditionalFiles configuration */ -public class AdditionalFileDSL { - private final Project project; - private final NamedDomainObjectContainer additionalFiles; - - /** - * Instantiates a new additionalFiles configuration. - * - * @param project Project - */ +public abstract class AdditionalFileDSL { @Inject - public AdditionalFileDSL(final Project project) { - this.project = project; - this.additionalFiles = project.getObjects().domainObjectContainer(NamedAdditionalFile.class); - } + protected abstract ObjectFactory getObjects(); /** - * Returns the complete NamedAdditionalFile container set mapped and collected as a {@literal List} - * - * @return {@literal List} + * @return additional files to be uploaded alongside the main file */ - public List getNamedAdditionalFilesAsList() { - return new ArrayList<>(this.additionalFiles); - } + public abstract ListProperty getAdditionalFiles(); /** * Creates a required resource pack AdditionalFile Container @@ -41,7 +23,7 @@ public List getNamedAdditionalFilesAsList() { * @param file the file */ public void requiredResourcePack(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.REQUIRED_RESOURCE_PACK, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.REQUIRED_RESOURCE_PACK, file); } /** @@ -50,7 +32,7 @@ public void requiredResourcePack(final Object file) { * @param file the file */ public void optionalResourcePack(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.OPTIONAL_RESOURCE_PACK, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.OPTIONAL_RESOURCE_PACK, file); } /** @@ -59,7 +41,7 @@ public void optionalResourcePack(final Object file) { * @param file the file */ public void sourcesJar(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.SOURCES_JAR, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.SOURCES_JAR, file); } /** @@ -68,7 +50,7 @@ public void sourcesJar(final Object file) { * @param file the file */ public void devJar(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.DEV_JAR, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.DEV_JAR, file); } /** @@ -77,7 +59,7 @@ public void devJar(final Object file) { * @param file the file */ public void javadocJar(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.JAVADOC_JAR, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.JAVADOC_JAR, file); } /** @@ -86,7 +68,7 @@ public void javadocJar(final Object file) { * @param file the file */ public void signature(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.SIGNATURE, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.SIGNATURE, file); } /** @@ -95,6 +77,13 @@ public void signature(final Object file) { * @param file the file */ public void other(final Object file) { - this.additionalFiles.add(new NamedAdditionalFile(AdditionalFileType.OTHER, Util.resolveFileProperty(project, file).get())); + addTyped(AdditionalFileType.OTHER, file); + } + + private void addTyped(AdditionalFileType type, Object file) { + TypedFileCollection namedFiles = getObjects().newInstance(TypedFileCollection.class); + namedFiles.getType().set(type); + namedFiles.getFiles().from(file); + this.getAdditionalFiles().add(namedFiles); } } diff --git a/src/main/java/com/modrinth/minotaur/additionalfiles/NamedAdditionalFile.java b/src/main/java/com/modrinth/minotaur/additionalfiles/NamedAdditionalFile.java deleted file mode 100644 index a9e6139..0000000 --- a/src/main/java/com/modrinth/minotaur/additionalfiles/NamedAdditionalFile.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.modrinth.minotaur.additionalfiles; - -import org.gradle.api.Named; -import org.gradle.api.file.RegularFile; -import org.jetbrains.annotations.NotNull; - -/** - * Defines a Named AdditionalFile for our NamedAdditionalFileContainer. - */ -public class NamedAdditionalFile implements Named { - private final AdditionalFileType additionalFileType; - private final RegularFile file; - - /** - * Instantiates a new NamedAdditionalFile. - * - * @param additionalFileType the AdditionalFileType - * @param file the file to upload - */ - protected NamedAdditionalFile(AdditionalFileType additionalFileType, RegularFile file) { - this.additionalFileType = additionalFileType; - this.file = file; - } - - /** - * @return the file name - */ - @NotNull - @Override - public String getName() { - return this.file.getAsFile().getName(); - } - - public RegularFile getFile() { - return file; - } - - /** - * Gets the AdditionalFileType as String. - * - * @return the type - */ - public AdditionalFileType getAdditionalFileType() { - return this.additionalFileType; - } -} diff --git a/src/main/java/com/modrinth/minotaur/additionalfiles/TypedFileCollection.java b/src/main/java/com/modrinth/minotaur/additionalfiles/TypedFileCollection.java new file mode 100644 index 0000000..eb190ff --- /dev/null +++ b/src/main/java/com/modrinth/minotaur/additionalfiles/TypedFileCollection.java @@ -0,0 +1,23 @@ +package com.modrinth.minotaur.additionalfiles; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; + +/** + * Defines a set of additional files to be uploaded to a Modrinth project, along with their type. + */ +public interface TypedFileCollection { + /** + * @return files to be uploaded + */ + @InputFiles + ConfigurableFileCollection getFiles(); + + /** + * @return the type of these files + */ + @Input + Property getType(); +} diff --git a/src/main/java/com/modrinth/minotaur/dependencies/Dependency.java b/src/main/java/com/modrinth/minotaur/dependencies/Dependency.java index c03d6fa..10b7afd 100644 --- a/src/main/java/com/modrinth/minotaur/dependencies/Dependency.java +++ b/src/main/java/com/modrinth/minotaur/dependencies/Dependency.java @@ -9,13 +9,14 @@ import org.gradle.api.GradleException; import org.jetbrains.annotations.ApiStatus; +import java.io.Serializable; import java.util.Locale; import java.util.Objects; /** * Represents the superclass for {@link ModDependency} and {@link VersionDependency}. */ -public class Dependency { +public class Dependency implements Serializable { /** * The {@link ProjectDependencyType} of the dependency. diff --git a/src/main/java/com/modrinth/minotaur/dependencies/container/DependencyDSL.java b/src/main/java/com/modrinth/minotaur/dependencies/container/DependencyDSL.java index 551d780..ac9cede 100644 --- a/src/main/java/com/modrinth/minotaur/dependencies/container/DependencyDSL.java +++ b/src/main/java/com/modrinth/minotaur/dependencies/container/DependencyDSL.java @@ -1,8 +1,8 @@ package com.modrinth.minotaur.dependencies.container; import com.modrinth.minotaur.dependencies.Dependency; -import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; import javax.inject.Inject; import java.util.List; @@ -12,7 +12,7 @@ * the Nested Dependencies configuration */ public class DependencyDSL { - private final NamedDomainObjectContainer dependencies; + private final ListProperty dependencies; private final NamedDependencyContainer.Incompatible incompatible; private final NamedDependencyContainer.Optional optional; private final NamedDependencyContainer.Required required; @@ -25,7 +25,7 @@ public class DependencyDSL { */ @Inject protected DependencyDSL(final ObjectFactory objects) { - this.dependencies = objects.domainObjectContainer(NamedDependency.class); + this.dependencies = objects.listProperty(NamedDependency.class).empty(); this.incompatible = objects.newInstance(NamedDependencyContainer.Incompatible.class, dependencies); this.optional = objects.newInstance(NamedDependencyContainer.Optional.class, dependencies); this.required = objects.newInstance(NamedDependencyContainer.Required.class, dependencies); @@ -36,46 +36,43 @@ protected DependencyDSL(final ObjectFactory objects) { * Returns the complete NamedDependency container set mapped and collected as a {@literal List} * * @return {@literal List} + * @deprecated this forces eager evaluation; use {@link #getNamedDependencies()} instead */ + @Deprecated public List getNamedDependenciesAsList() { - return this.dependencies.stream().map(NamedDependency::getDependency).collect(Collectors.toList()); + return this.dependencies.get().stream().map(NamedDependency::getDependency).collect(Collectors.toList()); } /** - * Retrieve the reference to an {@link NamedDependencyContainer.Incompatible} instance. - * Provided as a utility method for external uses. - * - * @return incompatible {@link NamedDependencyContainer.Incompatible} + * @return the complete NamedDependency container set + */ + public ListProperty getNamedDependencies() { + return this.dependencies; + } + + /** + * @return the dependency container for incompatible dependencies */ public NamedDependencyContainer.Incompatible getIncompatible() { return this.incompatible; } /** - * Retrieve the reference to an {@link NamedDependencyContainer.Optional} instance. - * Provided as a utility method for external uses. - * - * @return optional {@link NamedDependencyContainer.Optional} + * @return the dependency container for optional dependencies */ public NamedDependencyContainer.Optional getOptional() { return this.optional; } /** - * Retrieve the reference to an {@link NamedDependencyContainer.Required} instance. - * Provided as a utility method for external uses. - * - * @return required {@link NamedDependencyContainer.Required} + * @return the dependency container for required dependencies */ public NamedDependencyContainer.Required getRequired() { return this.required; } /** - * Retrieve the reference to an {@link NamedDependencyContainer.Embedded} instance. - * Provided as a utility method for external uses. - * - * @return embedded {@link NamedDependencyContainer.Embedded} + * @return the dependency container for embedded dependencies */ public NamedDependencyContainer.Embedded getEmbedded() { return this.embedded; diff --git a/src/main/java/com/modrinth/minotaur/dependencies/container/NamedDependencyContainer.java b/src/main/java/com/modrinth/minotaur/dependencies/container/NamedDependencyContainer.java index bee604d..be512cb 100644 --- a/src/main/java/com/modrinth/minotaur/dependencies/container/NamedDependencyContainer.java +++ b/src/main/java/com/modrinth/minotaur/dependencies/container/NamedDependencyContainer.java @@ -1,31 +1,25 @@ package com.modrinth.minotaur.dependencies.container; import com.modrinth.minotaur.dependencies.DependencyType; -import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.provider.ListProperty; import javax.inject.Inject; /** - * The root NamedDependencyContainer class + * A proxy to a dependency collection that exposes a DSL for adding dependencies by project ID or version ID. */ public class NamedDependencyContainer { - private final NamedDomainObjectContainer dependencyContainer; + private final ListProperty dependencyContainer; private final DependencyType dependencyType; - /** - * Instantiates a new Dependency object. - * - * @param container {@literal NamedDomainObjectContainer} - * @param dependencyType {@link DependencyType} - */ @Inject - protected NamedDependencyContainer(NamedDomainObjectContainer container, DependencyType dependencyType) { - this.dependencyContainer = container; + protected NamedDependencyContainer(ListProperty dependencyContainer, DependencyType dependencyType) { + this.dependencyContainer = dependencyContainer; this.dependencyType = dependencyType; } /** - * Creates an incompatible Dependency Container and applies the projectId property + * Adds dependencies to this container by project ID * * @param projectIds the project id(s) */ @@ -36,7 +30,7 @@ public void project(final String... projectIds) { } /** - * Creates a incompatible Dependency Container and applies the versionId property + * Adds dependencies to this container by version ID * * @param versionIds the version id(s) */ @@ -47,7 +41,7 @@ public void version(final String... versionIds) { } /** - * Creates a incompatible Dependency Container and applies the versionId property + * Adds a dependency to this container by project ID and version ID * * @param projectId the project id * @param versionId the version number @@ -56,62 +50,30 @@ public void version(final String projectId, final String versionId) { this.dependencyContainer.add(new NamedDependency(projectId, versionId, this.dependencyType)); } - /** - * Incompatible DependencyType container class - */ public static class Incompatible extends NamedDependencyContainer { - /** - * Instantiates a new incompatible object. - * - * @param container {@literal NamedDomainObjectContainer} - */ @Inject - public Incompatible(NamedDomainObjectContainer container) { + public Incompatible(ListProperty container) { super(container, DependencyType.INCOMPATIBLE); } } - /** - * Optional DependencyType container class - */ public static class Optional extends NamedDependencyContainer { - /** - * Instantiates a new optional object. - * - * @param container {@literal NamedDomainObjectContainer} - */ @Inject - public Optional(NamedDomainObjectContainer container) { + public Optional(ListProperty container) { super(container, DependencyType.OPTIONAL); } } - /** - * Required DependencyType container class - */ public static class Required extends NamedDependencyContainer { - /** - * Instantiates a new required object. - * - * @param container {@literal NamedDomainObjectContainer} - */ @Inject - public Required(NamedDomainObjectContainer container) { + public Required(ListProperty container) { super(container, DependencyType.REQUIRED); } } - /** - * Embedded DependencyType container class - */ public static class Embedded extends NamedDependencyContainer { - /** - * Instantiates a new required object. - * - * @param container {@literal NamedDomainObjectContainer} - */ @Inject - public Embedded(NamedDomainObjectContainer container) { + public Embedded(ListProperty container) { super(container, DependencyType.EMBEDDED); } } diff --git a/src/main/java/com/modrinth/minotaur/gameversion/GameVersionDetection.java b/src/main/java/com/modrinth/minotaur/gameversion/GameVersionDetection.java new file mode 100644 index 0000000..c9f63ee --- /dev/null +++ b/src/main/java/com/modrinth/minotaur/gameversion/GameVersionDetection.java @@ -0,0 +1,68 @@ +package com.modrinth.minotaur.gameversion; + +import io.papermc.paperweight.userdev.PaperweightUserExtension; +import org.gradle.api.Project; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.gradle.api.plugins.PluginManager; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; + +import java.util.*; + +@ApiStatus.Internal +public class GameVersionDetection { + private GameVersionDetection() { + throw new UnsupportedOperationException(); + } + + public static List detectGameVersions(Project project) { + Logger logger = project.getLogger(); + PluginManager pluginManager = project.getPluginManager(); + + LinkedHashSet versions = new LinkedHashSet<>(); + + if (pluginManager.hasPlugin("net.minecraftforge.gradle") || + pluginManager.hasPlugin("net.neoforged.gradle") || + pluginManager.hasPlugin("net.neoforged.gradle.userdev")) { + + String[] props = {"MC_VERSION", "minecraftVersion"}; + + ExtraPropertiesExtension extraProperties = project.getExtensions().getExtraProperties(); + for (String prop : props) { + try { + String version = (String) extraProperties.get(prop); + if (version != null) { + logger.debug("Adding fallback game version {} from ForgeGradle/NeoGradle.", version); + versions.add(version); + break; + } + } catch (Exception e) { + logger.debug("Could not find property {}", prop); + } + } + } + + if (project.getExtensions().findByName("loom") != null) { + // Get the version from the first dependency in the "minecraft" configuration, similar to how Loom does it. + // https://github.com/FabricMC/fabric-loom/blob/97f594da8e132c3d33cf39fe8d7cc0e76d84aeb6/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java#LL60C26-L60C56 + Optional.ofNullable(project.getConfigurations().findByName("minecraft")) + .map(m -> m.getDependencies().iterator()) + .filter(Iterator::hasNext) + .map(i -> i.next().getVersion()) + .ifPresent(version -> { + project.getLogger().debug("Adding fallback game version {} from Loom.", version); + versions.add(version); + }); + } + + if (project.getExtensions().findByName("paperweight") != null) { + String mcVer = project.getExtensions().getByType(PaperweightUserExtension.class).getMinecraftVersion().getOrNull(); + if (mcVer != null) { + logger.debug("Adding fallback game version {} from paperweight-userdev.", mcVer); + versions.add(mcVer); + } + } + + return new ArrayList<>(versions); + } +} diff --git a/src/main/java/com/modrinth/minotaur/loader/LoaderDetection.java b/src/main/java/com/modrinth/minotaur/loader/LoaderDetection.java new file mode 100644 index 0000000..672f85c --- /dev/null +++ b/src/main/java/com/modrinth/minotaur/loader/LoaderDetection.java @@ -0,0 +1,56 @@ +package com.modrinth.minotaur.loader; + +import org.gradle.api.Project; +import org.gradle.api.plugins.PluginManager; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; + +import java.util.*; + +@ApiStatus.Internal +public class LoaderDetection { + private static final LinkedHashMap pluginLoaderMap = new LinkedHashMap<>(); + + static { + pluginLoaderMap.put("net.minecraftforge.gradle", "forge"); + pluginLoaderMap.put("net.neoforged.gradle", "neoforge"); + pluginLoaderMap.put("net.neoforged.gradle.userdev", "neoforge"); + pluginLoaderMap.put("net.neoforged.moddev", "neoforge"); + pluginLoaderMap.put("net.neoforged.moddev.legacyforge", "forge"); + pluginLoaderMap.put("org.quiltmc.loom", "quilt"); + pluginLoaderMap.put("org.spongepowered.gradle.plugin", "sponge"); + pluginLoaderMap.put("io.papermc.paperweight.userdev", "paper"); + pluginLoaderMap.put("xyz.jpenilla.run-paper", "paper"); + pluginLoaderMap.put("xyz.jpenilla.run-waterfall", "waterfall"); + pluginLoaderMap.put("xyz.jpenilla.run-velocity", "velocity"); + } + + private LoaderDetection() { + throw new UnsupportedOperationException(); + } + + public static List detectLoaders(Project project) { + Set loaders = new LinkedHashSet<>(); + PluginManager pluginManager = project.getPluginManager(); + Logger logger = project.getLogger(); + pluginLoaderMap.forEach((plugin, loader) -> { + if (pluginManager.hasPlugin(plugin) && loaders.add(loader)) { + logger.debug("Adding loader '{}' because plugin '{}' was found.", loader, plugin); + } + }); + + if (!loaders.contains("quilt") // don't count quilt-loom twice + && project.getExtensions().findByName("loom") != null) { + Object loomPlatform = project.findProperty("loom.platform"); + if (loomPlatform instanceof String) { + logger.debug("Adding loader '{}' because 'loom' extension was found and loom.platform={}.", loomPlatform, loomPlatform); + loaders.add((String) loomPlatform); + } else { + logger.debug("Adding loader 'fabric' because 'loom' extension was found."); + loaders.add("fabric"); + } + } + + return new ArrayList<>(loaders); + } +} diff --git a/src/main/java/com/modrinth/minotaur/masecla/modrinth4j/endpoints/version/TemporaryCreateVersion.java b/src/main/java/com/modrinth/minotaur/masecla/modrinth4j/endpoints/version/TemporaryCreateVersion.java index 9b64015..1341cf3 100644 --- a/src/main/java/com/modrinth/minotaur/masecla/modrinth4j/endpoints/version/TemporaryCreateVersion.java +++ b/src/main/java/com/modrinth/minotaur/masecla/modrinth4j/endpoints/version/TemporaryCreateVersion.java @@ -16,30 +16,16 @@ */ package com.modrinth.minotaur.masecla.modrinth4j.endpoints.version; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.time.Instant; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import com.google.gson.*; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; - -import com.modrinth.minotaur.ModrinthExtension; import com.modrinth.minotaur.Util; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.modrinth.minotaur.request.ModrinthApiSettings; +import lombok.*; import lombok.Builder.Default; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.SneakyThrows; import masecla.modrinth4j.client.HttpClient; -import masecla.modrinth4j.client.agent.UserAgent; import masecla.modrinth4j.client.instances.RatelimitedHttpClient; import masecla.modrinth4j.endpoints.generic.Endpoint; import masecla.modrinth4j.model.adapters.ISOTimeAdapter; @@ -52,7 +38,17 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.Response; -import org.gradle.api.Project; +import org.slf4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; /** * This endpoint is used to create a new version. @@ -180,36 +176,20 @@ public TemporaryCreateVersionRequestBuilder files(List files) { /** * This constructor is used to create a new instance of the endpoint. */ - public TemporaryCreateVersion(Project project) { - super(httpClient(project), new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + public TemporaryCreateVersion(Logger logger, ModrinthApiSettings settings) { + super(httpClient(logger, settings), new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(FacetCollection.class, new FacetCollection.FacetAdapter()) .registerTypeAdapter(ModrinthPermissionMask.class, new ModrinthPermissionMask.ModrinthPermissionMaskAdapter()) .registerTypeAdapter(Instant.class, new ISOTimeAdapter()) .create()); } - private static HttpClient httpClient(Project project) { - ModrinthExtension ext = Util.ext(project); - String url = ext.getApiUrl().get(); - if (url.endsWith("/")) { - url = url.substring(0, url.length() - 1); - } - - UserAgent agent = UserAgent.builder() - .authorUsername("modrinth") - .projectName("minotaur") - .projectVersion(Util.class.getPackage().getImplementationVersion()) - .contact(ext.getProjectId().get() + "/" + Util.resolveVersionNumber(project)) - .build(); - - String token = ext.getToken().get(); - if (token.startsWith("mra")) { - throw new RuntimeException("Token must be a personal-access token, not a session token!"); - } else if (!token.startsWith("mrp")) { - project.getLogger().warn("Using GitHub tokens for authentication is deprecated. Please begin to use personal-access tokens."); - } - - return new RatelimitedHttpClient(agent, url, token); + private static HttpClient httpClient(Logger logger, ModrinthApiSettings settings) { + Util.validateToken(logger, settings.getToken().get()); + return new RatelimitedHttpClient( + Util.buildUserAgent(settings), + Util.stripTrailingSlash(settings.getApiUrl().get()), + settings.getToken().get()); } /** diff --git a/src/main/java/com/modrinth/minotaur/request/ModrinthApiSettings.java b/src/main/java/com/modrinth/minotaur/request/ModrinthApiSettings.java new file mode 100644 index 0000000..eed8de4 --- /dev/null +++ b/src/main/java/com/modrinth/minotaur/request/ModrinthApiSettings.java @@ -0,0 +1,36 @@ +package com.modrinth.minotaur.request; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +public interface ModrinthApiSettings { + /** + * This should not be changed unless you know what you're doing. Its main use case is for debug, development, or + * advanced user configurations. + * + * @return The URL used for communicating with Modrinth. + */ + @Input + Property getApiUrl(); + + /** + * Make sure you keep this private! + * + * @return The API token used to communicate with Modrinth. + */ + @Internal + Property getToken(); + + /** + * @return The ID of the project to upload the file to. + */ + @Input + Property getProjectId(); + + /** + * @return The version number of the project being uploaded. + */ + @Input + Property getVersionNumber(); +}