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