Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
import groovy.json.JsonSlurper
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import javax.inject.Inject
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
// Loads the mach environment configurations into a gradle extension property.
//
// This script runs during Gradle configuration. Originally it called `./mach
// environment` to get the full build configuration, but that's expensive (often 2-3
// seconds). Now, `./mach configure` dumps the configuration to
// `config.status.json`, so we just need to know the topobjdir to read it.
// Determining the topobjdir without mach would require duplicating topobjdir
// resolution logic, so we run `./mach environment` to get just the topobjdir.
//
// We use two layers of caching to avoid running `./mach environment` unnecessarily:
//
// 1. Gradle's configuration cache (via ValueSource API): We declare all inputs that
// affect topobjdir resolution as ValueSource parameters. When these inputs are
// unchanged, Gradle reuses the entire cached configuration without calling obtain().
//
// 2. Local file cache (inside obtain()): When Gradle's config cache is invalidated
// for other reasons (e.g., build script changes), we still check our local cache
// before running `./mach environment`. This cache is stored in
// .gradle/mach-environment-cache/ and keyed by a hash of the same inputs.
//
// This means `./mach environment` only runs when inputs actually change.
def startTime = System.currentTimeMillis()
logger.debug("mozconfig.gradle> Loading mach environment")
apply from: file('./mach_env.gradle')
if (!ext.hasProperty("topsrcdir")) {
ext.topsrcdir = file(buildscript.getSourceFile()).getParentFile().getParentFile().getParentFile().getParentFile().absolutePath
}
abstract class TopobjdirValueSource implements ValueSource<String, TopobjdirValueSource.Params> {
interface Params extends ValueSourceParameters {
Property<String> getTopsrcdir()
Property<String> getMozconfigEnv()
Property<String> getMozObjdirEnv()
Property<String> getMozAutomation()
Property<String> getMozconfigContents()
Property<String> getDotMozconfigContents()
Property<String> getDefaultMozconfigContents()
Property<String> getLocalPropertiesContents()
Property<String> getLocalMozconfigContents()
}
@Inject
abstract ExecOperations getExecOperations()
// Called when Gradle's configuration cache is invalidated. We maintain our own
// local file cache here so that `./mach environment` only runs when inputs
// actually change, not just when Gradle's config cache is wiped for other reasons.
@Override
String obtain() {
def logger = org.gradle.api.logging.Logging.getLogger(TopobjdirValueSource.class)
def topsrcdir = parameters.topsrcdir.get()
// Compute hash of all inputs for local caching
def inputs = new StringBuilder()
inputs.append("MOZCONFIG=${parameters.mozconfigEnv.get()}\n")
inputs.append("MOZ_OBJDIR=${parameters.mozObjdirEnv.get()}\n")
inputs.append("mozconfigContents=${parameters.mozconfigContents.get().hashCode()}\n")
inputs.append("dotMozconfigContents=${parameters.dotMozconfigContents.get().hashCode()}\n")
inputs.append("defaultMozconfigContents=${parameters.defaultMozconfigContents.get().hashCode()}\n")
inputs.append("localPropertiesContents=${parameters.localPropertiesContents.get().hashCode()}\n")
inputs.append("localMozconfigContents=${parameters.localMozconfigContents.get().hashCode()}\n")
logger.debug("mozconfig.gradle> Cache inputs:\n${inputs}")
def sha256 = MessageDigest.getInstance("SHA-256")
sha256.update(inputs.toString().getBytes(StandardCharsets.UTF_8))
def currentHash = sha256.digest().encodeHex().toString()
// Check local cache (this doesn't affect Gradle's config cache since we're inside obtain())
// Disable local caching in automation to ensure consistent behavior
def useCache = !parameters.mozAutomation.get()
def cacheDir = new File(topsrcdir, ".gradle/mach-environment-cache")
cacheDir.mkdirs()
def cacheHashFile = new File(cacheDir, "inputs.sha256")
def topobjdirCacheFile = new File(cacheDir, "topobjdir.txt")
if (useCache && cacheHashFile.exists() && topobjdirCacheFile.exists()) {
def cachedHash = cacheHashFile.text.trim()
if (cachedHash == currentHash) {
logger.debug("mozconfig.gradle> topobjdir cache hit!")
return topobjdirCacheFile.text.trim()
}
}
logger.debug("mozconfig.gradle> topobjdir cache miss! Running `./mach environment`")
def command = ["${topsrcdir}/mach", "environment", "--format", "json"]
if (System.properties["os.name"].contains("Windows")) {
command.addAll(0, ["python"])
}
def stdout = new ByteArrayOutputStream()
def stderr = new ByteArrayOutputStream()
def result = execOperations.exec { spec ->
spec.workingDir = new File(topsrcdir)
spec.commandLine = command
spec.standardOutput = stdout
spec.errorOutput = stderr
spec.ignoreExitValue = true
}
if (result.exitValue != 0) {
throw new GradleException(
"mozconfig.gradle> Error running ./mach environment:\n" +
"Process '${command}' finished with non-zero exit value ${result.exitValue}:\n\n" +
"stdout:\n${stdout}\n\n" +
"stderr:\n${stderr}"
)
}
def outputString = stdout.toString().normalize().trim()
// Ignore possible lines of output from pip installing packages,
// so only start at what looks like the beginning of a JSON object
if (outputString.lastIndexOf("\n") != -1) {
outputString = outputString.substring(outputString.lastIndexOf("\n") + 1).trim()
}
def slurper = new JsonSlurper()
def jsonMachEnv
try {
jsonMachEnv = slurper.parseText(outputString)
} catch (Exception e) {
throw new GradleException("Failed to parse JSON from ./mach environment: ${e.message}\nOutput: ${outputString}")
}
def topobjdir = jsonMachEnv.topobjdir
if (useCache) {
try {
cacheHashFile.text = currentHash
topobjdirCacheFile.text = topobjdir
} catch (Exception e) {
logger.debug("mozconfig.gradle> Warning: Failed to write cache: ${e.message}")
}
}
return topobjdir
}
}
def readFileContents = { File f ->
if (!f.exists() || f.isDirectory()) {
return ""
}
return providers.fileContents(layout.rootDirectory.file(f.absolutePath)).asText.getOrElse("")
}
def mozconfigEnv = providers.environmentVariable('MOZCONFIG').getOrElse("")
def mozObjdirEnv = providers.environmentVariable('MOZ_OBJDIR').getOrElse("")
def mozAutomation = providers.environmentVariable('MOZ_AUTOMATION').getOrElse("")
def mozconfigFile = mozconfigEnv ? (new File(mozconfigEnv).isAbsolute() ? new File(mozconfigEnv) : new File(topsrcdir, mozconfigEnv)) : null
def mozconfigContents = mozconfigFile ? readFileContents(mozconfigFile) : ""
def dotMozconfigContents = readFileContents(new File(topsrcdir, '.mozconfig'))
def defaultMozconfigContents = readFileContents(new File(topsrcdir, 'mozconfig'))
def localPropertiesContents = readFileContents(new File(topsrcdir, 'local.properties'))
def localMozconfigContents = ""
def localProperties = new Properties()
if (localPropertiesContents) {
localProperties.load(new StringReader(localPropertiesContents))
def localMozconfigPath = localProperties.getProperty("mozilla-central.mozconfig")
if (localMozconfigPath) {
def localMozconfigFile = new File(localMozconfigPath).isAbsolute() ? new File(localMozconfigPath) : new File(topsrcdir, localMozconfigPath)
localMozconfigContents = readFileContents(localMozconfigFile)
}
}
// Extract autoPublish paths from local.properties for local substitution builds
["autoPublish.application-services.dir", "autoPublish.glean.dir"].each { key ->
def relativeOrAbsolutePath = localProperties.getProperty(key)
if (relativeOrAbsolutePath != null) {
def autoPublishDir = new File(topsrcdir).toPath().resolve(relativeOrAbsolutePath)
gradle.ext."localProperties.$key" = autoPublishDir.toString()
}
}
def topsrcdirValue = topsrcdir
def mozconfigEnvValue = mozconfigEnv
def mozObjdirEnvValue = mozObjdirEnv
def mozAutomationValue = mozAutomation
def mozconfigContentsValue = mozconfigContents
def dotMozconfigContentsValue = dotMozconfigContents
def defaultMozconfigContentsValue = defaultMozconfigContents
def localPropertiesContentsValue = localPropertiesContents
def localMozconfigContentsValue = localMozconfigContents
def topobjdirProvider = providers.of(TopobjdirValueSource) {
parameters {
it.topsrcdir.set(topsrcdirValue)
it.mozconfigEnv.set(mozconfigEnvValue)
it.mozObjdirEnv.set(mozObjdirEnvValue)
it.mozAutomation.set(mozAutomationValue)
it.mozconfigContents.set(mozconfigContentsValue)
it.dotMozconfigContents.set(dotMozconfigContentsValue)
it.defaultMozconfigContents.set(defaultMozconfigContentsValue)
it.localPropertiesContents.set(localPropertiesContentsValue)
it.localMozconfigContents.set(localMozconfigContentsValue)
}
}
def topobjdir = topobjdirProvider.get()
logger.debug("mozconfig.gradle> topobjdir=${topobjdir}")
def machEnvFile = new File(topobjdir, 'config.status.json')
if (!machEnvFile.exists()) {
throw new GradleException("config.status.json not found at ${machEnvFile.absolutePath}. Run ./mach configure first.")
}
def slurper = new JsonSlurper()
def json
try {
json = slurper.parse(machEnvFile)
} catch (Exception e) {
logger.error("mozconfig.gradle> Failed to parse: ${machEnvFile.text}")
throw new GradleException("Failed to parse ${machEnvFile.absolutePath}: ${e.message}")
}
def elapsed = System.currentTimeMillis() - startTime
logger.info("mozconfig.gradle> Loaded mach environment for ${topobjdir} in ${elapsed} ms")
if (json.substs.MOZ_BUILD_APP != 'mobile/android') {
throw new GradleException("Building with Gradle is only supported for Firefox for Android, i.e., MOZ_BUILD_APP == 'mobile/android'.")
}
// The Gradle instance is shared between settings.gradle and all the
// other build.gradle files (see
// We use this ext property to pass the per-object-directory mozconfig
// between scripts. This lets us execute set-up code before we gradle
// tries to configure the project even once, and as a side benefit
// saves invoking |mach environment| multiple times.
gradle.ext.mozconfig = json
// While we're here, read our VCS configuration when available. This is usually
// present in the `mozconfig`, but the source of truth is `source-repo.h`, which
// incorporates environment variables after configure-time.
gradle.ext.mozconfig.source_repo = [:]
def sourceRepoFile = file("${gradle.mozconfig.topobjdir}/source-repo.h")
if (sourceRepoFile.canRead()) {
def defines = ["MOZ_SOURCE_STAMP", "MOZ_SOURCE_REPO", "MOZ_SOURCE_URL"]
sourceRepoFile.eachLine { line ->
defines.forEach { define ->
def matcher = line =~ ~/^#define ${define}\s(.*)/
if (matcher.find()) {
gradle.ext.mozconfig.source_repo[define] = matcher.group(1).trim()
}
}
}
if (gradle.ext.mozconfig.substs.MOZILLA_OFFICIAL) {
logger.info("mozconfig.gradle> Parsed source-repo.h: ${gradle.ext.mozconfig.source_repo}");
}
}
// Configures Maven repositories from mozconfig onto a RepositoryHandler.
// In Groovy, pass `delegate` from within a `repositories { }` closure, since
// `delegate` refers to the RepositoryHandler that receives method calls.
// In Kotlin, pass `this`.
gradle.ext.configureMavenRepositories = { handler ->
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
handler.maven {
url = repository
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
allowInsecureProtocol = true
}
}
}
}