diff --git a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusAssembleToolsTask.groovy b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusAssembleToolsTask.groovy new file mode 100644 index 000000000..a21b2f867 --- /dev/null +++ b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusAssembleToolsTask.groovy @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.appservices.tooling.nimbus + +import org.gradle.api.Action +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.FileVisitDetails +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.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.LocalState +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +import javax.inject.Inject + +import groovy.transform.Immutable + +/** + * A task that fetches a prebuilt `nimbus-fml` binary for the current platform. + * + * Prebuilt binaries for all platforms are packaged into ZIP archives, and + * published to sources like `archive.mozilla.org` (for releases) or + * TaskCluster (for nightly builds). + * + * This task takes a variable number of inputs: a list of archive sources, + * and a list of glob patterns to find the binary for the current platform + * in the archive. + * + * The unzipped binary is this task's only output. This output is then used as + * an optional input to the `NimbusFmlCommandTask`s. + */ +@CacheableTask +abstract class NimbusAssembleToolsTask extends DefaultTask { + @Inject + abstract ArchiveOperations getArchiveOperations() + + @Nested + abstract FetchSpec getFetchSpec() + + @Nested + abstract UnzipSpec getUnzipSpec() + + /** The location of the fetched ZIP archive. */ + @LocalState + abstract RegularFileProperty getArchiveFile() + + /** + * The location of the fetched hash file, which contains the + * archive's checksum. + */ + @LocalState + abstract RegularFileProperty getHashFile() + + /** The location of the unzipped binary. */ + @OutputFile + abstract RegularFileProperty getFmlBinary() + + /** + * Configures the task to download the archive. + * + * @param action The configuration action. + */ + void fetch(Action action) { + action.execute(fetchSpec) + } + + /** + * Configures the task to extract the binary from the archive. + * + * @param action The configuration action. + */ + void unzip(Action action) { + action.execute(unzipSpec) + } + + @TaskAction + void assembleTools() { + def sources = [fetchSpec, *fetchSpec.fallbackSources.get()].collect { + new Source(new URI(it.archive.get()), new URI(it.hash.get())) + } + + def successfulSource = sources.find { it.trySaveArchiveTo(archiveFile.get().asFile) } + if (successfulSource == null) { + throw new GradleException("Couldn't fetch archive from any of: ${sources*.archiveURI.collect { "`$it`" }.join(', ')}") + } + + // We get the checksum, although don't do anything with it yet; + // Checking it here would be able to detect if the zip file was tampered with + // in transit between here and the server. + // It won't detect compromise of the CI server. + try { + successfulSource.saveHashTo(hashFile.get().asFile) + } catch (IOException e) { + throw new GradleException("Couldn't fetch hash from `${successfulSource.hashURI}`", e) + } + + def zipTree = archiveOperations.zipTree(archiveFile.get()) + def visitedFilePaths = [] + zipTree.matching { + include unzipSpec.includePatterns.get() + }.visit { FileVisitDetails details -> + if (!details.directory) { + if (visitedFilePaths.empty) { + details.copyTo(fmlBinary.get().asFile) + fmlBinary.get().asFile.setExecutable(true) + } + visitedFilePaths.add(details.relativePath) + } + } + + if (visitedFilePaths.size() > 1) { + throw new GradleException("Ambiguous unzip spec matched ${visitedFilePaths.size()} files in archive: ${visitedFilePaths.collect { "`$it`" }.join(', ')}") + } + } + + /** + * Specifies the source from which to fetch the archive and + * its hash file. + */ + static abstract class FetchSpec extends SourceSpec { + @Inject + abstract ObjectFactory getObjectFactory() + + @Nested + abstract ListProperty getFallbackSources() + + /** + * Configures a fallback to try if the archive can't be fetched + * from this source. + * + * The task will try fallbacks in the order in which they're + * configured. + * + * @param action The configuration action. + */ + void fallback(Action action) { + def spec = objectFactory.newInstance(SourceSpec) + action(spec) + fallbackSources.add(spec) + } + } + + /** Specifies the URL of an archive and its hash file. */ + static abstract class SourceSpec { + @Input + abstract Property getArchive() + + @Input + abstract Property getHash() + } + + /** + * Specifies which binary to extract from the fetched archive. + * + * The spec should only match one file in the archive. If the spec + * matches multiple files in the archive, the task will fail. + */ + static abstract class UnzipSpec { + @Input + abstract ListProperty getIncludePatterns() + + /** + * Includes all files whose paths match the pattern. + * + * @param pattern An Ant-style glob pattern. + * @see org.gradle.api.tasks.util.PatternFilterable#include + */ + void include(String pattern) { + includePatterns.add(pattern) + } + } + + /** A helper to fetch an archive and its hash file. */ + @Immutable + static class Source { + URI archiveURI + URI hashURI + + boolean trySaveArchiveTo(File destination) { + try { + saveURITo(archiveURI, destination) + true + } catch (IOException ignored) { + false + } + } + + void saveHashTo(File destination) { + saveURITo(hashURI, destination) + } + + private static void saveURITo(URI source, File destination) { + source.toURL().withInputStream { from -> + destination.withOutputStream { out -> + out << from + } + } + } + } +} diff --git a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFeaturesTask.groovy b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFeaturesTask.groovy new file mode 100644 index 000000000..d7a172a6a --- /dev/null +++ b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFeaturesTask.groovy @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.appservices.tooling.nimbus + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.LocalState +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.process.ExecSpec + +@CacheableTask +abstract class NimbusFeaturesTask extends NimbusFmlCommandTask { + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + abstract RegularFileProperty getInputFile() + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + abstract ConfigurableFileCollection getRepoFiles() + + @Input + abstract Property getChannel() + + @LocalState + abstract DirectoryProperty getCacheDir() + + @OutputDirectory + abstract DirectoryProperty getOutputDir() + + @Override + void configureFmlCommand(ExecSpec spec) { + spec.with { + args 'generate' + + args '--language', 'kotlin' + args '--channel', channel.get() + args '--cache-dir', cacheDir.get() + for (File file : repoFiles) { + args '--repo-file', file + } + + args inputFile.get().asFile + args outputDir.get().asFile + } + } +} diff --git a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFmlCommandTask.groovy b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFmlCommandTask.groovy new file mode 100644 index 000000000..99299c305 --- /dev/null +++ b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusFmlCommandTask.groovy @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.appservices.tooling.nimbus + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.gradle.process.ExecSpec + +import javax.inject.Inject + +/** + * A base task to execute a `nimbus-fml` command. + * + * Subclasses can declare additional inputs and outputs, and override + * `configureFmlCommand` to set additional command arguments. + * + * This task requires either `applicationServicesDir` to be set, or + * the `fmlBinary` to exist. If `applicationServicesDir` is set, + * the task will run `nimbus-fml` from the Application Services repo; + * otherwise, it'll fall back to a prebuilt `fmlBinary`. + */ +abstract class NimbusFmlCommandTask extends DefaultTask { + public static final String APPSERVICES_FML_HOME = 'components/support/nimbus-fml' + + @Inject + abstract ExecOperations getExecOperations() + + @Inject + abstract ProjectLayout getProjectLayout() + + @Input + abstract Property getProjectDir() + + @Input + @Optional + abstract Property getApplicationServicesDir() + + // `@InputFiles` instead of `@InputFile` because we don't want + // the task to fail if the `fmlBinary` file doesn't exist + // (https://github.com/gradle/gradle/issues/2016). + @InputFiles + @PathSensitive(PathSensitivity.NONE) + abstract RegularFileProperty getFmlBinary() + + /** + * Configures the `nimbus-fml` command for this task. + * + * This method is invoked from the `@TaskAction` during the execution phase, + * and so has access to the final values of the inputs and outputs. + * + * @param spec The specification for the `nimbus-fml` command. + */ + abstract void configureFmlCommand(ExecSpec spec) + + @TaskAction + void execute() { + execOperations.exec { spec -> + spec.with { + // Absolutize `projectDir`, so that we can resolve our paths + // against it. If it's already absolute, it'll be used as-is. + def projectDir = projectLayout.projectDirectory.dir(projectDir.get()) + def localAppServices = applicationServicesDir.getOrNull() + if (localAppServices == null) { + if (!fmlBinary.get().asFile.exists()) { + throw new GradleException("`nimbus-fml` wasn't downloaded and `nimbus.applicationServicesDir` isn't set") + } + workingDir projectDir + commandLine fmlBinary.get().asFile + } else { + def cargoManifest = projectDir.file("$localAppServices/$APPSERVICES_FML_HOME/Cargo.toml").asFile + + commandLine 'cargo' + args 'run' + args '--manifest-path', cargoManifest + args '--' + } + } + configureFmlCommand(spec) + } + } +} diff --git a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.groovy b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.groovy index 2cb67ccc4..1525deb15 100644 --- a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.groovy +++ b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.groovy @@ -4,17 +4,13 @@ package org.mozilla.appservices.tooling.nimbus -import org.gradle.api.Task -import org.gradle.api.provider.ListProperty - -import java.util.stream.Collectors -import java.util.zip.ZipFile -import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.Exec +import org.gradle.api.provider.Provider abstract class NimbusPluginExtension { /** @@ -27,11 +23,6 @@ abstract class NimbusPluginExtension { */ abstract Property getManifestFile() - File getManifestFileActual(Project project) { - var filename = this.manifestFile.getOrNull() ?: "nimbus.fml.yaml" - return project.file(filename) - } - /** * The mapping between the build variant and the release channel. * @@ -40,11 +31,6 @@ abstract class NimbusPluginExtension { */ abstract MapProperty getChannels() - String getChannelActual(variant) { - Map channels = this.channels.get() ?: new HashMap() - return channels.getOrDefault(variant.name, variant.name) - } - /** * The filename of the manifest ingested by Experimenter. * @@ -55,11 +41,6 @@ abstract class NimbusPluginExtension { */ abstract Property getExperimenterManifest() - File getExperimenterManifestActual(Project project) { - var filename = this.experimenterManifest.getOrNull() ?: ".experimenter.json" - return project.file(filename) - } - /** * The directory to which the generated files should be written. * @@ -69,11 +50,6 @@ abstract class NimbusPluginExtension { */ abstract Property getOutputDir() - File getOutputDirActual(Object variant, Project project) { - var outputDir = this.outputDir.getOrNull() ?: "generated/source/nimbus/${variant.name}/kotlin" - return project.layout.buildDirectory.dir(outputDir).get().asFile - } - /** * The file(s) containing the version(s)/ref(s)/location(s) for additional repositories. * @@ -83,13 +59,6 @@ abstract class NimbusPluginExtension { */ abstract ListProperty getRepoFiles() - List getRepoFilesActual(Project project) { - var repoFiles = this.repoFiles.getOrNull() ?: new ArrayList() - return repoFiles.stream().map(filename -> { - project.file(filename) - }).collect(Collectors.toList()) - } - /** * The directory where downloaded files are or where they should be cached. * @@ -99,11 +68,6 @@ abstract class NimbusPluginExtension { */ abstract Property getCacheDir() - File getCacheDirActual(Project project) { - var cacheDir = this.cacheDir.getOrNull() ?: "nimbus-cache" - return project.rootProject.layout.buildDirectory.dir(cacheDir).get().asFile - } - /** * The directory where a local installation of application services can be found. * @@ -113,127 +77,96 @@ abstract class NimbusPluginExtension { * @return */ abstract Property getApplicationServicesDir() - - File getApplicationServicesDirActual(Project project) { - var applicationServicesDir = this.applicationServicesDir.getOrNull() - return applicationServicesDir ? project.file(applicationServicesDir) : null - } } class NimbusPlugin implements Plugin { - public static final String APPSERVICES_FML_HOME = "components/support/nimbus-fml" - void apply(Project project) { def extension = project.extensions.create('nimbus', NimbusPluginExtension) - Collection oneTimeTasks = new ArrayList<>() - if (project.hasProperty("android")) { - if (project.android.hasProperty('applicationVariants')) { - project.android.applicationVariants.all { variant -> - setupVariantTasks(variant, project, extension, oneTimeTasks, false) - } - } + // Configure default values ("conventions") for our + // extension properties. + extension.manifestFile.convention('nimbus.fml.yaml') + extension.cacheDir.convention('nimbus-cache') - if (project.android.hasProperty('libraryVariants')) { - project.android.libraryVariants.all { variant -> - setupVariantTasks(variant, project, extension, oneTimeTasks, true) + def assembleToolsTask = setupAssembleNimbusTools(project) + + def validateTask = setupValidateTask(project) + validateTask.configure { + // Gradle tracks the dependency on the `nimbus-fml` binary that the + // `assembleNimbusTools` task produces implicitly; we don't need an + // explicit `dependsOn` here. + fmlBinary = assembleToolsTask.flatMap { it.fmlBinary } + } + + if (project.hasProperty('android')) { + // If the Android Gradle Plugin is configured, add the sources + // generated by the `nimbusFeatures{variant}` task to the sources + // for that variant. `variant.sources` is the modern, lazy + // replacement for the deprecated `registerJavaGeneratingTask` API. + def androidComponents = project.extensions.getByName('androidComponents') + androidComponents.onVariants(androidComponents.selector().all()) { variant -> + def generateTask = setupNimbusFeatureTasks(variant, project) + + generateTask.configure { + fmlBinary = assembleToolsTask.flatMap { it.fmlBinary } + dependsOn validateTask } + + variant.sources.java.addGeneratedSourceDirectory(generateTask) { it.outputDir } } } else { - setupVariantTasks([ + // Otherwise, if we aren't building for Android, add an explicit + // dependency on the `nimbusFeatures` task to each `*compile*` + // task. + def generateTask = setupNimbusFeatureTasks([ name: project.name - ], - project, extension, oneTimeTasks, true) + ], project) + + generateTask.configure { + fmlBinary = assembleToolsTask.flatMap { it.fmlBinary } + dependsOn validateTask + } + + project.tasks.named { + it.contains('compile') + }.configureEach { task -> + task.dependsOn generateTask + } } } - def setupAssembleNimbusTools(Project project, NimbusPluginExtension extension) { - return project.task("assembleNimbusTools") { + def setupAssembleNimbusTools(Project project) { + return project.tasks.register('assembleNimbusTools', NimbusAssembleToolsTask) { task -> group "Nimbus" description "Fetch the Nimbus FML tools from Application Services" - doLast { - if (extension.getApplicationServicesDirActual(project) == null) { - fetchNimbusBinaries(project) - } else { - println("Using local application services") - } - } - } - } - // Try one or more hosts to download the given file. - // Return the hostname that successfully downloaded, or null if none succeeded. - static def tryDownload(File directory, String filename, String[] urlPrefixes) { - return urlPrefixes.find { prefix -> - def urlString = filename == null ? prefix : "$prefix/$filename" - try { - new URL(urlString).withInputStream { from -> - new File(directory, filename).withOutputStream { out -> - out << from; - } - } - true - } catch (e) { - false - } - } - } + def asVersion = getProjectVersion() + def fmlRoot = getFMLRoot(project, asVersion) - // Fetches and extracts the pre-built nimbus-fml binaries - def fetchNimbusBinaries(Project project) { - def asVersion = getProjectVersion() + archiveFile = fmlRoot.map { it.file('nimbus-fml.zip') } + hashFile = fmlRoot.map { it.file('nimbus-fml.sha256') } + fmlBinary = fmlRoot.map { it.file(getFMLFile()) } - def fmlPath = getFMLFile(project, asVersion) - println("Checking fml binaries in $fmlPath") - if (fmlPath.exists()) { - println("nimbus-fml already exists at $fmlPath") - return - } - - def rootDirectory = getFMLRoot(project, asVersion) - def archive = new File(rootDirectory, "nimbus-fml.zip") - ensureDirExists(rootDirectory) - - if (!archive.exists()) { - println("Downloading nimbus-fml cli version $asVersion") - - def successfulHost = tryDownload(archive.getParentFile(), archive.getName(), + fetch { // Try archive.mozilla.org release first - "https://archive.mozilla.org/pub/app-services/releases/$asVersion", - // Try a github release next (TODO: remove this once we verify that publishing to - // archive.mozilla.org is working). - "https://github.com/mozilla/application-services/releases/download/v$asVersion", + archive = "https://archive.mozilla.org/pub/app-services/releases/$asVersion/nimbus-fml.zip" + hash = "https://archive.mozilla.org/pub/app-services/releases/$asVersion/nimbus-fml.sha256" + // Fall back to a nightly release - "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/project.application-services.v2.nimbus-fml.$asVersion/artifacts/public/build" - ) - - if (successfulHost == null) { - throw GradleException("Unable to download nimbus-fml tooling with version $asVersion.\n\nIf you are using a development version of the Nimbus Gradle Plugin, please set `applicationServicesDir` in your `build.gradle`'s nimbus block as the path to your local application services directory relative to your project's root.") - } else { - println("Downloaded nimbus-fml from $successfulHost") - } - - // We get the checksum, although don't do anything with it yet; - // Checking it here would be able to detect if the zip file was tampered with - // in transit between here and the server. - // It won't detect compromise of the CI server. - tryDownload(rootDirectory, "nimbus-fml.sha256", successfulHost) - } - - def archOs = getArchOs() - println("Unzipping binary, looking for $archOs/nimbus-fml") - def zipFile = new ZipFile(archive) - zipFile.entries().findAll { entry -> - return !entry.directory && entry.name.contains(archOs) - }.each { entry -> - fmlPath.withOutputStream { out -> - zipFile.getInputStream(entry).withStream { from -> - out << from + fallback { + archive = "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/project.application-services.v2.nimbus-fml.$asVersion/artifacts/public/build/nimbus-fml.zip" + hash = "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/project.application-services.v2.nimbus-fml.$asVersion/artifacts/public/build/nimbus-fml.sha256" } } - fmlPath.setExecutable(true) + unzip { + include "${getArchOs()}/release/nimbus-fml*" + } + + onlyIf('`applicationServicesDir` == null') { + project.nimbus.applicationServicesDir.getOrNull() == null + } } } @@ -244,8 +177,8 @@ class NimbusPlugin implements Plugin { * @param version * @return */ - static File getFMLRoot(Project project, String version) { - return project.layout.buildDirectory.dir("bin/nimbus/$version").get().asFile + static Provider getFMLRoot(Project project, String version) { + return project.layout.buildDirectory.dir("bin/nimbus/$version") } static def getArchOs() { @@ -276,17 +209,13 @@ class NimbusPlugin implements Plugin { return "${archPart}-${osPart}" } - static File getFMLFile(Project project, String version) { + static String getFMLFile() { String os = System.getProperty("os.name").toLowerCase() String binaryName = "nimbus-fml" if (os.contains("win")) { binaryName = "nimbus-fml.exe" } - return new File(getFMLRoot(project, version), binaryName) - } - - static String getFMLPath(Project project, String version) { - return getFMLFile(project, version).getPath() + return binaryName } String getProjectVersion() { @@ -296,163 +225,66 @@ class NimbusPlugin implements Plugin { return props.get("version") } - def setupVariantTasks(variant, project, extension, oneTimeTasks, isLibrary = false) { - def task = setupNimbusFeatureTasks(variant, project, extension) - - if (oneTimeTasks.isEmpty()) { - // The extension doesn't seem to be ready until now, so we have this complicated - // oneTimeTasks thing going on here. Ideally, we'd run this outside of this function. - def assembleToolsTask = setupAssembleNimbusTools(project, extension) - oneTimeTasks.add(assembleToolsTask) - - def validateTask = setupValidateTask(project, extension) - validateTask.dependsOn(assembleToolsTask) - oneTimeTasks.add(validateTask) - } - - // Generating experimenter manifest is cheap, for now. - // So we generate this every time. - // In the future, we should try and make this an incremental task. - oneTimeTasks.forEach {oneTimeTask -> - if (oneTimeTask != null) { - task.dependsOn(oneTimeTask) - } - } - } - - def setupNimbusFeatureTasks(variant, project, extension) { - String channel = extension.getChannelActual(variant) - File inputFile = extension.getManifestFileActual(project) - File outputDir = extension.getOutputDirActual(variant, project) - File cacheDir = extension.getCacheDirActual(project) - List repoFiles = extension.getRepoFilesActual(project) - - var generateTask = project.task("nimbusFeatures${variant.name.capitalize()}", type: Exec) { + def setupNimbusFeatureTasks(Object variant, Project project) { + return project.tasks.register("nimbusFeatures${variant.name.capitalize()}", NimbusFeaturesTask) { description = "Generate Kotlin data classes for Nimbus enabled features" group = "Nimbus" doFirst { - ensureDirExists(outputDir) - ensureDirExists(cacheDir) println("Nimbus FML generating Kotlin") - println("manifest $inputFile") - println("cache dir $cacheDir") - println("repo file(s) ${repoFiles.join(", ")}") - println("channel $channel") + println("manifest ${inputFile.get().asFile}") + println("cache dir ${cacheDir.get().asFile}") + println("repo file(s) ${repoFiles.files.join()}") + println("channel ${channel.get()}") } doLast { - println("outputFile $outputDir") + println("outputFile ${outputDir.get().asFile}") } - def localAppServices = extension.getApplicationServicesDirActual(project) - if (localAppServices == null) { - workingDir project.rootDir - commandLine getFMLPath(project, getProjectVersion()) - } else { - def cargoManifest = new File(localAppServices, "$APPSERVICES_FML_HOME/Cargo.toml") - - commandLine "cargo" - args "run" - args "--manifest-path", cargoManifest - args "--" + projectDir = project.rootDir.toString() + repoFiles = project.files(project.nimbus.repoFiles) + applicationServicesDir = project.nimbus.applicationServicesDir + inputFile = project.layout.projectDirectory.file(project.nimbus.manifestFile) + cacheDir = project.layout.buildDirectory.dir(project.nimbus.cacheDir).map { + // The `nimbusFeatures*` and `nimbusValidate` tasks can + // technically use the same cache directory, but Gradle + // discourages this, because such "overlapping outputs" + // inhibit caching and parallelization + // (https://github.com/gradle/gradle/issues/28394). + it.dir("features${variant.name.capitalize()}") } - args "generate" - args "--language", "kotlin" - args "--channel", channel - args "--cache-dir", cacheDir - for (File file : repoFiles) { - args "--repo-file", file - } - - args inputFile - args outputDir - - println args + channel = project.nimbus.channels.getting(variant.name).orElse(variant.name) + outputDir = project.layout.buildDirectory.dir("generated/source/nimbus/${variant.name}/kotlin") } - - if(variant.metaClass.respondsTo(variant, 'registerJavaGeneratingTask', Task, File)) { - variant.registerJavaGeneratingTask(generateTask, outputDir) - } - - def generateSourcesTask = project.tasks.findByName("generate${variant.name.capitalize()}Sources") - if (generateSourcesTask != null) { - generateSourcesTask.dependsOn(generateTask) - } else { - project.tasks.findAll().stream() - .filter({ task -> - return task.name.contains("compile") - }) - .forEach({ task -> - task.dependsOn(generateTask) - }) - } - - return generateTask } - def setupValidateTask(project, extension) { - File inputFile = extension.getManifestFileActual(project) - File cacheDir = extension.getCacheDirActual(project) - List repoFiles = extension.getRepoFilesActual(project) - - return project.task("nimbusValidate", type: Exec) { + def setupValidateTask(Project project) { + return project.tasks.register('nimbusValidate', NimbusValidateTask) { description = "Validate the Nimbus feature manifest for the app" group = "Nimbus" doFirst { - ensureDirExists(cacheDir) println("Nimbus FML: validating manifest") - println("manifest $inputFile") - println("cache dir $cacheDir") - println("repo file(s) ${repoFiles.join()}") + println("manifest ${inputFile.get().asFile}") + println("cache dir ${cacheDir.get().asFile}") + println("repo file(s) ${repoFiles.files.join()}") } - def localAppServices = extension.getApplicationServicesDirActual(project) - if (localAppServices == null) { - workingDir project.rootDir - commandLine getFMLPath(project, getProjectVersion()) - } else { - def cargoManifest = new File(localAppServices, "$APPSERVICES_FML_HOME/Cargo.toml") - - commandLine "cargo" - args "run" - args "--manifest-path", cargoManifest - args "--" - } - args "validate" - args "--cache-dir", cacheDir - for (File file : repoFiles) { - args "--repo-file", file + projectDir = project.rootDir.toString() + repoFiles = project.files(project.nimbus.repoFiles) + applicationServicesDir = project.nimbus.applicationServicesDir + inputFile = project.layout.projectDirectory.file(project.nimbus.manifestFile) + cacheDir = project.layout.buildDirectory.dir(project.nimbus.cacheDir).map { + it.dir('validate') } - args inputFile + // `nimbusValidate` doesn't have any outputs, so Gradle will always + // run it, even if its inputs haven't changed. This predicate tells + // Gradle to ignore the outputs, and only consider the inputs, for + // up-to-date checks. + outputs.upToDateWhen { true } } } - static def ensureDirExists(File dir) { - if (dir.exists()) { - if (!dir.isDirectory()) { - dir.delete() - dir.mkdirs() - } - } else { - dir.mkdirs() - } - } - - static def versionCompare(String versionA, String versionB) { - def a = versionA.split("\\.", 3) - def b = versionB.split("\\.", 3) - for (i in 0.. nb) { - return 1 - } else if (na < nb) { - return -1 - } - } - return 0 - } } diff --git a/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusValidateTask.groovy b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusValidateTask.groovy new file mode 100644 index 000000000..998d9a49e --- /dev/null +++ b/tools/nimbus-gradle-plugin/src/main/groovy/org/mozilla/appservices/tooling/nimbus/NimbusValidateTask.groovy @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.appservices.tooling.nimbus + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.LocalState +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.process.ExecSpec + +@CacheableTask +abstract class NimbusValidateTask extends NimbusFmlCommandTask { + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + abstract RegularFileProperty getInputFile() + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + abstract ConfigurableFileCollection getRepoFiles() + + @LocalState + abstract DirectoryProperty getCacheDir() + + @Override + void configureFmlCommand(ExecSpec spec) { + spec.with { + args 'validate' + + args '--cache-dir', cacheDir.get() + for (File file : repoFiles) { + args '--repo-file', file + } + + args inputFile.get() + } + } +}