// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import groovy.transform.AutoClone
import groovy.util.slurpersupport.GPathResult
import org.gradle.api.Project
import org.gradle.api.artifacts.repositories.ArtifactRepository
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedConfiguration
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.ResolvedModuleVersion
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.logging.Logger
/**
* Parses the project dependencies and generates a graph of {@link ChromiumDepGraph.DependencyDescription} objects to
* make the data manipulation easier.
*/
class ChromiumDepGraph {
private static final String DEFAULT_CIPD_SUFFIX = 'cr1'
// Some libraries don't properly fill their POM with the appropriate licensing information. It is provided here from
// manual lookups. Note that licenseUrl must provide textual content rather than be an html page.
static final Map<String, PropertyOverride> PROPERTY_OVERRIDES = [
androidx_multidex_multidex: new PropertyOverride(
url: 'https://maven.google.com/androidx/multidex/multidex/2.0.0/multidex-2.0.0.aar'),
com_google_android_datatransport_transport_api: new PropertyOverride(
description: 'Interfaces for data logging in GmsCore SDKs.'),
// Chrome uses the window APIs directly instead of going through the androidx middleware.
// See //third_party/android_sdk/window_extensions/README.md
androidx_window_window: new PropertyOverride(exclude: true),
com_google_android_datatransport_transport_backend_cct: new PropertyOverride(
exclude: true), // We're not using datatransport functionality.
com_google_android_datatransport_transport_runtime: new PropertyOverride(
exclude: true), // We're not using datatransport functionality.
com_google_android_gms_play_services_cloud_messaging: new PropertyOverride(
description: 'Firebase Cloud Messaging library that interfaces with GmsCore.'),
com_google_android_gms_play_services_base: new PropertyOverride(
description: 'Base library for gmscore / Google Play Services.'),
com_google_android_gms_play_services_location: new PropertyOverride(
description: 'Provides data about the device\'s physical location via gmscore.'),
com_google_auto_auto_common: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_auto_service_auto_service: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_auto_service_auto_service_annotations: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_auto_value_auto_value_annotations: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_code_gson_gson: new PropertyOverride(
url: 'https://github.com/google/gson',
description: 'A Java serialization/deserialization library to convert Java Objects into JSON and back',
licenseUrl: 'https://raw.githubusercontent.com/google/gson/master/LICENSE',
licenseName: 'Apache 2.0'),
com_google_errorprone_error_prone_annotation: new PropertyOverride(
// Robolectric has a (seemingly unnecessary) dep on this. It's meant to be needed
// only for writing custom Error Prone checks. Chrome's copy is within the
// Error Prone fat jar: //third_party/android_build_tools/error_prone
// Depending on this fat jar pulls in a conflicting copy of protobuf library.
exclude: true),
com_google_errorprone_error_prone_annotations: new PropertyOverride(
url: 'https://github.com/google/error-prone/tree/master/annotations',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0',
description: 'ErrorProne Annotations.',),
com_google_firebase_firebase_annotations: new PropertyOverride(
description: 'Common annotations for Firebase SKDs.'),
com_google_firebase_firebase_common: new PropertyOverride(
description: 'Common classes for Firebase SDKs.'),
com_google_firebase_firebase_components: new PropertyOverride(
description: 'Provides dependency management for Firebase SDKs.'),
com_google_firebase_firebase_datatransport: new PropertyOverride(
exclude: true), // We're not using datatransport functionality.
com_google_firebase_firebase_encoders_json: new PropertyOverride(
description: 'JSON encoders used in Firebase SDKs.'),
com_google_firebase_firebase_encoders: new PropertyOverride(
description: 'Commonly used encoders for Firebase SKDs.'),
com_google_firebase_firebase_iid_interop: new PropertyOverride(
description: 'Interface library for Firebase IID SDK.'),
com_google_firebase_firebase_iid: new PropertyOverride(
description: 'Firebase IID SDK to get access to Instance IDs.'),
com_google_firebase_firebase_installations_interop: new PropertyOverride(
description: 'Interface library for Firebase Installations SDK.'),
com_google_firebase_firebase_installations: new PropertyOverride(
description: 'Firebase Installations SDK containing the client libraries to manage FIS.'),
com_google_firebase_firebase_measurement_connector: new PropertyOverride(
description: 'Bridge interfaces for Firebase analytics into GmsCore.'),
com_google_firebase_firebase_messaging: new PropertyOverride(
description: 'Firebase Cloud Messaging SDK to send and receive push messages via FCM.'),
com_google_googlejavaformat_google_java_format: new PropertyOverride(
url: 'https://github.com/google/google-java-format',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_guava_failureaccess: new PropertyOverride(
url: 'https://github.com/google/guava',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
com_google_guava_guava: new PropertyOverride(
url: 'https://github.com/google/guava',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0',
// Both -jre and -android versions are listed. Filter to only the -jre ones.
versionFilter: '-jre'),
com_google_guava_guava_android: new PropertyOverride(
url: 'https://github.com/google/guava',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0',
// Both -jre and -android versions are listed. Filter to only the -android ones.
versionFilter: '-android'),
com_squareup_wire_wire_runtime_jvm: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
org_bouncycastle_bcprov_jdk18on: new PropertyOverride(
cpePrefix: 'cpe:/a:bouncycastle:legion-of-the-bouncy-castle:1.72',
url: 'https://github.com/bcgit/bc-java',
licensePath: 'licenses/Bouncy_Castle-2015.txt',
licenseName: 'MIT'),
org_codehaus_mojo_animal_sniffer_annotations: new PropertyOverride(
url: 'http://www.mojohaus.org/animal-sniffer/animal-sniffer-annotations/',
description: 'Animal Sniffer Annotations allow marking methods which Animal Sniffer should ignore ' +
'signature violations of.',
/* groovylint-disable-next-line LineLength */
licenseUrl: 'https://raw.githubusercontent.com/mojohaus/animal-sniffer/master/animal-sniffer-annotations/pom.xml',
licensePath: 'licenses/Codehaus_License-2009.txt',
licenseName: 'MIT'),
com_google_protobuf_protobuf_lite: new PropertyOverride(
exclude: true, // There is a phantom dep on this target, but this is deprecated and not used in chrome.
url: 'https://github.com/protocolbuffers/protobuf/blob/master/java/README.md',
licenseUrl: 'https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE',
licenseName: 'BSD'),
com_google_protobuf_protobuf_javalite: new PropertyOverride(
url: 'https://github.com/protocolbuffers/protobuf/blob/master/java/lite.md',
licenseUrl: 'https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE',
licenseName: 'BSD'),
jakarta_inject_jakarta_inject_api: new PropertyOverride(
// Help gradle resolve the same version that our 3pp script does.
versionFilter: '\\d+\\.\\d+\\.\\d+$'),
javax_annotation_javax_annotation_api: new PropertyOverride(
isShipped: false, // Annotations are stripped by R8.
licenseName: 'CDDLv1.1',
licensePath: 'licenses/CDDLv1.1.txt'),
javax_annotation_jsr250_api: new PropertyOverride(
isShipped: false, // Annotations are stripped by R8.
licenseName: 'CDDLv1.0',
licensePath: 'licenses/CDDLv1.0.txt'),
net_bytebuddy_byte_buddy: new PropertyOverride(
url: 'https://github.com/raphw/byte-buddy',
licenseUrl: 'https://raw.githubusercontent.com/raphw/byte-buddy/master/LICENSE',
licenseName: 'Apache 2.0'),
net_bytebuddy_byte_buddy_agent: new PropertyOverride(
url: 'https://github.com/raphw/byte-buddy',
licenseUrl: 'https://raw.githubusercontent.com/raphw/byte-buddy/master/LICENSE',
licenseName: 'Apache 2.0'),
net_bytebuddy_byte_buddy_android: new PropertyOverride(
url: 'https://github.com/raphw/byte-buddy',
licenseUrl: 'https://raw.githubusercontent.com/raphw/byte-buddy/master/LICENSE',
licenseName: 'Apache 2.0'),
org_checkerframework_checker_compat_qual: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/typetools/checker-framework/master/LICENSE.txt',
licenseName: 'GPL v2 with the classpath exception'),
org_checkerframework_checker_qual: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/typetools/checker-framework/master/LICENSE.txt',
licenseName: 'GPL v2 with the classpath exception'),
org_checkerframework_checker_util: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/typetools/checker-framework/master/checker-util/LICENSE.txt',
licenseName: 'MIT'),
org_conscrypt_conscrypt_openjdk_uber: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/google/conscrypt/master/LICENSE',
licenseName: 'Apache 2.0'),
org_hamcrest_hamcrest: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/hamcrest/JavaHamcrest/master/LICENSE',
licenseName: 'BSD'),
org_jsoup_jsoup: new PropertyOverride(
cpePrefix: 'cpe:/a:jsoup:jsoup:1.14.3',
licenseUrl: 'https://raw.githubusercontent.com/jhy/jsoup/master/LICENSE',
licenseName: 'The MIT License'),
org_mockito_mockito_android: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/mockito/mockito/main/LICENSE',
licenseName: 'The MIT License'),
org_mockito_mockito_core: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/mockito/mockito/main/LICENSE',
licenseName: 'The MIT License'),
org_mockito_mockito_subclass: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/mockito/mockito/main/LICENSE',
licenseName: 'The MIT License'),
org_objenesis_objenesis: new PropertyOverride(
url: 'http://objenesis.org/index.html',
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
org_ow2_asm_asm: new PropertyOverride(
licenseUrl: 'https://gitlab.ow2.org/asm/asm/raw/master/LICENSE.txt',
licenseName: 'BSD'),
org_ow2_asm_asm_analysis: new PropertyOverride(
licenseUrl: 'https://gitlab.ow2.org/asm/asm/raw/master/LICENSE.txt',
licenseName: 'BSD'),
org_ow2_asm_asm_commons: new PropertyOverride(
licenseUrl: 'https://gitlab.ow2.org/asm/asm/raw/master/LICENSE.txt',
licenseName: 'BSD'),
org_ow2_asm_asm_tree: new PropertyOverride(
licenseUrl: 'https://gitlab.ow2.org/asm/asm/raw/master/LICENSE.txt',
licenseName: 'BSD'),
org_ow2_asm_asm_util: new PropertyOverride(
licenseUrl: 'https://gitlab.ow2.org/asm/asm/raw/master/LICENSE.txt',
licenseName: 'BSD'),
org_robolectric_annotations: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_junit: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_nativeruntime: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_nativeruntime_dist_compat: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_pluginapi: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_plugins_maven_dependency_resolver: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_resources: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_robolectric: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_sandbox: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_shadowapi: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_shadows_framework: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_shadows_versioning: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_utils: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
org_robolectric_utils_reflector: new PropertyOverride(
licenseUrl: 'https://raw.githubusercontent.com/robolectric/robolectric/master/LICENSE',
licenseName: 'MIT'),
// Prevent version changing ~weekly. https://crbug.com/1257197
org_jetbrains_kotlinx_kotlinx_coroutines_core_jvm: new PropertyOverride(
resolveVersion: '1.6.4'),
org_jetbrains_kotlinx_kotlinx_coroutines_android: new PropertyOverride(
resolveVersion: '1.6.4'),
org_jetbrains_kotlinx_kotlinx_coroutines_guava: new PropertyOverride(
resolveVersion: '1.6.4'),
io_grpc_grpc_binder: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_grpc_grpc_core: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_grpc_grpc_api: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_grpc_grpc_context: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_grpc_grpc_protobuf_lite: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_grpc_grpc_stub: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
io_perfmark_perfmark_api: new PropertyOverride(
licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0.txt',
licenseName: 'Apache 2.0'),
]
// Bill of materials (BOM) deps are used to specify versions for other dependencies and don't have children or
// artifacts of their own. Add other such empty deps here when we encounter them.
private static final Set<String> ALLOWED_EMPTY_DEPS = [] as Set
// Local text versions of HTML licenses. This cannot replace PROPERTY_OVERRIDES because some libraries refer to
// license templates such as https://opensource.org/licenses/MIT.
// Keys should be 'https', since customizeLicenses() will normalize URLs to https.
static final Map<String, String> LICENSE_OVERRIDES = [
'https://developer.android.com/studio/terms.html': 'licenses/Android_SDK_License-December_9_2016.txt',
'https://openjdk.java.net/legal/gplv2+ce.html': 'licenses/GNU_v2_with_Classpath_Exception_1991.txt',
'https://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web': 'licenses/SIL_Open_Font.txt',
'https://www.unicode.org/copyright.html#License': 'licenses/Unicode.txt',
'https://www.unicode.org/license.html': 'licenses/Unicode.txt',
]
final Map<String, DependencyDescription> dependencies = [:]
Project[] projects
Logger logger
boolean skipLicenses
static String makeModuleId(ResolvedModuleVersion module) {
// Does not include version because by default the resolution strategy for gradle is to use the newest version
// among the required ones. We want to be able to match it in the BUILD.gn file.
String moduleId = sanitize("${module.id.group}_${module.id.name}")
// Add 'android' suffix for guava-android so that its module name is distinct from the module for guava.
if (module.id.name == 'guava' && module.id.version.contains('android')) {
moduleId += '_android'
}
return moduleId
}
static String makeModuleId(ResolvedArtifact artifact) {
// Does not include version because by default the resolution strategy for gradle is to use the newest version
// among the required ones. We want to be able to match it in the BUILD.gn file.
ComponentIdentifier componentId = artifact.id.componentIdentifier
String moduleId = sanitize("${componentId.group}_${componentId.module}")
// Add 'android' suffix for guava-android so that its module name is distinct from the module for guava.
if (componentId.module == 'guava' && componentId.version.contains('android')) {
moduleId += '_android'
}
return moduleId
}
void collectDependencies() {
Set<ResolvedConfiguration> deps = [] as Set
Map<String, List<ResolvedArtifact>> resolvedArtifacts = [:]
String[] configNames = [
'compile',
'buildCompile',
'testCompile',
'androidTestCompile',
'buildCompileNoDeps'
]
for (Project project : projects) {
for (String configName : configNames) {
ResolvedConfiguration resolvedConfig = project.configurations.getByName(configName).resolvedConfiguration
deps += resolvedConfig.firstLevelModuleDependencies
if (!resolvedArtifacts.containsKey(configName)) {
resolvedArtifacts[configName] = []
}
resolvedArtifacts[configName].addAll(resolvedConfig.resolvedArtifacts)
}
}
List<String> topLevelIds = []
deps.each { dependency ->
topLevelIds.add(makeModuleId(dependency.module))
collectDependenciesInternal(dependency)
}
topLevelIds.each { id -> dependencies.get(id).visible = true }
resolvedArtifacts['testCompile'].each { artifact ->
String id = makeModuleId(artifact)
DependencyDescription dep = dependencies.get(id)
assert dep : "No dependency collected for artifact ${artifact.name}"
dep.testOnly = true
}
resolvedArtifacts['androidTestCompile'].each { artifact ->
DependencyDescription dep = dependencies.get(makeModuleId(artifact))
assert dep : "No dependency collected for artifact ${artifact.name} (${makeModuleId(artifact)})"
dep.supportsAndroid = true
dep.testOnly = true
}
resolvedArtifacts['buildCompile'].each { artifact ->
String id = makeModuleId(artifact)
DependencyDescription dep = dependencies.get(id)
assert dep : "No dependency collected for artifact ${artifact.name}"
dep.usedInBuild = true
dep.testOnly = false
}
resolvedArtifacts['buildCompileNoDeps'].each { artifact ->
String id = makeModuleId(artifact)
DependencyDescription dep = dependencies.get(id)
assert dep : "No dependency collected for artifact ${artifact.name}"
dep.usedInBuild = true
dep.testOnly = false
}
List<ResolvedArtifact> compileResolvedArtifacts = resolvedArtifacts['compile']
compileResolvedArtifacts.each { artifact ->
String id = makeModuleId(artifact)
DependencyDescription dep = dependencies.get(id)
assert dep : "No dependency collected for artifact ${artifact.name}"
dep.supportsAndroid = true
dep.testOnly = false
dep.isShipped = true
}
PROPERTY_OVERRIDES.each { id, overrides ->
DependencyDescription dep = dependencies.get(id)
if (dep) {
// Null-check is required since isShipped is a boolean. This
// check must come after all the deps are resolved instead of in
// customizeDep, since otherwise it gets overwritten.
if (overrides?.isShipped != null) {
dep.isShipped = overrides.isShipped
}
// if overrideLatest is true, set it recursively on the dep and
// all its children. This makes it easier to manage since you do
// not have to set it on a whole set of old deps.
if (overrides?.overrideLatest) {
recursivelyOverrideLatestVersion(dep)
}
dep.versionFilter = overrides.versionFilter
} else {
logger.warn('PROPERTY_OVERRIDES has stale dep: ' + id)
}
}
}
private static String sanitize(String input) {
return input.replaceAll('[:.-]', '_')
}
private void recursivelyOverrideLatestVersion(DependencyDescription dep) {
dep.overrideLatest = true
dep.children.each { childID ->
PropertyOverride overrides = PROPERTY_OVERRIDES.get(childID)
if (!overrides?.resolveVersion) {
DependencyDescription child = dependencies.get(childID)
recursivelyOverrideLatestVersion(child)
}
}
}
private void collectDependenciesInternal(ResolvedDependency dependency) {
String id = makeModuleId(dependency.module)
if (dependencies.containsKey(id)) {
String gotVersion = dependency.module.id.version
if (dependencies.get(id).version == gotVersion) {
return
}
PropertyOverride overrides = PROPERTY_OVERRIDES.get(id)
if (overrides?.resolveVersion) {
if (overrides.resolveVersion != gotVersion) {
return
}
} else if (isVersionLower(gotVersion, dependencies.get(id).version)) {
// Default to using largest version for version conflict resolution. See http://crbug.com/1040958.
// https://docs.gradle.org/current/userguide/dependency_resolution.html#sec:version-conflict
return
}
}
List<ResolvedDependency> childDependenciesWithArtifacts = []
List<String> childModules = []
dependency.children.each { childDependency ->
// Replace dependency which acts as a redirect (ex: org.jetbrains.kotlinx:kotlinx-coroutines-core) with
// dependencies it redirects to.
if (childDependency.moduleArtifacts) {
childDependenciesWithArtifacts += childDependency
} else {
if (childDependency.children) {
childDependenciesWithArtifacts += childDependency.children
} else {
String childDepId = makeModuleId(childDependency.module)
if (!childDepId.endsWith("_bom") && childDepId !in ALLOWED_EMPTY_DEPS) {
// BOM dependencies are deps that only specify other deps as dependencies but have no
// artifact of their own. These typically have _bom at the end of their names but may also
// be identified by looking at their pom.xml file. For more context see maven's doc:
/* groovylint-disable-next-line LineLength */
// https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#bill-of-materials-bom-poms
throw new IllegalStateException(
"The dependency ${childDepId} has no children and no artifacts. If this is " +
'expected (e.g. for BOM dependencies), then please add it to the ' +
'|ALLOWED_EMPTY_DEPS| set.')
}
}
}
}
childDependenciesWithArtifacts.each { childDependency ->
childModules += makeModuleId(childDependency.module)
}
if (dependency.moduleArtifacts.empty) {
assert childModules : "${id} has no children and no artifacts."
dependencies.put(id, buildDepDescriptionNoArtifact(id, dependency, childModules))
childDependenciesWithArtifacts.each {
childDependency -> collectDependenciesInternal(childDependency)
}
} else if (!areAllModuleArtifactsSameFile(dependency.moduleArtifacts)) {
throw new IllegalStateException("The dependency ${id} has multiple different artifacts: " +
"${dependency.moduleArtifacts}")
} else {
ResolvedArtifact artifact = dependency.moduleArtifacts[0]
if (artifact.extension != 'jar' && artifact.extension != 'aar') {
throw new IllegalStateException("Type ${artifact.extension} of ${id} not supported.")
}
dependencies.put(id, buildDepDescription(id, dependency, artifact, childModules))
childDependenciesWithArtifacts.each {
childDependency -> collectDependenciesInternal(childDependency)
}
}
}
private boolean areAllModuleArtifactsSameFile(Set<ResolvedArtifact> artifacts) {
String expectedPath
for (ResolvedArtifact artifact : artifacts) {
String path = artifact.file.absolutePath
if (expectedPath == null) {
expectedPath = path
continue
}
if (expectedPath != path) {
return false
}
}
return true
}
private DependencyDescription buildDepDescriptionNoArtifact(
String id, ResolvedDependency dependency, List<String> childModules) {
return customizeDep(new DependencyDescription(
id: id,
group: dependency.module.id.group,
name: dependency.module.id.name,
version: dependency.module.id.version,
extension: 'group',
children: Collections.unmodifiableList(new ArrayList<>(childModules)),
directoryName: id.toLowerCase(),
displayName: dependency.module.id.name,
exclude: false,
cipdSuffix: DEFAULT_CIPD_SUFFIX,
))
}
private DependencyDescription buildDepDescription(
String id, ResolvedDependency dependency, ResolvedArtifact artifact, List<String> childModules) {
String pomUrl, repoUrl
GPathResult pomContent
(repoUrl, pomUrl, pomContent) = computePomFromArtifact(artifact)
List<LicenseSpec> licenses = []
if (!skipLicenses) {
licenses = resolveLicenseInformation(pomContent)
}
// Build |fileUrl| by swapping '.pom' file extension with artifact file extension.
String fileUrl = pomUrl[0..-4] + artifact.extension
// Check that the URL is correct explicitly here. Otherwise, we won't
// find out until 3pp bot runs.
checkDownloadable(fileUrl)
// Get rid of irrelevant indent that might be present in the XML file.
String description = pomContent.description?.text()?.trim()?.replaceAll(/\s+/, ' ')
String displayName = pomContent.name?.text()
displayName = displayName ?: dependency.module.id.name
return customizeDep(new DependencyDescription(
id: id,
artifact: artifact,
group: dependency.module.id.group,
name: dependency.module.id.name,
version: dependency.module.id.version,
extension: artifact.extension,
componentId: artifact.id.componentIdentifier,
children: Collections.unmodifiableList(new ArrayList<>(childModules)),
licenses: licenses,
directoryName: id.toLowerCase(),
fileName: artifact.file.name,
fileUrl: fileUrl,
repoUrl: repoUrl,
description: description,
url: pomContent.url?.text(),
displayName: displayName,
exclude: false,
cipdSuffix: DEFAULT_CIPD_SUFFIX,
))
}
private void customizeLicenses(DependencyDescription dep, PropertyOverride overrides) {
for (LicenseSpec license : dep.licenses) {
if (!license.url) {
continue
}
String normalizedLicenseUrl = license.url.replace('http://', 'https://')
String licenseOverridePath = LICENSE_OVERRIDES[normalizedLicenseUrl]
if (licenseOverridePath) {
license.url = ''
license.path = licenseOverridePath
}
}
if (dep.id?.startsWith('com_google_android_')) {
logger.debug("Using Android license for $dep.id")
dep.licenses.clear()
dep.licenses.add(new LicenseSpec(
name: 'Android Software Development Kit License',
path: 'licenses/Android_SDK_License-December_9_2016.txt'))
}
if (overrides) {
if (overrides.licenseName) {
dep.licenses.clear()
LicenseSpec license = new LicenseSpec(
name : overrides.licenseName,
path: overrides.licensePath,
url: overrides.licenseUrl,
)
dep.licenses.add(license)
} else {
if (overrides.licensePath || overrides.licenseUrl) {
throw new IllegalStateException('PropertyOverride must specify "licenseName" if either ' +
'"licensePath" or "licenseUrl" is specified.')
}
}
}
}
private DependencyDescription customizeDep(DependencyDescription dep) {
if (dep.id?.startsWith('com_google_android_')) {
// Many google dependencies don't set their URL, here is a good default.
dep.url = dep.url ?: 'https://developers.google.com/android/guides/setup'
} else if (dep.id?.startsWith('com_google_firebase_')) {
// Same as above for some firebase dependencies.
dep.url = dep.url ?: 'https://firebase.google.com'
}
PropertyOverride overrides = PROPERTY_OVERRIDES.get(dep.id)
if (overrides) {
logger.debug("Using override properties for $dep.id")
dep.with {
description = overrides.description ?: description
url = overrides.url ?: url
cipdSuffix = overrides.cipdSuffix ?: cipdSuffix
cpePrefix = overrides.cpePrefix ?: cpePrefix
// Boolean properties require explicit null checks instead of only when truish.
if (overrides.generateTarget != null) {
generateTarget = overrides.generateTarget
}
if (overrides.exclude != null) {
exclude = overrides.exclude
}
}
}
if (skipLicenses) {
dep.licenses = []
if (dep.id?.endsWith('license')) {
dep.exclude = true
}
} else {
customizeLicenses(dep, overrides)
}
return dep
}
private List<LicenseSpec> resolveLicenseInformation(GPathResult pomContent) {
GPathResult licenses = pomContent?.licenses?.license
if (!licenses) {
return []
}
List<LicenseSpec> out = []
for (GPathResult license : licenses) {
out.add(new LicenseSpec(
name: license.name.text(),
url: license.url.text()
))
}
return out
}
private List computePomFromArtifact(ResolvedArtifact artifact) {
ComponentIdentifier component = artifact.id.componentIdentifier
String componentPomSubpath = String.format('%s/%s/%s/%s-%s.pom',
component.group.replace('.', '/'),
component.module,
component.version,
component.module,
// While mavenCentral and google use "version", https://androidx.dev uses "timestampedVersion" as part
// of the file url
component.hasProperty('timestampedVersion') ? component.timestampedVersion : component.version)
List<String> repoUrls = []
for (Project project : projects) {
for (ArtifactRepository repository : project.repositories.asList()) {
String repoUrl = repository.properties.get('url')
// Some repo url may have trailing '/' and this breaks the file url generation below. So remove it if
// present.
if (repoUrl.endsWith('/')) {
repoUrl = repoUrl[0..-2]
}
// Deduplicate while collecting repo urls since subprojects (e.g. androidx) may use the same repos. Use
// a list instead of a set to preserve order. Since there are very few repositories, 2-3 per project,
// this O(n^2) complexity is acceptable.
if (repoUrls.contains(repoUrl)) {
continue
}
// If the component is from androidx, we likely need the nightly builds from androidx.dev, so check that
// repo first to avoid potential 404s. Inserting at the front preserves order between google and
// mavenCentral.
if (component.group.contains('androidx') && repoUrl.contains('androidx.dev')) {
repoUrls.add(0, repoUrl)
} else {
repoUrls.add(repoUrl)
}
}
}
for (String repoUrl : repoUrls) {
// Constructs the file url for pom. For example, with
// * repoUrl as "https://maven.google.com"
// * component.group as "android.arch.core"
// * component.module as "common"
// * component.version as "1.1.1"
//
// The file url will be: https://maven.google.com/android/arch/core/common/1.1.1/common-1.1.1.pom
String fileUrl = String.format('%s/%s', repoUrl, componentPomSubpath)
try {
GPathResult content = new XmlSlurper(
false /* validating */, false /* namespaceAware */).parse(fileUrl)
logger.debug("Succeeded in resolving url $fileUrl")
return [repoUrl, fileUrl, content]
} catch (any) {
logger.debug("Failed in resolving url $fileUrl")
}
}
throw new RuntimeException("Could not find pom from artifact $componentPomSubpath in $repoUrls")
}
private void checkDownloadable(String url) {
// file: URLs happen when using fetch_all_androidx.py --local-repo.
if (url.startsWith('file:')) {
if (!new File(new URI(url).getPath()).exists()) {
throw new RuntimeException('File not found: ' + url)
}
return
}
// Use a background thread to avoid slowing down main thread.
// Saves about 80 seconds currently.
new Thread().start(() -> {
HttpURLConnection http = new URL(url).openConnection()
http.requestMethod = 'HEAD'
if (http.responseCode != 200) {
/* groovylint-disable-next-line PrintStackTrace */
new RuntimeException("Resolved POM but could not resolve $url").printStackTrace()
// Exception is logged and ignored if thrown, so explicitly exit.
/* groovylint-disable-next-line SystemExit */
System.exit(1)
}
http.disconnect()
});
}
// Checks if currentVersion is lower than versionInQuestion.
private boolean isVersionLower(String currentVersion, String versionInQuestion) {
List verA = currentVersion.tokenize('.')
List verB = versionInQuestion.tokenize('.')
int commonIndices = Math.min(verA.size(), verB.size())
for (int i = 0; i < commonIndices; ++i) {
// toInteger could fail as some versions are 2.11.alpha-06.
// so revert to a string comparison.
try {
int numA = verA[i].toInteger()
int numB = verB[i].toInteger()
if (numA == numB) {
continue
}
return numA < numB
} catch (any) {
logger.debug('Using String comparison for a version check.')
// This could lead to issues where a version such as 2.11.alpha11
// is registered as less than 2.11.alpha9.
return verA[i] < verB[i]
}
}
// If we got this far then all the common indices are identical,
// so whichever version is longer is larger.
return verA.size() < verB.size()
}
@AutoClone
static class DependencyDescription {
String id
ResolvedArtifact artifact
String group, name, version, extension, displayName, description, url
List<LicenseSpec> licenses
String fileName, fileUrl
// |repoUrl| is the url to the repo that hosts this dep's artifact
// (|fileUrl|). Basically |fileurl|.startswith(|repoUrl|). |url| is the
// project homepage as supplied by the developer.
String repoUrl
// The local directory name to store the files like artifact, license file, 3pp subdirectory, and etc. Must be
// lowercase since 3pp uses the directory name as part of the CIPD names. However CIPD does not allow uppercase
// in names.
String directoryName
boolean supportsAndroid, visible, exclude, testOnly, isShipped, usedInBuild
boolean generateTarget = true
boolean licenseAndroidCompatible
ComponentIdentifier componentId
List<String> children
String cipdSuffix
String cpePrefix
// When set overrides the version downloaded by the 3pp fetch script to
// be, instead of the latest available, the resolved version by gradle
// in this run.
Boolean overrideLatest
// When set, consider only versions that contain this string.
String versionFilter
}
static class LicenseSpec {
String name, url, path
}
static class PropertyOverride {
String description
String url
String licenseName, licenseUrl, licensePath
String cipdSuffix
String cpePrefix
String resolveVersion
Boolean isShipped
// Set to true if this dependency is not needed.
Boolean exclude
// Set to false to skip creation of BUILD.gn target.
Boolean generateTarget
Boolean overrideLatest
String versionFilter
}
}