blob: 1176bf6ce4f9954ebb8c383076dfe9d8a76eb0c2 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import groovy.json.JsonOutput
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.util.regex.Pattern
/**
* Task to download dependencies specified in {@link ChromiumPlugin} and configure the
* Chromium build to integrate them. Used by declaring a new task in a {@code build.gradle}
* file:
* <pre>
* task myTaskName(type: BuildConfigGenerator) {
* repositoryPath 'build_files_and_repository_location/'
* }
* </pre>
*/
class BuildConfigGenerator extends DefaultTask {
private static final BUILD_GN_TOKEN_START = "# === Generated Code Start ==="
private static final BUILD_GN_TOKEN_END = "# === Generated Code End ==="
private static final BUILD_GN_GEN_PATTERN = Pattern.compile(
"${BUILD_GN_TOKEN_START}(.*)${BUILD_GN_TOKEN_END}",
Pattern.DOTALL)
private static final BUILD_GN_GEN_REMINDER = "# This is generated, do not edit. Update BuildConfigGenerator.groovy instead.\n"
private static final DEPS_TOKEN_START = "# === ANDROID_DEPS Generated Code Start ==="
private static final DEPS_TOKEN_END = "# === ANDROID_DEPS Generated Code End ==="
private static final DEPS_GEN_PATTERN = Pattern.compile(
"${DEPS_TOKEN_START}(.*)${DEPS_TOKEN_END}",
Pattern.DOTALL)
private static final DOWNLOAD_DIRECTORY_NAME = "libs"
// Some libraries are hosted in Chromium's //third_party directory. This is a mapping between
// them so they can be used instead of android_deps pulling in its own copy.
private static final def EXISTING_LIBS = [
'junit_junit': '//third_party/junit:junit',
'org_hamcrest_hamcrest_core': '//third_party/hamcrest:hamcrest_core_java',
]
/**
* Directory where the artifacts will be downloaded and where files will be generated.
* Note: this path is specified as relative to the chromium source root, and must be normalised
* to an absolute path before being used, as Groovy would base relative path where the script
* is being executed.
*/
String repositoryPath
/**
* Relative path to the DEPS file where the cipd packages are specified.
*/
String depsPath
/**
* Relative path to the Chromium source root from the build.gradle file.
*/
String chromiumSourceRoot
/**
* Name of the cipd root package.
*/
String cipdBucket
/**
* Prefix of path to strip before uploading to CIPD.
*/
String stripFromCipdPath
/**
* Skips license file import.
*/
boolean skipLicenses
/**
* Only pull play services targets into BUILD.gn file.
* If the play services target depends on a non-play services target, it will use the target in
* //third_party/android_deps/BUILD.gn.
*/
boolean onlyPlayServices
@TaskAction
void main() {
def graph = new ChromiumDepGraph(project: project, skipLicenses: skipLicenses)
def normalisedRepoPath = normalisePath(repositoryPath)
def rootDirPath = normalisePath(".")
// 1. Parse the dependency data
graph.collectDependencies()
// 2. Import artifacts into the local repository
def dependencyDirectories = []
graph.dependencies.values().each { dependency ->
if (excludeDependency(dependency, onlyPlayServices)) {
return
}
logger.debug "Processing ${dependency.name}: \n${jsonDump(dependency)}"
def depDir = "${DOWNLOAD_DIRECTORY_NAME}/${dependency.id}"
def absoluteDepDir = "${normalisedRepoPath}/${depDir}"
dependencyDirectories.add(depDir)
if (new File("${absoluteDepDir}/${dependency.fileName}").exists()) {
getLogger().quiet("${dependency.id} exists, skipping.")
return
}
project.copy {
from dependency.artifact.file
into absoluteDepDir
}
new File("${absoluteDepDir}/README.chromium").write(makeReadme(dependency))
new File("${absoluteDepDir}/cipd.yaml").write(makeCipdYaml(dependency, cipdBucket,
stripFromCipdPath,
repositoryPath))
new File("${absoluteDepDir}/OWNERS").write(makeOwners())
if (!skipLicenses) {
if (!dependency.licensePath?.trim()?.isEmpty()) {
new File("${absoluteDepDir}/LICENSE").write(
new File("${normalisedRepoPath}/${dependency.licensePath}").text)
} else if (!dependency.licenseUrl?.trim()?.isEmpty()) {
File destFile = new File("${absoluteDepDir}/LICENSE")
downloadFile(dependency.id, dependency.licenseUrl, destFile)
if (destFile.text.contains("<html")) {
throw new RuntimeException("Found HTML in LICENSE file. Please add an "
+ "override to ChromiumDepGraph.groovy for ${dependency.name}.")
}
}
}
}
// 3. Generate the root level build files
updateBuildTargetDeclaration(graph, "${normalisedRepoPath}/BUILD.gn", onlyPlayServices)
updateDepsDeclaration(graph, cipdBucket, stripFromCipdPath, repositoryPath,
"${rootDirPath}/${depsPath}", onlyPlayServices)
dependencyDirectories.sort { path1, path2 -> return path1.compareTo(path2) }
updateReadmeReferenceFile(dependencyDirectories,
"${normalisedRepoPath}/additional_readme_paths.json")
}
private static void updateBuildTargetDeclaration(ChromiumDepGraph depGraph, String path,
boolean onlyPlayServices) {
File buildFile = new File(path)
def sb = new StringBuilder()
// Comparator to sort the dependency in alphabetical order, with the visible ones coming
// before all the internal ones.
def dependencyComparator = { dependency1, dependency2 ->
def visibilityResult = Boolean.compare(dependency1.visible, dependency2.visible)
if (visibilityResult != 0) return -visibilityResult
return dependency1.id.compareTo(dependency2.id)
}
depGraph.dependencies.values().sort(dependencyComparator).each { dependency ->
if (excludeDependency(dependency, onlyPlayServices)) {
return
}
def depsStr = ""
if (!dependency.children.isEmpty()) {
dependency.children.each { childDep ->
def dep = depGraph.dependencies[childDep]
if (dep.exclude) {
return
}
// Special case: If a child dependency is an existing lib, rather than skipping
// it, replace the child dependency with the existing lib.
def existingLib = EXISTING_LIBS.get(dep.id)
def targetName = translateTargetName(dep.id) + "_java"
if (existingLib != null) {
depsStr += "\"${existingLib}\","
} else if (onlyPlayServices && !isPlayServicesTarget(dep.id)) {
depsStr += "\"//third_party/android_deps:${targetName}\","
} else {
depsStr += "\":${targetName}\","
}
}
}
def libPath = "${DOWNLOAD_DIRECTORY_NAME}/${dependency.id}"
def targetName = translateTargetName(dependency.id) + "_java"
sb.append(BUILD_GN_GEN_REMINDER)
if (dependency.extension == 'jar') {
sb.append("""\
java_prebuilt("${targetName}") {
jar_path = "${libPath}/${dependency.fileName}"
output_name = "${dependency.id}"
""".stripIndent())
if (dependency.supportsAndroid) {
sb.append(" supports_android = true\n")
} else {
// No point in enabling asserts third-party prebuilts.
// Also required to break a dependency cycle for errorprone.
sb.append(" enable_bytecode_rewriter = false\n")
}
} else if (dependency.extension == 'aar') {
sb.append("""\
android_aar_prebuilt("${targetName}") {
aar_path = "${libPath}/${dependency.fileName}"
info_path = "${libPath}/${dependency.id}.info"
""".stripIndent())
} else {
throw new IllegalStateException("Dependency type should be JAR or AAR")
}
if (!dependency.visible) {
sb.append(" # To remove visibility constraint, add this dependency to\n")
sb.append(" # //tools/android/roll/android_deps/build.gradle.\n")
sb.append(" visibility = [ \":*\" ]\n")
}
if (dependency.testOnly) sb.append(" testonly = true\n")
if (!depsStr.empty) sb.append(" deps = [${depsStr}]\n")
addSpecialTreatment(sb, dependency.id)
sb.append("}\n\n")
}
def matcher = BUILD_GN_GEN_PATTERN.matcher(buildFile.getText())
if (!matcher.find()) throw new IllegalStateException("BUILD.gn insertion point not found.")
buildFile.write(matcher.replaceFirst(
"${BUILD_GN_TOKEN_START}\n${sb.toString()}\n${BUILD_GN_TOKEN_END}"))
}
public static String translateTargetName(String targetName) {
if (isPlayServicesTarget(targetName)) {
return targetName.replaceFirst("com_", "").replaceFirst("android_gms_", "")
}
return targetName
}
public static boolean isPlayServicesTarget(String dependencyId) {
// Firebase has historically been treated as a part of play services, so it counts here for
// backwards compatibility.
return Pattern.matches(".*google.*(play_services|firebase).*", dependencyId)
}
private static void addSpecialTreatment(StringBuilder sb, String dependencyId) {
if (isPlayServicesTarget(dependencyId)) {
if (Pattern.matches(".*cast_framework.*", dependencyId)) {
sb.append(' # Removing all resources from cast framework as they are unused bloat.\n')
sb.append(' # Can only safely remove them when R8 will strip the path that accesses them.\n')
sb.append(' strip_resources = !is_java_debug\n')
} else {
sb.append(' # Removing drawables from GMS .aars as they are unused bloat.\n')
sb.append(' strip_drawables = true\n')
}
}
switch(dependencyId) {
case 'androidx_annotation_annotation':
sb.append(' # https://crbug.com/989505\n')
sb.append(' jar_excluded_patterns = ["META-INF/proguard/*"]\n')
break
case 'androidx_core_core':
case 'androidx_media_media':
case 'androidx_versionedparcelable_versionedparcelable':
case 'com_android_support_support_compat':
case 'com_android_support_support_media_compat':
case 'com_android_support_versionedparcelable':
// Target has AIDL, but we don't support it yet: http://crbug.com/644439
sb.append(' ignore_aidl = true\n')
break
case 'androidx_test_uiautomator_uiautomator':
sb.append(' deps = [":androidx_test_runner_java"]\n')
break
case 'com_android_support_mediarouter_v7':
sb.append(' # https://crbug.com/1000382\n')
sb.append(' proguard_configs = ["support_mediarouter.flags"]\n')
break
case 'androidx_mediarouter_mediarouter':
sb.append(' # https://crbug.com/1000382\n')
sb.append(' proguard_configs = ["androidx_mediarouter.flags"]\n')
break
case 'androidx_transition_transition':
// Not specified in the POM, compileOnly dependency not supposed to be used unless
// the library is present: b/70887421
sb.append(' deps += [":androidx_fragment_fragment_java"]\n')
break
case 'androidx_vectordrawable_vectordrawable':
case 'com_android_support_support_vector_drawable':
// Target has AIDL, but we don't support it yet: http://crbug.com/644439
sb.append(' create_srcjar = false\n')
break
case 'android_arch_lifecycle_runtime':
case 'android_arch_lifecycle_viewmodel':
sb.append(' # https://crbug.com/887942#c1\n')
sb.append(' ignore_proguard_configs = true\n')
break
case 'com_android_support_coordinatorlayout':
sb.append(' # https:crbug.com/954584\n')
sb.append(' ignore_proguard_configs = true\n')
break
case 'com_android_support_design':
// Reduce binary size. https:crbug.com/954584
sb.append(' ignore_proguard_configs = true\n')
break
case 'com_android_support_support_annotations':
sb.append(' # https://crbug.com/989505\n')
sb.append(' jar_excluded_patterns = ["META-INF/proguard/*"]\n')
break
case 'com_android_support_transition':
// Not specified in the POM, compileOnly dependency not supposed to be used unless
// the library is present: b/70887421
sb.append(' deps += [":com_android_support_support_fragment_java"]\n')
break
case 'com_google_android_gms_play_services_basement':
// Deprecated deps jar but still needed by play services basement.
sb.append(' input_jars_paths=["\\$android_sdk/optional/org.apache.http.legacy.jar"]\n')
break
case 'com_google_ar_core':
// Target .aar file contains .so libraries that need to be extracted,
// and android_aar_prebuilt template will fail if it's not set explictly.
sb.append(' extract_native_libraries = true\n')
break
case 'com_google_guava_guava':
// Need to exclude class and replace it with class library as
// com_google_guava_listenablefuture has support_androids=true.
sb.append(' deps += [":com_google_guava_listenablefuture_java"]\n')
sb.append(' jar_excluded_patterns = ["*/ListenableFuture.class"]\n')
break
case 'com_google_guava_listenablefuture_java':
// Needed to break dependency cycle for errorprone_plugin_java.
sb.append(' no_build_hooks = true\n')
break
case 'androidx_test_rules':
// Target needs Android SDK deps which exist in third_party/android_sdk.
sb.append("""\
| deps += [
| "//third_party/android_sdk:android_test_base_java",
| "//third_party/android_sdk:android_test_mock_java",
| "//third_party/android_sdk:android_test_runner_java",
| ]
|
|""".stripMargin())
break
case 'net_sf_kxml_kxml2':
sb.append(' # Target needs to exclude *xmlpull* files as already included in Android SDK.\n')
sb.append(' jar_excluded_patterns = [ "*xmlpull*" ]\n')
break
case 'com_android_support_preference_v7':
// Replace broad library -keep rules with a more limited set in
// chrome/android/java/proguard.flags instead.
sb.append(' ignore_proguard_configs = true\n')
break
}
}
private static void updateDepsDeclaration(ChromiumDepGraph depGraph, String cipdBucket,
String stripFromCipdPath, String repoPath,
String depsFilePath, boolean onlyPlayServices) {
File depsFile = new File(depsFilePath)
def sb = new StringBuilder()
// Note: The string we're inserting is nested 1 level, hence the 2 leading spaces. Same
// applies to the multiline package declaration string below.
sb.append(" # Generated by //tools/android/roll/android_deps/fetch_all.py")
// Comparator to sort the dependencies in alphabetical order.
def dependencyComparator = { dependency1, dependency2 ->
return dependency1.id.compareTo(dependency2.id)
}
depGraph.dependencies.values().sort(dependencyComparator).each { dependency ->
if (excludeDependency(dependency, onlyPlayServices)) {
return
}
def depPath = "${DOWNLOAD_DIRECTORY_NAME}/${dependency.id}"
def cipdPath = "${cipdBucket}/"
if (stripFromCipdPath) {
assert repoPath.startsWith(stripFromCipdPath)
cipdPath += repoPath.substring(stripFromCipdPath.length() + 1)
} else {
cipdPath += repoPath
}
// CIPD does not allow uppercase in names.
cipdPath += "/${depPath}".toLowerCase()
sb.append("""\
|
| 'src/${repoPath}/${depPath}': {
| 'packages': [
| {
| 'package': '${cipdPath}',
| 'version': 'version:${dependency.version}-${dependency.cipdSuffix}',
| },
| ],
| 'condition': 'checkout_android',
| 'dep_type': 'cipd',
| },
|""".stripMargin())
}
def matcher = DEPS_GEN_PATTERN.matcher(depsFile.getText())
if (!matcher.find()) throw new IllegalStateException("DEPS insertion point not found.")
depsFile.write(matcher.replaceFirst("${DEPS_TOKEN_START}\n${sb}\n ${DEPS_TOKEN_END}"))
}
private static void updateReadmeReferenceFile(List<String> directories, String readmePath) {
File refFile = new File(readmePath)
refFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(directories)) + "\n")
}
public static boolean excludeDependency(ChromiumDepGraph.DependencyDescription dependency,
boolean onlyPlayServices) {
return dependency.exclude || EXISTING_LIBS.get(dependency.id) != null ||
(onlyPlayServices && !isPlayServicesTarget(dependency.id))
}
private String normalisePath(String pathRelativeToChromiumRoot) {
return project.file("${chromiumSourceRoot}/${pathRelativeToChromiumRoot}").absolutePath
}
static String makeOwners() {
// Make it easier to upgrade existing dependencies without full third_party review.
return "file://third_party/android_deps/OWNERS"
}
static String makeReadme(ChromiumDepGraph.DependencyDescription dependency) {
def licenseString
// Replace license names with ones that are whitelisted, see third_party/PRESUBMIT.py
switch (dependency.licenseName) {
case "The Apache Software License, Version 2.0":
licenseString = "Apache Version 2.0"
break
default:
licenseString = dependency.licenseName
}
def securityCritical = dependency.supportsAndroid && dependency.isShipped
def licenseFile = dependency.isShipped? "LICENSE" : "NOT_SHIPPED"
return """\
Name: ${dependency.displayName}
Short Name: ${dependency.name}
URL: ${dependency.url}
Version: ${dependency.version}
License: ${licenseString}
License File: ${licenseFile}
Security Critical: ${securityCritical? "yes" : "no"}
${dependency.licenseAndroidCompatible? "License Android Compatible: yes" : ""}
Description:
${dependency.description}
Local Modifications:
No modifications.
""".stripIndent()
}
static String makeCipdYaml(ChromiumDepGraph.DependencyDescription dependency, String cipdBucket,
String stripFromCipdPath, String repoPath) {
if (!stripFromCipdPath) {
stripFromCipdPath = ''
}
def cipdVersion = "${dependency.version}-${dependency.cipdSuffix}"
def cipdPath = "${cipdBucket}/"
if (stripFromCipdPath) {
assert repoPath.startsWith(stripFromCipdPath)
cipdPath += repoPath.substring(stripFromCipdPath.length() + 1)
} else {
cipdPath += repoPath
}
// CIPD does not allow uppercase in names.
cipdPath += "/${DOWNLOAD_DIRECTORY_NAME}/" + dependency.id.toLowerCase()
// NOTE: the fetch_all.py script relies on the format of this file!
// See fetch_all.py:GetCipdPackageInfo().
// NOTE: keep the copyright year 2018 until this generated code is
// updated, avoiding annual churn of all cipd.yaml files.
def str = """\
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# To create CIPD package run the following command.
# cipd create --pkg-def cipd.yaml -tag version:${cipdVersion}
package: ${cipdPath}
description: "${dependency.displayName}"
data:
- file: ${dependency.fileName}
""".stripIndent()
return str
}
static String jsonDump(obj) {
return JsonOutput.prettyPrint(JsonOutput.toJson(obj))
}
static void printDump(obj) {
getLogger().warn(jsonDump(obj))
}
static HttpURLConnection connectAndFollowRedirects(String id, String sourceUrl) {
URL urlObj = new URL(sourceUrl)
HttpURLConnection connection
for (int i = 0; i < 10; ++i) {
// Several deps use this URL for their license, but it just points to license
// *template*. Generally the actual license can be found in the source code.
if (sourceUrl.contains("://opensource.org/licenses")) {
throw new RuntimeException("Found templated license URL for dependency "
+ id + ": " + sourceUrl
+ ". You will need to edit FALLBACK_PROPERTIES for this dep.")
}
connection = urlObj.openConnection()
switch (connection.getResponseCode()) {
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MOVED_TEMP:
String location = connection.getHeaderField("Location");
urlObj = new URL(urlObj, location);
continue
case HttpURLConnection.HTTP_OK:
return connection
default:
throw new RuntimeException(
"Url had statusCode=" + connection.getResponseCode() + ": " + sourceUrl)
}
}
throw new RuntimeException("Url in redirect loop: " + sourceUrl)
}
static void downloadFile(String id, String sourceUrl, File destinationFile) {
destinationFile.withOutputStream { out ->
out << connectAndFollowRedirects(id, sourceUrl).getInputStream()
}
}
}