Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Generates an Android Studio project from a GN target.""" | |
| 7 | |
| 8 import argparse | |
| 9 import codecs | |
| 10 import logging | |
| 11 import os | |
| 12 import shutil | |
| 13 import subprocess | |
| 14 import sys | |
| 15 import zipfile | |
| 16 | |
| 17 _BUILD_ANDROID = os.path.join(os.path.dirname(__file__), os.pardir) | |
| 18 sys.path.append(_BUILD_ANDROID) | |
| 19 from pylib import constants | |
| 20 from pylib.constants import host_paths | |
| 21 from pylib.utils import run_tests_helper | |
|
jbudorick
2016/07/12 19:20:17
from devil.utils import run_tests_helper
pylib.ut
agrieve
2016/07/13 14:09:08
Done.
| |
| 22 | |
| 23 sys.path.append(os.path.join(_BUILD_ANDROID, 'gyp')) | |
| 24 import jinja_template | |
| 25 from util import build_utils | |
| 26 | |
| 27 | |
| 28 _DEFAULT_ANDROID_MANIFEST_PATH = os.path.join( | |
| 29 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'AndroidManifest.xml') | |
| 30 _JINJA_TEMPLATE_PATH = os.path.join( | |
| 31 os.path.dirname(__file__), 'build.gradle.jinja') | |
| 32 | |
| 33 _JAVA_SUBDIR = 'symlinked-java' | |
| 34 _SRCJARS_SUBDIR = 'extracted-srcjars' | |
| 35 | |
| 36 | |
| 37 def _RebasePath(path_or_list, new_cwd=None, old_cwd=None): | |
| 38 """Makes the given path(s) relative to new_cwd, or absolute if not specified. | |
| 39 | |
| 40 If new_cwd is not specified, absolute paths are returned. | |
| 41 If old_cwd is not specified, constants.GetOutDirectory() is assumed. | |
| 42 """ | |
| 43 if not isinstance(path_or_list, basestring): | |
| 44 return [_RebasePath(p, new_cwd, old_cwd) for p in path_or_list] | |
| 45 if old_cwd is None: | |
| 46 old_cwd = constants.GetOutDirectory() | |
| 47 old_cwd = os.path.abspath(old_cwd) | |
| 48 if new_cwd: | |
| 49 new_cwd = os.path.abspath(new_cwd) | |
| 50 return os.path.relpath(os.path.join(old_cwd, path_or_list), new_cwd) | |
| 51 return os.path.abspath(os.path.join(old_cwd, path_or_list)) | |
| 52 | |
| 53 | |
| 54 def _IsSubpathOf(child, parent): | |
| 55 """Returns whether |child| is a subpath of |parent|.""" | |
| 56 return not os.path.relpath(child, parent).startswith(os.pardir) | |
| 57 | |
| 58 | |
| 59 def _WriteFile(path, data): | |
| 60 """Writes |data| to |path|, constucting parent directories if necessary.""" | |
| 61 logging.info('Writing %s', path) | |
| 62 dirname = os.path.dirname(path) | |
| 63 if not os.path.exists(dirname): | |
| 64 os.makedirs(dirname) | |
| 65 with codecs.open(path, 'w', 'utf-8') as output_file: | |
| 66 output_file.write(data) | |
| 67 | |
| 68 | |
| 69 def _RunNinja(output_dir, ninja_targets): | |
| 70 cmd = ['ninja', '-C', output_dir, '-j50'] | |
| 71 cmd.extend(ninja_targets) | |
| 72 logging.info('Running: %r', cmd) | |
| 73 subprocess.check_call(cmd) | |
| 74 | |
| 75 | |
| 76 class _ProjectEntry(object): | |
| 77 """Helper class for various path transformations.""" | |
| 78 def __init__(self, gn_target): | |
| 79 assert gn_target.startswith('//') and ':' in gn_target, gn_target | |
|
jbudorick
2016/07/12 19:20:17
Is the second half of this condition necessarily t
agrieve
2016/07/13 14:09:07
Done.
| |
| 80 self._gn_target = gn_target | |
| 81 self._build_config = None | |
| 82 | |
| 83 @classmethod | |
| 84 def FromBuildConfigPath(cls, path): | |
| 85 prefix = 'gen/' | |
| 86 suffix = '.build_config' | |
| 87 assert path.startswith(prefix) and path.endswith(suffix), path | |
| 88 subdir = path[len(prefix):-len(suffix)] | |
| 89 return cls('//%s:%s' % (os.path.split(subdir))) | |
| 90 | |
| 91 def __hash__(self): | |
| 92 return hash(self._gn_target) | |
| 93 | |
| 94 def __eq__(self, other): | |
| 95 return self._gn_target == other.GnTarget() | |
| 96 | |
| 97 def GnTarget(self): | |
| 98 return self._gn_target | |
| 99 | |
| 100 def NinjaTarget(self): | |
| 101 return self._gn_target[2:] | |
| 102 | |
| 103 def GnBuildConfigTarget(self): | |
| 104 return '%s__build_config' % self._gn_target | |
| 105 | |
| 106 def NinjaBuildConfigTarget(self): | |
| 107 return '%s__build_config' % self.NinjaTarget() | |
| 108 | |
| 109 def GradleSubdir(self): | |
| 110 """Returns the output subdirectory.""" | |
| 111 return self.NinjaTarget().replace(':', os.path.sep) | |
| 112 | |
| 113 def ProjectName(self): | |
| 114 """Returns the Gradle project name.""" | |
| 115 return self.GradleSubdir().replace(os.path.sep, '\\$') | |
| 116 | |
| 117 def BuildConfig(self): | |
| 118 """Reads and returns the project's .build_config JSON.""" | |
| 119 if not self._build_config: | |
| 120 path = os.path.join('gen', self.GradleSubdir() + '.build_config') | |
| 121 self._build_config = build_utils.ReadJson(_RebasePath(path)) | |
| 122 return self._build_config | |
| 123 | |
| 124 | |
| 125 def _ComputeJavaSourceDirs(java_files): | |
| 126 """Returns the list of source directories for the given files.""" | |
| 127 found_roots = set() | |
| 128 for path in java_files: | |
| 129 path_root = path | |
| 130 # Recognize these tokens as top-level. | |
| 131 while os.path.basename(path_root) not in ('javax', 'org', 'com', 'src'): | |
| 132 assert path_root, 'Failed to find source dir for ' + path | |
| 133 path_root = os.path.dirname(path_root) | |
| 134 # Assume that if we've hit "src", the we're at the root. | |
| 135 if os.path.basename(path_root) != 'src': | |
| 136 path_root = os.path.dirname(path_root) | |
| 137 found_roots.add(path_root) | |
| 138 return list(found_roots) | |
| 139 | |
| 140 | |
| 141 def _CreateSymlinkTree(entry_output_dir, symlink_dir, desired_files, | |
| 142 parent_dirs): | |
| 143 """Creates a directory tree of symlinks to the given files. | |
| 144 | |
| 145 The idea here is to replicate a directory tree while leaving out files within | |
| 146 it not listed by |desired_files|. | |
| 147 """ | |
| 148 assert _IsSubpathOf(symlink_dir, entry_output_dir) | |
| 149 if os.path.exists(symlink_dir): | |
| 150 shutil.rmtree(symlink_dir) | |
| 151 | |
| 152 for target_path in desired_files: | |
| 153 prefix = next(d for d in parent_dirs if target_path.startswith(d)) | |
| 154 subpath = os.path.relpath(target_path, prefix) | |
| 155 symlinked_path = os.path.join(symlink_dir, subpath) | |
| 156 symlinked_dir = os.path.dirname(symlinked_path) | |
| 157 if not os.path.exists(symlinked_dir): | |
| 158 os.makedirs(symlinked_dir) | |
| 159 relpath = os.path.relpath(target_path, symlinked_dir) | |
| 160 logging.debug('Creating symlink %s -> %s', symlinked_path, relpath) | |
| 161 os.symlink(relpath, symlinked_path) | |
| 162 | |
| 163 | |
| 164 def _CreateJavaSourceDir(entry_output_dir, java_sources_file): | |
| 165 """Computes and constructs when necessary the list of java source directories. | |
| 166 | |
| 167 1. Computes the root java source directories from the list of files. | |
| 168 2. Determines whether there are any .java files in them that are not included | |
| 169 in |java_sources_file|. | |
| 170 3. If not, returns the list of java source directories. If so, constructs a | |
| 171 tree of symlinks within |entry_output_dir| of all files in | |
| 172 |java_sources_file|. | |
| 173 """ | |
| 174 java_dirs = [] | |
| 175 if java_sources_file: | |
| 176 with open(java_sources_file) as f: | |
| 177 java_files = _RebasePath([l.strip() for l in f]) | |
| 178 java_dirs = _ComputeJavaSourceDirs(java_files) | |
| 179 | |
| 180 found_java_files = build_utils.FindInDirectories(java_dirs, '*.java') | |
| 181 unwanted_java_files = set(found_java_files) - set(java_files) | |
|
jbudorick
2016/07/12 19:20:17
Is it possible for there to be missing files? (i.e
agrieve
2016/07/13 14:09:08
It's possible, but I would expect "gn gen" to fail
| |
| 182 if unwanted_java_files: | |
| 183 logging.debug('Target requires .java symlinks: %s', entry_output_dir) | |
| 184 symlink_dir = os.path.join(entry_output_dir, _JAVA_SUBDIR) | |
| 185 _CreateSymlinkTree(entry_output_dir, symlink_dir, java_files, java_dirs) | |
| 186 java_dirs = [symlink_dir] | |
| 187 | |
| 188 return java_dirs | |
| 189 | |
| 190 | |
| 191 def _GenerateLocalProperties(sdk_dir): | |
| 192 """Returns the data for project.properties as a string.""" | |
| 193 return '\n'.join([ | |
| 194 '# Generated by //build/android/generate_gradle.py', | |
|
jbudorick
2016/07/12 19:20:17
nit: update this path
agrieve
2016/07/13 14:09:08
Done.
| |
| 195 'sdk.dir=%s' % sdk_dir, | |
| 196 '']) | |
| 197 | |
| 198 | |
| 199 def _GenerateGradleFile(build_config, config_json, java_dirs, relativize, | |
| 200 use_gradle_process_resources): | |
| 201 """Returns the data for a project's build.gradle.""" | |
| 202 deps_info = build_config['deps_info'] | |
| 203 gradle = build_config['gradle'] | |
| 204 | |
| 205 if deps_info['type'] == 'android_apk': | |
| 206 target_type = 'android_apk' | |
| 207 elif deps_info['type'] == 'java_library' and not deps_info['is_prebuilt']: | |
| 208 if deps_info['requires_android']: | |
| 209 target_type = 'android_library' | |
| 210 else: | |
| 211 target_type = 'java_library' | |
| 212 else: | |
| 213 return None | |
| 214 | |
| 215 variables = {} | |
| 216 variables['template_type'] = target_type | |
| 217 variables['use_gradle_process_resources'] = use_gradle_process_resources | |
| 218 variables['build_tools_version'] = config_json['build_tools_version'] | |
| 219 variables['compile_sdk_version'] = config_json['compile_sdk_version'] | |
| 220 android_manifest = gradle.get('android_manifest', | |
| 221 _DEFAULT_ANDROID_MANIFEST_PATH) | |
| 222 variables['android_manifest'] = relativize(android_manifest) | |
| 223 | |
| 224 variables['java_dirs'] = relativize(java_dirs) | |
| 225 variables['prebuilts'] = relativize(gradle['dependent_prebuilt_jars']) | |
| 226 deps = [_ProjectEntry.FromBuildConfigPath(p) | |
| 227 for p in gradle['dependent_android_projects']] | |
| 228 | |
|
jbudorick
2016/07/12 19:20:17
nit: odd to have a line break here rather than one
agrieve
2016/07/13 14:09:08
Done.
| |
| 229 variables['android_project_deps'] = [d.ProjectName() for d in deps] | |
| 230 deps = [_ProjectEntry.FromBuildConfigPath(p) | |
| 231 for p in gradle['dependent_java_projects']] | |
| 232 variables['java_project_deps'] = [d.ProjectName() for d in deps] | |
| 233 | |
| 234 processor = jinja_template.JinjaProcessor(host_paths.DIR_SOURCE_ROOT) | |
| 235 return processor.Render(_JINJA_TEMPLATE_PATH, variables) | |
| 236 | |
| 237 | |
| 238 def _GenerateRootGradle(): | |
| 239 """Returns the data for the root project's build.gradle.""" | |
| 240 variables = {'template_type': 'root'} | |
| 241 processor = jinja_template.JinjaProcessor(host_paths.DIR_SOURCE_ROOT) | |
| 242 return processor.Render(_JINJA_TEMPLATE_PATH, variables) | |
| 243 | |
| 244 | |
| 245 def _GenerateSettingsGradle(project_entries): | |
| 246 """Returns the data for settings.gradle.""" | |
| 247 project_name = os.path.basename(os.path.dirname(host_paths.DIR_SOURCE_ROOT)) | |
| 248 lines = [] | |
| 249 lines.append('// Generated by //build/android/generate_gradle.py') | |
|
jbudorick
2016/07/12 19:20:17
nit: update this path
agrieve
2016/07/13 14:09:08
Done.
| |
| 250 lines.append('rootProject.name = "%s"' % project_name) | |
| 251 lines.append('rootProject.projectDir = settingsDir') | |
| 252 lines.append('') | |
| 253 | |
| 254 for entry in project_entries: | |
| 255 # Example target: android_webview:android_webview_java__build_config | |
| 256 lines.append('include ":%s"' % entry.ProjectName()) | |
| 257 lines.append('project(":%s").projectDir = new File(settingsDir, "%s")' % | |
| 258 (entry.ProjectName(), entry.GradleSubdir())) | |
| 259 return '\n'.join(lines) | |
| 260 | |
| 261 | |
| 262 def _ExtractSrcjars(entry_output_dir, srcjar_tuples): | |
| 263 """Extracts all srcjars to the directory given by the tuples.""" | |
| 264 extracted_paths = set(s[1] for s in srcjar_tuples) | |
| 265 for extracted_path in extracted_paths: | |
| 266 assert _IsSubpathOf(extracted_path, entry_output_dir) | |
| 267 if os.path.exists(extracted_path): | |
| 268 shutil.rmtree(extracted_path) | |
| 269 | |
| 270 for srcjar_path, extracted_path in srcjar_tuples: | |
| 271 logging.info('Extracting %s to %s', srcjar_path, extracted_path) | |
| 272 with zipfile.ZipFile(srcjar_path) as z: | |
| 273 z.extractall(extracted_path) | |
| 274 | |
| 275 | |
| 276 def _FindAllProjectEntries(main_entry): | |
| 277 """Returns the list of all _ProjectEntry instances given the root project.""" | |
| 278 found = set() | |
| 279 to_scan = [main_entry] | |
| 280 while to_scan: | |
| 281 cur_entry = to_scan.pop() | |
| 282 if cur_entry in found: | |
| 283 continue | |
| 284 found.add(cur_entry) | |
| 285 build_config = cur_entry.BuildConfig() | |
| 286 sub_config_paths = build_config['deps_info']['deps_configs'] | |
| 287 to_scan.extend( | |
| 288 _ProjectEntry.FromBuildConfigPath(p) for p in sub_config_paths) | |
| 289 return list(found) | |
| 290 | |
| 291 | |
| 292 def main(): | |
| 293 parser = argparse.ArgumentParser() | |
| 294 parser.add_argument('--output-directory', | |
| 295 help='Path to the root build directory.') | |
| 296 parser.add_argument('-v', | |
| 297 '--verbose', | |
| 298 dest='verbose_count', | |
| 299 default=0, | |
| 300 action='count', | |
| 301 help='Verbose level') | |
| 302 parser.add_argument('--target', | |
| 303 help='GN target to generate project for.', | |
| 304 default='//chrome/android:chrome_public_apk') | |
| 305 parser.add_argument('--project-dir', | |
| 306 help='Root of the output project.', | |
| 307 default=os.path.join('$CHROMIUM_OUTPUT_DIR', 'gradle')) | |
| 308 parser.add_argument('--use-gradle-process-resources', | |
| 309 action='store_true', | |
| 310 help='Have gradle generate R.java rather than ninja') | |
| 311 args = parser.parse_args() | |
| 312 run_tests_helper.SetLogLevel(args.verbose_count) | |
| 313 if args.output_directory: | |
| 314 constants.SetOutputDirectory(args.output_directory) | |
| 315 constants.CheckOutputDirectory() | |
| 316 output_dir = constants.GetOutDirectory() | |
| 317 | |
| 318 gradle_output_dir = os.path.abspath( | |
| 319 args.project_dir.replace('$CHROMIUM_OUTPUT_DIR', output_dir)) | |
| 320 logging.warning('Creating project at: %s', gradle_output_dir) | |
| 321 | |
| 322 main_entry = _ProjectEntry(args.target) | |
| 323 logging.warning('Building .build_config files...') | |
| 324 _RunNinja(output_dir, [main_entry.NinjaBuildConfigTarget()]) | |
| 325 | |
| 326 all_entries = _FindAllProjectEntries(main_entry) | |
| 327 logging.info('Found %d dependent build_config targets.', len(all_entries)) | |
| 328 | |
| 329 config_json = build_utils.ReadJson( | |
| 330 os.path.join(output_dir, 'gradle', 'config.json')) | |
| 331 project_entries = [] | |
| 332 srcjar_tuples = [] | |
| 333 for entry in all_entries: | |
| 334 build_config = entry.BuildConfig() | |
| 335 if build_config['deps_info']['type'] not in ('android_apk', 'java_library'): | |
| 336 continue | |
| 337 | |
| 338 entry_output_dir = os.path.join(gradle_output_dir, entry.GradleSubdir()) | |
| 339 relativize = lambda x, d=entry_output_dir: _RebasePath(x, d) | |
| 340 | |
| 341 srcjars = _RebasePath(build_config['gradle'].get('bundled_srcjars', [])) | |
| 342 if not args.use_gradle_process_resources: | |
| 343 srcjars += _RebasePath(build_config['javac']['srcjars']) | |
| 344 | |
| 345 java_sources_file = build_config['gradle'].get('java_sources_file') | |
| 346 if java_sources_file: | |
| 347 java_sources_file = _RebasePath(java_sources_file) | |
| 348 | |
| 349 java_dirs = _CreateJavaSourceDir(entry_output_dir, java_sources_file) | |
| 350 if srcjars: | |
| 351 java_dirs.append(os.path.join(entry_output_dir, _SRCJARS_SUBDIR)) | |
| 352 | |
| 353 data = _GenerateGradleFile(build_config, config_json, java_dirs, relativize, | |
| 354 args.use_gradle_process_resources) | |
| 355 if data: | |
| 356 project_entries.append(entry) | |
| 357 srcjar_tuples.extend( | |
| 358 (s, os.path.join(entry_output_dir, _SRCJARS_SUBDIR)) for s in srcjars) | |
| 359 _WriteFile(os.path.join(entry_output_dir, 'build.gradle'), data) | |
| 360 | |
| 361 _WriteFile(os.path.join(gradle_output_dir, 'build.gradle'), | |
| 362 _GenerateRootGradle()) | |
| 363 | |
| 364 _WriteFile(os.path.join(gradle_output_dir, 'settings.gradle'), | |
| 365 _GenerateSettingsGradle(project_entries)) | |
| 366 | |
| 367 sdk_path = _RebasePath(config_json['android_sdk_root']) | |
| 368 _WriteFile(os.path.join(gradle_output_dir, 'local.properties'), | |
| 369 _GenerateLocalProperties(sdk_path)) | |
| 370 | |
| 371 if srcjar_tuples: | |
| 372 logging.warning('Building all .srcjar files...') | |
| 373 targets = _RebasePath([s[0] for s in srcjar_tuples], output_dir) | |
| 374 _RunNinja(output_dir, targets) | |
| 375 _ExtractSrcjars(gradle_output_dir, srcjar_tuples) | |
| 376 logging.warning('Project created successfully!') | |
| 377 | |
| 378 | |
| 379 if __name__ == '__main__': | |
| 380 main() | |
| OLD | NEW |