nimbus-gradle-plugin: Overhaul the plugin to support lazy configuration.
This is a substantial refactor of the plugin that should fix issues like bug 1854254 and bug 1856461, and speed up FML generation. These bugs had the same underlying cause: the plugin was trying to configure its tasks without observing the Gradle build lifecycle, leading to inconsistencies and accidental interdependencies. Further, because the old tasks didn't declare their inputs or outputs, Gradle couldn't "see" the full task graph, causing too much or too little to be rebuilt. The easiest way to force Gradle to pick up the changes used to be a full clobber. This commit resolves all those issues by: * Adopting Gradle's lazy configuration API [1], which lets Gradle track inputs and outputs, and avoids ordering issues caused by realizing the task graph at the wrong time [2]. As a bonus, we should be able to work much better with `afterEvaluate` [3] in m-c's Gradle scripts. * Adopting the new Android Gradle Plugin `SourceDirectories` API that supports lazy configuration [4], which replaces the deprecated `registerJavaGeneratingTask` API. * Adding task classes and custom types to help with naming and configuring inputs and outputs. [1]: https://docs.gradle.org/current/userguide/lazy_configuration.html [2]: https://docs.gradle.org/current/userguide/task_configuration_avoidance.html [3]: https://mbonnin.hashnode.dev/my-life-after-afterevaluate [4]: https://developer.android.com/reference/tools/gradle-api/8.4/com/android/build/api/variant/SourceDirectories
This commit is contained in:
Родитель
574be82135
Коммит
9197c0bdce
|
@ -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<FetchSpec> action) {
|
||||
action.execute(fetchSpec)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the task to extract the binary from the archive.
|
||||
*
|
||||
* @param action The configuration action.
|
||||
*/
|
||||
void unzip(Action<UnzipSpec> 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<SourceSpec> 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<SourceSpec> 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<String> getArchive()
|
||||
|
||||
@Input
|
||||
abstract Property<String> 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<String> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> getProjectDir()
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
abstract Property<String> 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String, String> getChannels()
|
||||
|
||||
String getChannelActual(variant) {
|
||||
Map<String, String> 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<String> 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<String> 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<String> getRepoFiles()
|
||||
|
||||
List<File> getRepoFilesActual(Project project) {
|
||||
var repoFiles = this.repoFiles.getOrNull() ?: new ArrayList<File>()
|
||||
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<String> 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<String> getApplicationServicesDir()
|
||||
|
||||
File getApplicationServicesDirActual(Project project) {
|
||||
var applicationServicesDir = this.applicationServicesDir.getOrNull()
|
||||
return applicationServicesDir ? project.file(applicationServicesDir) : null
|
||||
}
|
||||
}
|
||||
|
||||
class NimbusPlugin implements Plugin<Project> {
|
||||
|
||||
public static final String APPSERVICES_FML_HOME = "components/support/nimbus-fml"
|
||||
|
||||
void apply(Project project) {
|
||||
def extension = project.extensions.create('nimbus', NimbusPluginExtension)
|
||||
|
||||
Collection<Task> 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<Project> {
|
|||
* @param version
|
||||
* @return
|
||||
*/
|
||||
static File getFMLRoot(Project project, String version) {
|
||||
return project.layout.buildDirectory.dir("bin/nimbus/$version").get().asFile
|
||||
static Provider<Directory> getFMLRoot(Project project, String version) {
|
||||
return project.layout.buildDirectory.dir("bin/nimbus/$version")
|
||||
}
|
||||
|
||||
static def getArchOs() {
|
||||
|
@ -276,17 +209,13 @@ class NimbusPlugin implements Plugin<Project> {
|
|||
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<Project> {
|
|||
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<File> 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<File> 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..<a.length) {
|
||||
def na = Integer.parseInt(a[i])
|
||||
def nb = Integer.parseInt(b[i])
|
||||
if (na > nb) {
|
||||
return 1
|
||||
} else if (na < nb) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче