1#!/usr/bin/env python
2#
3# Copyright 2015 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7'''Prepares the Google Play services split client libraries before usage by
8Chrome's build system.
9
10We need to preprocess Google Play services before using it in Chrome
11builds for 2 main reasons:
12
13- Getting rid of unused resources: unsupported languages, unused
14drawables, etc.
15
16- Merging the differents jars so that it can be proguarded more
17easily. This is necessary since debug and test apks get very close
18to the dex limit.
19
20The script is supposed to be used with the maven repository that can be
21obtained by downloading the "extra-google-m2repository" from the Android SDK
22Manager. It also supports importing from already extracted AAR files using the
23--is-extracted-repo flag. The expected directory structure in that case would
24look like:
25
26    REPOSITORY_DIR
27    +-- CLIENT_1
28    |   +-- <content of the first AAR file>
29    +-- CLIENT_2
30    +-- etc.
31
32The output is a directory with the following structure:
33
34    OUT_DIR
35    +-- google-play-services.jar
36    +-- res
37    |   +-- CLIENT_1
38    |   |   +-- color
39    |   |   +-- values
40    |   |   +-- etc.
41    |   +-- CLIENT_2
42    |       +-- ...
43    +-- stub
44        +-- res/[.git-keep-directory]
45        +-- src/android/UnusedStub.java
46
47Requires the `jar` utility in the path.
48
49'''
50
51import argparse
52import glob
53import itertools
54import os
55import shutil
56import stat
57import sys
58import tempfile
59import zipfile
60
61from datetime import datetime
62
63sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
64import devil_chromium
65from devil.utils import cmd_helper
66from play_services import utils
67from pylib.utils import argparse_utils
68
69
70M2_PKG_PATH = os.path.join('com', 'google', 'android', 'gms')
71
72
73def main():
74  parser = argparse.ArgumentParser(description=(
75      "Prepares the Google Play services split client libraries before usage "
76      "by Chrome's build system. See the script's documentation for more a "
77      "detailed help."))
78  argparse_utils.CustomHelpAction.EnableFor(parser)
79  required_args = parser.add_argument_group('required named arguments')
80  required_args.add_argument('-r',
81                             '--repository',
82                             help=('the Google Play services repository '
83                                   'location'),
84                             required=True,
85                             metavar='FILE')
86  required_args.add_argument('-o',
87                             '--out-dir',
88                             help='the output directory',
89                             required=True,
90                             metavar='FILE')
91  required_args.add_argument('-c',
92                             '--config-file',
93                             help='the config file path',
94                             required=True,
95                             metavar='FILE')
96  parser.add_argument('-x',
97                      '--is-extracted-repo',
98                      action='store_true',
99                      help='the provided repository is not made of AAR files')
100  parser.add_argument('--config-help',
101                      action='custom_help',
102                      custom_help_text=utils.ConfigParser.__doc__,
103                      help='show the configuration file format help')
104
105  args = parser.parse_args()
106
107  devil_chromium.Initialize()
108
109  return ProcessGooglePlayServices(args.repository,
110                                   args.out_dir,
111                                   args.config_file,
112                                   args.is_extracted_repo)
113
114
115def ProcessGooglePlayServices(repo, out_dir, config_path, is_extracted_repo):
116  config = utils.ConfigParser(config_path)
117
118  tmp_root = tempfile.mkdtemp()
119  try:
120    tmp_paths = _SetupTempDir(tmp_root)
121
122    if is_extracted_repo:
123      _ImportFromExtractedRepo(config, tmp_paths, repo)
124    else:
125      _ImportFromAars(config, tmp_paths, repo)
126
127    _GenerateCombinedJar(tmp_paths)
128    _ProcessResources(config, tmp_paths, repo)
129    _BuildOutput(config, tmp_paths, out_dir)
130  finally:
131    shutil.rmtree(tmp_root)
132
133  return 0
134
135
136def _SetupTempDir(tmp_root):
137  tmp_paths = {
138      'root': tmp_root,
139      'imported_clients': os.path.join(tmp_root, 'imported_clients'),
140      'extracted_jars': os.path.join(tmp_root, 'jar'),
141      'combined_jar': os.path.join(tmp_root, 'google-play-services.jar'),
142  }
143  os.mkdir(tmp_paths['imported_clients'])
144  os.mkdir(tmp_paths['extracted_jars'])
145
146  return tmp_paths
147
148
149def _SetupOutputDir(out_dir):
150  out_paths = {
151      'root': out_dir,
152      'res': os.path.join(out_dir, 'res'),
153      'jar': os.path.join(out_dir, 'google-play-services.jar'),
154      'stub': os.path.join(out_dir, 'stub'),
155  }
156
157  shutil.rmtree(out_paths['jar'], ignore_errors=True)
158  shutil.rmtree(out_paths['res'], ignore_errors=True)
159  shutil.rmtree(out_paths['stub'], ignore_errors=True)
160
161  return out_paths
162
163
164def _MakeWritable(dir_path):
165  for root, dirs, files in os.walk(dir_path):
166    for path in itertools.chain(dirs, files):
167      st = os.stat(os.path.join(root, path))
168      os.chmod(os.path.join(root, path), st.st_mode | stat.S_IWUSR)
169
170
171def _ImportFromAars(config, tmp_paths, repo):
172  for client in config.clients:
173    aar_name = '%s-%s.aar' % (client, config.sdk_version)
174    aar_path = os.path.join(repo, M2_PKG_PATH, client,
175                            config.sdk_version, aar_name)
176    aar_out_path = os.path.join(tmp_paths['imported_clients'], client)
177    _ExtractAll(aar_path, aar_out_path)
178
179    client_jar_path = os.path.join(aar_out_path, 'classes.jar')
180    _ExtractAll(client_jar_path, tmp_paths['extracted_jars'])
181
182
183def _ImportFromExtractedRepo(config, tmp_paths, repo):
184  # Import the clients
185  try:
186    for client in config.clients:
187      client_out_dir = os.path.join(tmp_paths['imported_clients'], client)
188      shutil.copytree(os.path.join(repo, client), client_out_dir)
189
190      client_jar_path = os.path.join(client_out_dir, 'classes.jar')
191      _ExtractAll(client_jar_path, tmp_paths['extracted_jars'])
192  finally:
193    _MakeWritable(tmp_paths['imported_clients'])
194
195
196def _GenerateCombinedJar(tmp_paths):
197  out_file_name = tmp_paths['combined_jar']
198  working_dir = tmp_paths['extracted_jars']
199  cmd_helper.Call(['jar', '-cf', out_file_name, '-C', working_dir, '.'])
200
201
202def _ProcessResources(config, tmp_paths, repo):
203  LOCALIZED_VALUES_BASE_NAME = 'values-'
204  locale_whitelist = set(config.locale_whitelist)
205
206  glob_pattern = os.path.join(tmp_paths['imported_clients'], '*', 'res', '*')
207  for res_dir in glob.glob(glob_pattern):
208    dir_name = os.path.basename(res_dir)
209
210    if dir_name.startswith('drawable'):
211      shutil.rmtree(res_dir)
212      continue
213
214    if dir_name.startswith(LOCALIZED_VALUES_BASE_NAME):
215      dir_locale = dir_name[len(LOCALIZED_VALUES_BASE_NAME):]
216      if dir_locale not in locale_whitelist:
217        shutil.rmtree(res_dir)
218
219  # Reimport files from the whitelist.
220  for res_path in config.resource_whitelist:
221    for whitelisted_file in glob.glob(os.path.join(repo, res_path)):
222      resolved_file = os.path.relpath(whitelisted_file, repo)
223      rebased_res = os.path.join(tmp_paths['imported_clients'], resolved_file)
224
225      if not os.path.exists(os.path.dirname(rebased_res)):
226        os.makedirs(os.path.dirname(rebased_res))
227
228      shutil.copy(os.path.join(repo, whitelisted_file), rebased_res)
229
230
231def _BuildOutput(config, tmp_paths, out_dir):
232  generation_date = datetime.utcnow()
233  version_xml_path = os.path.join(tmp_paths['imported_clients'],
234                                  config.version_xml_path)
235  play_services_full_version = utils.GetVersionNumberFromLibraryResources(
236      version_xml_path)
237
238  out_paths = _SetupOutputDir(out_dir)
239
240  # Copy the resources to the output dir
241  for client in config.clients:
242    res_in_tmp_dir = os.path.join(tmp_paths['imported_clients'], client, 'res')
243    if os.path.isdir(res_in_tmp_dir) and os.listdir(res_in_tmp_dir):
244      res_in_final_dir = os.path.join(out_paths['res'], client)
245      shutil.copytree(res_in_tmp_dir, res_in_final_dir)
246
247  # Copy the jar
248  shutil.copyfile(tmp_paths['combined_jar'], out_paths['jar'])
249
250  # Write the java dummy stub. Needed for gyp to create the resource jar
251  stub_location = os.path.join(out_paths['stub'], 'src', 'android')
252  os.makedirs(stub_location)
253  with open(os.path.join(stub_location, 'UnusedStub.java'), 'w') as stub:
254    stub.write('package android;'
255               'public final class UnusedStub {'
256               '    private UnusedStub() {}'
257               '}')
258
259  # Create the main res directory. It is needed by gyp
260  stub_res_location = os.path.join(out_paths['stub'], 'res')
261  os.makedirs(stub_res_location)
262  with open(os.path.join(stub_res_location, '.res-stamp'), 'w') as stamp:
263    content_str = 'google_play_services_version: %s\nutc_date: %s\n'
264    stamp.write(content_str % (play_services_full_version, generation_date))
265
266  config.UpdateVersionNumber(play_services_full_version)
267
268
269def _ExtractAll(zip_path, out_path):
270  with zipfile.ZipFile(zip_path, 'r') as zip_file:
271    zip_file.extractall(out_path)
272
273if __name__ == '__main__':
274  sys.exit(main())
275