/* 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") if (!ext.hasProperty("topsrcdir")) { ext.topsrcdir = file(buildscript.getSourceFile()).getParentFile().getParentFile().getParentFile().getParentFile().absolutePath } abstract class TopobjdirValueSource implements ValueSource { interface Params extends ValueSourceParameters { Property getTopsrcdir() Property getMozconfigEnv() Property getMozObjdirEnv() Property getMozAutomation() Property getMozconfigContents() Property getDotMozconfigContents() Property getDefaultMozconfigContents() Property getLocalPropertiesContents() Property 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 def mozconfigEnv = parameters.mozconfigEnv.get() if (mozconfigEnv) { spec.environment('MOZCONFIG', mozconfigEnv) } 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 mozObjdirEnv = providers.environmentVariable('MOZ_OBJDIR').getOrElse("") def mozAutomation = providers.environmentVariable('MOZ_AUTOMATION').getOrElse("") def dotMozconfigContents = readFileContents(new File(topsrcdir, '.mozconfig')) def defaultMozconfigContents = readFileContents(new File(topsrcdir, 'mozconfig')) def localPropertiesContents = readFileContents(new File(topsrcdir, 'local.properties')) def localMozconfigResolvedPath = "" def localMozconfigContents = "" def localProperties = new Properties() if (localPropertiesContents) { localProperties.load(new StringReader(localPropertiesContents)) def localMozconfigProperty = localProperties.getProperty("mozilla-central.mozconfig") if (localMozconfigProperty) { def localMozconfigFile = new File(localMozconfigProperty).isAbsolute() ? new File(localMozconfigProperty) : new File(topsrcdir, localMozconfigProperty) localMozconfigResolvedPath = localMozconfigFile.absolutePath localMozconfigContents = readFileContents(localMozconfigFile) } } def mozconfigEnv = providers.environmentVariable('MOZCONFIG').getOrElse("") if (mozconfigEnv && !new File(mozconfigEnv).isAbsolute()) { mozconfigEnv = new File(topsrcdir, mozconfigEnv).absolutePath } if (localMozconfigResolvedPath && (!mozconfigEnv || localMozconfigResolvedPath == mozconfigEnv)) { mozconfigEnv = localMozconfigResolvedPath logger.debug("mozconfig.gradle> Using mozconfig from local.properties: ${localMozconfigResolvedPath}") } else if (localMozconfigResolvedPath) { logger.debug("mozconfig.gradle> Preferring mozconfig from environment over local.properties: ${mozconfigEnv}") } else { logger.debug("mozconfig.gradle> No mozconfig found in local.properties") } def mozconfigFile = mozconfigEnv ? new File(mozconfigEnv) : null def mozconfigContents = mozconfigFile ? readFileContents(mozconfigFile) : "" // 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 // http://forums.gradle.org/gradle/topics/define_extension_properties_from_settings_xml). // 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 } } } }