| #!/usr/bin/env python3 |
| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Generates an Android Studio project from a GN target.""" |
| |
| import argparse |
| import codecs |
| import collections |
| import glob |
| import json |
| import logging |
| import os |
| import pathlib |
| import re |
| import shutil |
| import subprocess |
| import sys |
| |
| _BUILD_ANDROID = os.path.join(os.path.dirname(__file__), os.pardir) |
| sys.path.append(_BUILD_ANDROID) |
| import devil_chromium |
| from devil.utils import run_tests_helper |
| from pylib import constants |
| from pylib.constants import host_paths |
| |
| sys.path.append(os.path.join(_BUILD_ANDROID, 'gyp')) |
| import jinja_template |
| from util import build_utils |
| from util import resource_utils |
| |
| sys.path.append(os.path.dirname(_BUILD_ANDROID)) |
| import gn_helpers |
| |
| # Typically these should track the versions that works on the slowest release |
| # channel, i.e. Android Studio stable. |
| _DEFAULT_ANDROID_GRADLE_PLUGIN_VERSION = '7.3.1' |
| _DEFAULT_KOTLIN_GRADLE_PLUGIN_VERSION = '1.8.0' |
| _DEFAULT_GRADLE_WRAPPER_VERSION = '7.4' |
| |
| _DEPOT_TOOLS_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', |
| 'depot_tools') |
| _DEFAULT_ANDROID_MANIFEST_PATH = os.path.join( |
| host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gradle', |
| 'AndroidManifest.xml') |
| _FILE_DIR = os.path.dirname(__file__) |
| _GENERATED_JAVA_SUBDIR = 'generated_java' |
| _JNI_LIBS_SUBDIR = 'symlinked-libs' |
| _ARMEABI_SUBDIR = 'armeabi' |
| _GRADLE_BUILD_FILE = 'build.gradle' |
| _CMAKE_FILE = 'CMakeLists.txt' |
| # This needs to come first alphabetically among all modules. |
| _MODULE_ALL = '_all' |
| _INSTRUMENTATION_TARGET_SUFFIX = '_test_apk__test_apk__apk' |
| |
| _DEFAULT_TARGETS = [ |
| '//android_webview/test/embedded_test_server:aw_net_test_support_apk', |
| '//android_webview/test:webview_instrumentation_apk', |
| '//android_webview/test:webview_instrumentation_test_apk', |
| '//base:base_junit_tests', |
| '//chrome/android:chrome_junit_tests', |
| '//chrome/android:chrome_public_apk', |
| '//chrome/android:chrome_public_test_apk', |
| '//chrome/android:chrome_public_unit_test_apk', |
| '//content/public/android:content_junit_tests', |
| '//content/shell/android:content_shell_apk', |
| # Below must be included even with --all since they are libraries. |
| '//base/android/jni_generator:jni_processor', |
| '//tools/android/errorprone_plugin:errorprone_plugin_java', |
| ] |
| |
| |
| def _TemplatePath(name): |
| return os.path.join(_FILE_DIR, '{}.jinja'.format(name)) |
| |
| |
| def _RebasePath(path_or_list, new_cwd=None, old_cwd=None): |
| """Makes the given path(s) relative to new_cwd, or absolute if not specified. |
| |
| If new_cwd is not specified, absolute paths are returned. |
| If old_cwd is not specified, constants.GetOutDirectory() is assumed. |
| """ |
| if path_or_list is None: |
| return [] |
| if not isinstance(path_or_list, str): |
| return [_RebasePath(p, new_cwd, old_cwd) for p in path_or_list] |
| if old_cwd is None: |
| old_cwd = constants.GetOutDirectory() |
| old_cwd = os.path.abspath(old_cwd) |
| if new_cwd: |
| new_cwd = os.path.abspath(new_cwd) |
| return os.path.relpath(os.path.join(old_cwd, path_or_list), new_cwd) |
| return os.path.abspath(os.path.join(old_cwd, path_or_list)) |
| |
| |
| def _WriteFile(path, data): |
| """Writes |data| to |path|, constucting parent directories if necessary.""" |
| logging.info('Writing %s', path) |
| dirname = os.path.dirname(path) |
| if not os.path.exists(dirname): |
| os.makedirs(dirname) |
| with codecs.open(path, 'w', 'utf-8') as output_file: |
| output_file.write(data) |
| |
| |
| def _RunGnGen(output_dir, args=None): |
| cmd = [os.path.join(_DEPOT_TOOLS_PATH, 'gn'), 'gen', output_dir] |
| if args: |
| cmd.extend(args) |
| logging.info('Running: %r', cmd) |
| subprocess.check_call(cmd) |
| |
| |
| def _RunNinja(output_dir, args): |
| # Don't use version within _DEPOT_TOOLS_PATH, since most devs don't use |
| # that one when building. |
| cmd = ['autoninja', '-C', output_dir] |
| cmd.extend(args) |
| logging.info('Running: %r', cmd) |
| subprocess.check_call(cmd) |
| |
| |
| def _QueryForAllGnTargets(output_dir): |
| cmd = [ |
| os.path.join(_BUILD_ANDROID, 'list_java_targets.py'), '--gn-labels', |
| '--nested', '--build', '--output-directory', output_dir |
| ] |
| logging.info('Running: %r', cmd) |
| return subprocess.check_output(cmd, encoding='UTF-8').splitlines() |
| |
| |
| class _ProjectEntry: |
| """Helper class for project entries.""" |
| |
| _cached_entries = {} |
| |
| def __init__(self, gn_target): |
| # Use _ProjectEntry.FromGnTarget instead for caching. |
| self._gn_target = gn_target |
| self._build_config = None |
| self._java_files = None |
| self._all_entries = None |
| self.android_test_entries = [] |
| |
| @classmethod |
| def FromGnTarget(cls, gn_target): |
| assert gn_target.startswith('//'), gn_target |
| if ':' not in gn_target: |
| gn_target = '%s:%s' % (gn_target, os.path.basename(gn_target)) |
| if gn_target not in cls._cached_entries: |
| cls._cached_entries[gn_target] = cls(gn_target) |
| return cls._cached_entries[gn_target] |
| |
| @classmethod |
| def FromBuildConfigPath(cls, path): |
| prefix = 'gen/' |
| suffix = '.build_config.json' |
| assert path.startswith(prefix) and path.endswith(suffix), path |
| subdir = path[len(prefix):-len(suffix)] |
| gn_target = '//%s:%s' % (os.path.split(subdir)) |
| return cls.FromGnTarget(gn_target) |
| |
| def __hash__(self): |
| return hash(self._gn_target) |
| |
| def __eq__(self, other): |
| return self._gn_target == other.GnTarget() |
| |
| def GnTarget(self): |
| return self._gn_target |
| |
| def NinjaTarget(self): |
| return self._gn_target[2:] |
| |
| def GradleSubdir(self): |
| """Returns the output subdirectory.""" |
| ninja_target = self.NinjaTarget() |
| # Support targets at the root level. e.g. //:foo |
| if ninja_target[0] == ':': |
| ninja_target = ninja_target[1:] |
| return ninja_target.replace(':', os.path.sep) |
| |
| def GeneratedJavaSubdir(self): |
| return _RebasePath( |
| os.path.join('gen', self.GradleSubdir(), _GENERATED_JAVA_SUBDIR)) |
| |
| def ProjectName(self): |
| """Returns the Gradle project name.""" |
| return self.GradleSubdir().replace(os.path.sep, '.') |
| |
| def BuildConfig(self): |
| """Reads and returns the project's .build_config.json JSON.""" |
| if not self._build_config: |
| path = os.path.join('gen', self.GradleSubdir() + '.build_config.json') |
| with open(_RebasePath(path)) as jsonfile: |
| self._build_config = json.load(jsonfile) |
| return self._build_config |
| |
| def DepsInfo(self): |
| return self.BuildConfig()['deps_info'] |
| |
| def Gradle(self): |
| return self.BuildConfig()['gradle'] |
| |
| def Javac(self): |
| return self.BuildConfig()['javac'] |
| |
| def GetType(self): |
| """Returns the target type from its .build_config.""" |
| return self.DepsInfo()['type'] |
| |
| def IsValid(self): |
| return self.GetType() in ( |
| 'android_apk', |
| 'android_app_bundle_module', |
| 'java_library', |
| "java_annotation_processor", |
| 'java_binary', |
| 'robolectric_binary', |
| ) |
| |
| def ResSources(self): |
| return self.DepsInfo().get('lint_resource_sources', []) |
| |
| def JavaFiles(self): |
| if self._java_files is None: |
| target_sources_file = self.DepsInfo().get('target_sources_file') |
| java_files = [] |
| if target_sources_file: |
| target_sources_file = _RebasePath(target_sources_file) |
| java_files = build_utils.ReadSourcesList(target_sources_file) |
| self._java_files = java_files |
| return self._java_files |
| |
| def PrebuiltJars(self): |
| return self.Gradle().get('dependent_prebuilt_jars', []) |
| |
| def AllEntries(self): |
| """Returns a list of all entries that the current entry depends on. |
| |
| This includes the entry itself to make iterating simpler.""" |
| if self._all_entries is None: |
| logging.debug('Generating entries for %s', self.GnTarget()) |
| deps = [_ProjectEntry.FromBuildConfigPath(p) |
| for p in self.Gradle()['dependent_android_projects']] |
| deps.extend(_ProjectEntry.FromBuildConfigPath(p) |
| for p in self.Gradle()['dependent_java_projects']) |
| all_entries = set() |
| for dep in deps: |
| all_entries.update(dep.AllEntries()) |
| all_entries.add(self) |
| self._all_entries = list(all_entries) |
| return self._all_entries |
| |
| |
| class _ProjectContextGenerator: |
| """Helper class to generate gradle build files""" |
| def __init__(self, project_dir, build_vars, use_gradle_process_resources, |
| jinja_processor, split_projects): |
| self.project_dir = project_dir |
| self.build_vars = build_vars |
| self.use_gradle_process_resources = use_gradle_process_resources |
| self.jinja_processor = jinja_processor |
| self.split_projects = split_projects |
| self.processed_java_dirs = set() |
| self.processed_prebuilts = set() |
| self.processed_res_dirs = set() |
| |
| def _GenJniLibs(self, root_entry): |
| libraries = [] |
| for entry in self._GetEntries(root_entry): |
| libraries += entry.BuildConfig().get('native', {}).get('libraries', []) |
| if libraries: |
| return _CreateJniLibsDir(constants.GetOutDirectory(), |
| self.EntryOutputDir(root_entry), libraries) |
| return [] |
| |
| def _GenJavaDirs(self, root_entry): |
| java_files = [] |
| for entry in self._GetEntries(root_entry): |
| java_files += entry.JavaFiles() |
| java_dirs, excludes = _ComputeJavaSourceDirsAndExcludes( |
| constants.GetOutDirectory(), java_files) |
| return java_dirs, excludes |
| |
| def _GenCustomManifest(self, entry): |
| """Returns the path to the generated AndroidManifest.xml. |
| |
| Gradle uses package id from manifest when generating R.class. So, we need |
| to generate a custom manifest if we let gradle process resources. We cannot |
| simply set android.defaultConfig.applicationId because it is not supported |
| for library targets.""" |
| resource_packages = entry.Javac().get('resource_packages') |
| if not resource_packages: |
| logging.debug( |
| 'Target %s includes resources from unknown package. ' |
| 'Unable to process with gradle.', entry.GnTarget()) |
| return _DEFAULT_ANDROID_MANIFEST_PATH |
| if len(resource_packages) > 1: |
| logging.debug( |
| 'Target %s includes resources from multiple packages. ' |
| 'Unable to process with gradle.', entry.GnTarget()) |
| return _DEFAULT_ANDROID_MANIFEST_PATH |
| |
| variables = {'package': resource_packages[0]} |
| data = self.jinja_processor.Render(_TemplatePath('manifest'), variables) |
| output_file = os.path.join( |
| self.EntryOutputDir(entry), 'AndroidManifest.xml') |
| _WriteFile(output_file, data) |
| |
| return output_file |
| |
| def _Relativize(self, entry, paths): |
| return _RebasePath(paths, self.EntryOutputDir(entry)) |
| |
| def _GetEntries(self, entry): |
| if self.split_projects: |
| return [entry] |
| return entry.AllEntries() |
| |
| def EntryOutputDir(self, entry): |
| return os.path.join(self.project_dir, entry.GradleSubdir()) |
| |
| def GeneratedInputs(self, root_entry): |
| generated_inputs = set() |
| for entry in self._GetEntries(root_entry): |
| generated_inputs.update(entry.PrebuiltJars()) |
| return generated_inputs |
| |
| def GenerateManifest(self, root_entry): |
| android_manifest = root_entry.DepsInfo().get('android_manifest') |
| if not android_manifest: |
| android_manifest = self._GenCustomManifest(root_entry) |
| return self._Relativize(root_entry, android_manifest) |
| |
| def Generate(self, root_entry): |
| # TODO(agrieve): Add an option to use interface jars and see if that speeds |
| # things up at all. |
| variables = {} |
| java_dirs, excludes = self._GenJavaDirs(root_entry) |
| java_dirs.extend( |
| e.GeneratedJavaSubdir() for e in self._GetEntries(root_entry)) |
| self.processed_java_dirs.update(java_dirs) |
| java_dirs.sort() |
| variables['java_dirs'] = self._Relativize(root_entry, java_dirs) |
| variables['java_excludes'] = excludes |
| variables['jni_libs'] = self._Relativize( |
| root_entry, set(self._GenJniLibs(root_entry))) |
| prebuilts = set( |
| p for e in self._GetEntries(root_entry) for p in e.PrebuiltJars()) |
| self.processed_prebuilts.update(prebuilts) |
| variables['prebuilts'] = self._Relativize(root_entry, prebuilts) |
| res_sources_files = _RebasePath( |
| set(p for e in self._GetEntries(root_entry) for p in e.ResSources())) |
| res_sources = [] |
| for res_sources_file in res_sources_files: |
| res_sources.extend(build_utils.ReadSourcesList(res_sources_file)) |
| res_dirs = resource_utils.DeduceResourceDirsFromFileList(res_sources) |
| # Do not add generated resources for the all module since it creates many |
| # duplicates, and currently resources are only used for editing. |
| self.processed_res_dirs.update(res_dirs) |
| variables['res_dirs'] = self._Relativize(root_entry, res_dirs) |
| if self.split_projects: |
| deps = [_ProjectEntry.FromBuildConfigPath(p) |
| for p in root_entry.Gradle()['dependent_android_projects']] |
| variables['android_project_deps'] = [d.ProjectName() for d in deps] |
| deps = [_ProjectEntry.FromBuildConfigPath(p) |
| for p in root_entry.Gradle()['dependent_java_projects']] |
| variables['java_project_deps'] = [d.ProjectName() for d in deps] |
| return variables |
| |
| |
| def _ComputeJavaSourceDirs(java_files): |
| """Returns a dictionary of source dirs with each given files in one.""" |
| found_roots = {} |
| for path in java_files: |
| path_root = path |
| # Recognize these tokens as top-level. |
| while True: |
| path_root = os.path.dirname(path_root) |
| basename = os.path.basename(path_root) |
| assert basename, 'Failed to find source dir for ' + path |
| if basename in ('java', 'src'): |
| break |
| if basename in ('javax', 'org', 'com'): |
| path_root = os.path.dirname(path_root) |
| break |
| if path_root not in found_roots: |
| found_roots[path_root] = [] |
| found_roots[path_root].append(path) |
| return found_roots |
| |
| |
| def _ComputeExcludeFilters(wanted_files, unwanted_files, parent_dir): |
| """Returns exclude patters to exclude unwanted files but keep wanted files. |
| |
| - Shortens exclude list by globbing if possible. |
| - Exclude patterns are relative paths from the parent directory. |
| """ |
| excludes = [] |
| files_to_include = set(wanted_files) |
| files_to_exclude = set(unwanted_files) |
| while files_to_exclude: |
| unwanted_file = files_to_exclude.pop() |
| target_exclude = os.path.join( |
| os.path.dirname(unwanted_file), '*.java') |
| found_files = set(glob.glob(target_exclude)) |
| valid_files = found_files & files_to_include |
| if valid_files: |
| excludes.append(os.path.relpath(unwanted_file, parent_dir)) |
| else: |
| excludes.append(os.path.relpath(target_exclude, parent_dir)) |
| files_to_exclude -= found_files |
| return excludes |
| |
| |
| def _ComputeJavaSourceDirsAndExcludes(output_dir, source_files): |
| """Computes the list of java source directories and exclude patterns. |
| |
| This includes both Java and Kotlin files since both are listed in the same |
| "java" section for gradle. |
| |
| 1. Computes the root source directories from the list of files. |
| 2. Compute exclude patterns that exclude all extra files only. |
| 3. Returns the list of source directories and exclude patterns. |
| """ |
| java_dirs = [] |
| excludes = [] |
| if source_files: |
| source_files = _RebasePath(source_files) |
| computed_dirs = _ComputeJavaSourceDirs(source_files) |
| java_dirs = list(computed_dirs.keys()) |
| all_found_source_files = set() |
| |
| for directory, files in computed_dirs.items(): |
| found_source_files = (build_utils.FindInDirectory(directory, '*.java') + |
| build_utils.FindInDirectory(directory, '*.kt')) |
| all_found_source_files.update(found_source_files) |
| unwanted_source_files = set(found_source_files) - set(files) |
| if unwanted_source_files: |
| logging.debug('Directory requires excludes: %s', directory) |
| excludes.extend( |
| _ComputeExcludeFilters(files, unwanted_source_files, directory)) |
| |
| missing_source_files = set(source_files) - all_found_source_files |
| # Warn only about non-generated files that are missing. |
| missing_source_files = [ |
| p for p in missing_source_files if not p.startswith(output_dir) |
| ] |
| if missing_source_files: |
| logging.warning('Some source files were not found: %s', |
| missing_source_files) |
| |
| return java_dirs, excludes |
| |
| |
| def _CreateRelativeSymlink(target_path, link_path): |
| link_dir = os.path.dirname(link_path) |
| relpath = os.path.relpath(target_path, link_dir) |
| logging.debug('Creating symlink %s -> %s', link_path, relpath) |
| os.symlink(relpath, link_path) |
| |
| |
| def _CreateJniLibsDir(output_dir, entry_output_dir, so_files): |
| """Creates directory with symlinked .so files if necessary. |
| |
| Returns list of JNI libs directories.""" |
| |
| if so_files: |
| symlink_dir = os.path.join(entry_output_dir, _JNI_LIBS_SUBDIR) |
| shutil.rmtree(symlink_dir, True) |
| abi_dir = os.path.join(symlink_dir, _ARMEABI_SUBDIR) |
| if not os.path.exists(abi_dir): |
| os.makedirs(abi_dir) |
| for so_file in so_files: |
| target_path = os.path.join(output_dir, so_file) |
| symlinked_path = os.path.join(abi_dir, so_file) |
| _CreateRelativeSymlink(target_path, symlinked_path) |
| |
| return [symlink_dir] |
| |
| return [] |
| |
| |
| def _ParseVersionFromFile(file_path, version_regex_string, default_version): |
| if os.path.exists(file_path): |
| content = pathlib.Path(file_path).read_text() |
| match = re.search(version_regex_string, content) |
| if match: |
| version = match.group(1) |
| logging.info('Using existing version %s in %s.', version, file_path) |
| return version |
| logging.warning('Unable to find %s in %s:\n%s', version_regex_string, |
| file_path, content) |
| return default_version |
| |
| |
| def _GenerateLocalProperties(sdk_dir): |
| """Returns the data for local.properties as a string.""" |
| return '\n'.join([ |
| '# Generated by //build/android/gradle/generate_gradle.py', |
| 'sdk.dir=%s' % sdk_dir, |
| '', |
| ]) |
| |
| |
| def _GenerateGradleWrapperProperties(file_path): |
| """Returns the data for gradle-wrapper.properties as a string.""" |
| |
| version = _ParseVersionFromFile(file_path, |
| r'/distributions/gradle-([\d.]+)-all.zip', |
| _DEFAULT_GRADLE_WRAPPER_VERSION) |
| |
| return '\n'.join([ |
| '# Generated by //build/android/gradle/generate_gradle.py', |
| ('distributionUrl=https\\://services.gradle.org' |
| f'/distributions/gradle-{version}-all.zip'), |
| '', |
| ]) |
| |
| |
| def _GenerateGradleProperties(): |
| """Returns the data for gradle.properties as a string.""" |
| return '\n'.join([ |
| '# Generated by //build/android/gradle/generate_gradle.py', |
| '', |
| '# Tells Gradle to show warnings during project sync.', |
| 'org.gradle.warning.mode=all', |
| '', |
| ]) |
| |
| |
| def _GenerateBaseVars(generator, build_vars): |
| variables = {} |
| # Avoid pre-release SDKs since Studio might not know how to download them. |
| variables['compile_sdk_version'] = ('android-%s' % |
| build_vars['public_android_sdk_version']) |
| target_sdk_version = build_vars['public_android_sdk_version'] |
| if str(target_sdk_version).isalpha(): |
| target_sdk_version = '"{}"'.format(target_sdk_version) |
| variables['target_sdk_version'] = target_sdk_version |
| variables['min_sdk_version'] = build_vars['default_min_sdk_version'] |
| variables['use_gradle_process_resources'] = ( |
| generator.use_gradle_process_resources) |
| return variables |
| |
| |
| def _GenerateGradleFile(entry, generator, build_vars, jinja_processor): |
| """Returns the data for a project's build.gradle.""" |
| deps_info = entry.DepsInfo() |
| variables = _GenerateBaseVars(generator, build_vars) |
| sourceSetName = 'main' |
| |
| if deps_info['type'] == 'android_apk': |
| target_type = 'android_apk' |
| elif deps_info['type'] in ('java_library', 'java_annotation_processor'): |
| is_prebuilt = deps_info.get('is_prebuilt', False) |
| gradle_treat_as_prebuilt = deps_info.get('gradle_treat_as_prebuilt', False) |
| if is_prebuilt or gradle_treat_as_prebuilt: |
| return None |
| if deps_info['requires_android']: |
| target_type = 'android_library' |
| else: |
| target_type = 'java_library' |
| elif deps_info['type'] == 'java_binary': |
| target_type = 'java_binary' |
| variables['main_class'] = deps_info.get('main_class') |
| elif deps_info['type'] == 'robolectric_binary': |
| target_type = 'android_junit' |
| sourceSetName = 'test' |
| else: |
| return None |
| |
| variables['target_name'] = os.path.splitext(deps_info['name'])[0] |
| variables['template_type'] = target_type |
| variables['main'] = {} |
| variables[sourceSetName] = generator.Generate(entry) |
| variables['main']['android_manifest'] = generator.GenerateManifest(entry) |
| |
| if entry.android_test_entries: |
| variables['android_test'] = [] |
| for e in entry.android_test_entries: |
| test_entry = generator.Generate(e) |
| test_entry['android_manifest'] = generator.GenerateManifest(e) |
| variables['android_test'].append(test_entry) |
| for key, value in test_entry.items(): |
| if isinstance(value, list): |
| test_entry[key] = sorted(set(value) - set(variables['main'][key])) |
| |
| return jinja_processor.Render( |
| _TemplatePath(target_type.split('_')[0]), variables) |
| |
| |
| # Example: //chrome/android:monochrome |
| def _GetNative(relative_func, target_names): |
| """Returns an object containing native c++ sources list and its included path |
| |
| Iterate through all target_names and their deps to get the list of included |
| paths and sources.""" |
| out_dir = constants.GetOutDirectory() |
| with open(os.path.join(out_dir, 'project.json'), 'r') as project_file: |
| projects = json.load(project_file) |
| project_targets = projects['targets'] |
| root_dir = projects['build_settings']['root_path'] |
| includes = set() |
| processed_target = set() |
| targets_stack = list(target_names) |
| sources = [] |
| |
| while targets_stack: |
| target_name = targets_stack.pop() |
| if target_name in processed_target: |
| continue |
| processed_target.add(target_name) |
| target = project_targets[target_name] |
| includes.update(target.get('include_dirs', [])) |
| targets_stack.extend(target.get('deps', [])) |
| # Ignore generated files |
| sources.extend(f for f in target.get('sources', []) |
| if f.endswith('.cc') and not f.startswith('//out')) |
| |
| def process_paths(paths): |
| # Ignores leading // |
| return relative_func( |
| sorted(os.path.join(root_dir, path[2:]) for path in paths)) |
| |
| return { |
| 'sources': process_paths(sources), |
| 'includes': process_paths(includes), |
| } |
| |
| |
| def _GenerateModuleAll(gradle_output_dir, generator, build_vars, |
| jinja_processor, native_targets): |
| """Returns the data for a pseudo build.gradle of all dirs. |
| |
| See //docs/android_studio.md for more details.""" |
| variables = _GenerateBaseVars(generator, build_vars) |
| target_type = 'android_apk' |
| variables['target_name'] = _MODULE_ALL |
| variables['template_type'] = target_type |
| java_dirs = sorted(generator.processed_java_dirs) |
| prebuilts = sorted(generator.processed_prebuilts) |
| res_dirs = sorted(generator.processed_res_dirs) |
| def Relativize(paths): |
| return _RebasePath(paths, os.path.join(gradle_output_dir, _MODULE_ALL)) |
| |
| # As after clank modularization, the java and javatests code will live side by |
| # side in the same module, we will list both of them in the main target here. |
| main_java_dirs = [d for d in java_dirs if 'junit/' not in d] |
| junit_test_java_dirs = [d for d in java_dirs if 'junit/' in d] |
| variables['main'] = { |
| 'android_manifest': Relativize(_DEFAULT_ANDROID_MANIFEST_PATH), |
| 'java_dirs': Relativize(main_java_dirs), |
| 'prebuilts': Relativize(prebuilts), |
| 'java_excludes': ['**/*.java', '**/*.kt'], |
| 'res_dirs': Relativize(res_dirs), |
| } |
| variables['android_test'] = [{ |
| 'java_dirs': Relativize(junit_test_java_dirs), |
| 'java_excludes': ['**/*.java', '**/*.kt'], |
| }] |
| if native_targets: |
| variables['native'] = _GetNative( |
| relative_func=Relativize, target_names=native_targets) |
| data = jinja_processor.Render( |
| _TemplatePath(target_type.split('_')[0]), variables) |
| _WriteFile( |
| os.path.join(gradle_output_dir, _MODULE_ALL, _GRADLE_BUILD_FILE), data) |
| if native_targets: |
| cmake_data = jinja_processor.Render(_TemplatePath('cmake'), variables) |
| _WriteFile( |
| os.path.join(gradle_output_dir, _MODULE_ALL, _CMAKE_FILE), cmake_data) |
| |
| |
| def _GenerateRootGradle(jinja_processor, file_path): |
| """Returns the data for the root project's build.gradle.""" |
| android_gradle_plugin_version = _ParseVersionFromFile( |
| file_path, r'com.android.tools.build:gradle:([\d.]+)', |
| _DEFAULT_ANDROID_GRADLE_PLUGIN_VERSION) |
| kotlin_gradle_plugin_version = _ParseVersionFromFile( |
| file_path, r'org.jetbrains.kotlin:kotlin-gradle-plugin:([\d.]+)', |
| _DEFAULT_KOTLIN_GRADLE_PLUGIN_VERSION) |
| |
| return jinja_processor.Render( |
| _TemplatePath('root'), { |
| 'android_gradle_plugin_version': android_gradle_plugin_version, |
| 'kotlin_gradle_plugin_version': kotlin_gradle_plugin_version, |
| }) |
| |
| |
| def _GenerateSettingsGradle(project_entries): |
| """Returns the data for settings.gradle.""" |
| project_name = os.path.basename(os.path.dirname(host_paths.DIR_SOURCE_ROOT)) |
| lines = [] |
| lines.append('// Generated by //build/android/gradle/generate_gradle.py') |
| lines.append('rootProject.name = "%s"' % project_name) |
| lines.append('rootProject.projectDir = settingsDir') |
| lines.append('') |
| for name, subdir in project_entries: |
| # Example target: |
| # android_webview:android_webview_java__build_config_crbug_908819 |
| lines.append('include ":%s"' % name) |
| lines.append('project(":%s").projectDir = new File(settingsDir, "%s")' % |
| (name, subdir)) |
| return '\n'.join(lines) |
| |
| |
| def _FindAllProjectEntries(main_entries): |
| """Returns the list of all _ProjectEntry instances given the root project.""" |
| found = set() |
| to_scan = list(main_entries) |
| while to_scan: |
| cur_entry = to_scan.pop() |
| if cur_entry in found: |
| continue |
| found.add(cur_entry) |
| sub_config_paths = cur_entry.DepsInfo()['deps_configs'] |
| to_scan.extend( |
| _ProjectEntry.FromBuildConfigPath(p) for p in sub_config_paths) |
| return list(found) |
| |
| |
| def _CombineTestEntries(entries): |
| """Combines test apks into the androidTest source set of their target. |
| |
| - Speeds up android studio |
| - Adds proper dependency between test and apk_under_test |
| - Doesn't work for junit yet due to resulting circular dependencies |
| - e.g. base_junit_tests > base_junit_test_support > base_java |
| """ |
| combined_entries = [] |
| android_test_entries = collections.defaultdict(list) |
| for entry in entries: |
| target_name = entry.GnTarget() |
| if (target_name.endswith(_INSTRUMENTATION_TARGET_SUFFIX) |
| and 'apk_under_test' in entry.Gradle()): |
| apk_name = entry.Gradle()['apk_under_test'] |
| android_test_entries[apk_name].append(entry) |
| else: |
| combined_entries.append(entry) |
| for entry in combined_entries: |
| target_name = entry.DepsInfo()['name'] |
| if target_name in android_test_entries: |
| entry.android_test_entries = android_test_entries[target_name] |
| del android_test_entries[target_name] |
| # Add unmatched test entries as individual targets. |
| combined_entries.extend(e for l in android_test_entries.values() for e in l) |
| return combined_entries |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--output-directory', |
| help='Path to the root build directory.') |
| parser.add_argument('-v', |
| '--verbose', |
| dest='verbose_count', |
| default=0, |
| action='count', |
| help='Verbose level') |
| parser.add_argument('--target', |
| dest='targets', |
| action='append', |
| help='GN target to generate project for. Replaces set of ' |
| 'default targets. May be repeated.') |
| parser.add_argument('--extra-target', |
| dest='extra_targets', |
| action='append', |
| help='GN target to generate project for, in addition to ' |
| 'the default ones. May be repeated.') |
| parser.add_argument('--project-dir', |
| help='Root of the output project.', |
| default=os.path.join('$CHROMIUM_OUTPUT_DIR', 'gradle')) |
| parser.add_argument('--all', |
| action='store_true', |
| help='Include all .java files reachable from any ' |
| 'apk/test/binary target. On by default unless ' |
| '--split-projects is used (--split-projects can ' |
| 'slow down Studio given too many targets).') |
| parser.add_argument('--use-gradle-process-resources', |
| action='store_true', |
| help='Have gradle generate R.java rather than ninja') |
| parser.add_argument('--split-projects', |
| action='store_true', |
| help='Split projects by their gn deps rather than ' |
| 'combining all the dependencies of each target') |
| parser.add_argument('--native-target', |
| dest='native_targets', |
| action='append', |
| help='GN native targets to generate for. May be ' |
| 'repeated.') |
| parser.add_argument( |
| '--sdk-path', |
| default=os.path.expanduser('~/Android/Sdk'), |
| help='The path to use as the SDK root, overrides the ' |
| 'default at ~/Android/Sdk.') |
| args = parser.parse_args() |
| if args.output_directory: |
| constants.SetOutputDirectory(args.output_directory) |
| constants.CheckOutputDirectory() |
| output_dir = constants.GetOutDirectory() |
| devil_chromium.Initialize(output_directory=output_dir) |
| run_tests_helper.SetLogLevel(args.verbose_count) |
| |
| if args.use_gradle_process_resources: |
| assert args.split_projects, ( |
| 'Gradle resources does not work without --split-projects.') |
| |
| _gradle_output_dir = os.path.abspath( |
| args.project_dir.replace('$CHROMIUM_OUTPUT_DIR', output_dir)) |
| logging.warning('Creating project at: %s', _gradle_output_dir) |
| |
| # Generate for "all targets" by default when not using --split-projects (too |
| # slow), and when no --target has been explicitly set. "all targets" means all |
| # java targets that are depended on by an apk or java_binary (leaf |
| # java_library targets will not be included). |
| args.all = args.all or (not args.split_projects and not args.targets) |
| |
| targets_from_args = set(args.targets or _DEFAULT_TARGETS) |
| if args.extra_targets: |
| targets_from_args.update(args.extra_targets) |
| |
| if args.all: |
| if args.native_targets: |
| _RunGnGen(output_dir, ['--ide=json']) |
| elif not os.path.exists(os.path.join(output_dir, 'build.ninja')): |
| _RunGnGen(output_dir) |
| else: |
| # Faster than running "gn gen" in the no-op case. |
| _RunNinja(output_dir, ['build.ninja']) |
| # Query ninja for all __build_config_crbug_908819 targets. |
| targets = _QueryForAllGnTargets(output_dir) |
| else: |
| assert not args.native_targets, 'Native editing requires --all.' |
| targets = [ |
| re.sub(r'_test_apk$', _INSTRUMENTATION_TARGET_SUFFIX, t) |
| for t in targets_from_args |
| ] |
| # Necessary after "gn clean" |
| if not os.path.exists( |
| os.path.join(output_dir, gn_helpers.BUILD_VARS_FILENAME)): |
| _RunGnGen(output_dir) |
| |
| build_vars = gn_helpers.ReadBuildVars(output_dir) |
| jinja_processor = jinja_template.JinjaProcessor(_FILE_DIR) |
| generator = _ProjectContextGenerator(_gradle_output_dir, build_vars, |
| args.use_gradle_process_resources, |
| jinja_processor, args.split_projects) |
| |
| main_entries = [_ProjectEntry.FromGnTarget(t) for t in targets] |
| |
| if args.all: |
| # There are many unused libraries, so restrict to those that are actually |
| # used by apks/bundles/binaries/tests or that are explicitly mentioned in |
| # --targets. |
| BASE_TYPES = ('android_apk', 'android_app_bundle_module', 'java_binary', |
| 'robolectric_binary') |
| main_entries = [ |
| e for e in main_entries |
| if (e.GetType() in BASE_TYPES or e.GnTarget() in targets_from_args |
| or e.GnTarget().endswith(_INSTRUMENTATION_TARGET_SUFFIX)) |
| ] |
| |
| if args.split_projects: |
| main_entries = _FindAllProjectEntries(main_entries) |
| |
| logging.info('Generating for %d targets.', len(main_entries)) |
| |
| entries = [e for e in _CombineTestEntries(main_entries) if e.IsValid()] |
| logging.info('Creating %d projects for targets.', len(entries)) |
| |
| logging.warning('Writing .gradle files...') |
| project_entries = [] |
| # When only one entry will be generated we want it to have a valid |
| # build.gradle file with its own AndroidManifest. |
| for entry in entries: |
| data = _GenerateGradleFile(entry, generator, build_vars, jinja_processor) |
| if data and not args.all: |
| project_entries.append((entry.ProjectName(), entry.GradleSubdir())) |
| _WriteFile( |
| os.path.join(generator.EntryOutputDir(entry), _GRADLE_BUILD_FILE), |
| data) |
| if args.all: |
| project_entries.append((_MODULE_ALL, _MODULE_ALL)) |
| _GenerateModuleAll(_gradle_output_dir, generator, build_vars, |
| jinja_processor, args.native_targets) |
| |
| root_gradle_path = os.path.join(generator.project_dir, _GRADLE_BUILD_FILE) |
| _WriteFile(root_gradle_path, |
| _GenerateRootGradle(jinja_processor, root_gradle_path)) |
| |
| _WriteFile(os.path.join(generator.project_dir, 'settings.gradle'), |
| _GenerateSettingsGradle(project_entries)) |
| |
| # Ensure the Android Studio sdk is correctly initialized. |
| if not os.path.exists(args.sdk_path): |
| # Help first-time users avoid Android Studio forcibly changing back to |
| # the previous default due to not finding a valid sdk under this dir. |
| shutil.copytree(_RebasePath(build_vars['android_sdk_root']), args.sdk_path) |
| _WriteFile( |
| os.path.join(generator.project_dir, 'local.properties'), |
| _GenerateLocalProperties(args.sdk_path)) |
| _WriteFile(os.path.join(generator.project_dir, 'gradle.properties'), |
| _GenerateGradleProperties()) |
| |
| wrapper_properties = os.path.join(generator.project_dir, 'gradle', 'wrapper', |
| 'gradle-wrapper.properties') |
| _WriteFile(wrapper_properties, |
| _GenerateGradleWrapperProperties(wrapper_properties)) |
| |
| generated_inputs = set() |
| for entry in entries: |
| entries_to_gen = [entry] |
| entries_to_gen.extend(entry.android_test_entries) |
| for entry_to_gen in entries_to_gen: |
| # Build all paths references by .gradle that exist within output_dir. |
| generated_inputs.update(generator.GeneratedInputs(entry_to_gen)) |
| if generated_inputs: |
| # Skip targets outside the output_dir since those are not generated. |
| targets = [ |
| p for p in _RebasePath(generated_inputs, output_dir) |
| if not p.startswith(os.pardir) |
| ] |
| _RunNinja(output_dir, targets) |
| |
| print('Generated projects for Android Studio.') |
| print('** Building using Android Studio / Gradle does not work.') |
| print('** This project is only for IDE editing & tools.') |
| print('Note: Generated files will appear only if they have been built') |
| print('For more tips: https://chromium.googlesource.com/chromium/src.git/' |
| '+/main/docs/android_studio.md') |
| |
| |
| if __name__ == '__main__': |
| main() |