Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(23)

Side by Side Diff: build/android/gradle/generate_gradle.py

Issue 2130933002: 🎧 Initial version of Android Studio project generation (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: comments 1 Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698