1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this,
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5# If we add unicode_literals, Python 2.6.1 (required for OS X 10.6) breaks.
6from __future__ import absolute_import, print_function
7
8import errno
9import os
10import stat
11import subprocess
12import sys
13
14# We need the NDK version in multiple different places, and it's inconvenient
15# to pass down the NDK version to all relevant places, so we have this global
16# variable.
17NDK_VERSION = 'r15c'
18
19ANDROID_NDK_EXISTS = '''
20Looks like you have the Android NDK installed at:
21%s
22'''
23
24ANDROID_SDK_EXISTS = '''
25Looks like you have the Android SDK installed at:
26%s
27We will install all required Android packages.
28'''
29
30ANDROID_SDK_TOO_OLD = '''
31Looks like you have an outdated Android SDK installed at:
32%s
33I can't update outdated Android SDKs to have the required 'sdkmanager'
34tool.  Move it out of the way (or remove it entirely) and then run
35bootstrap again.
36'''
37
38INSTALLING_ANDROID_PACKAGES = '''
39We are now installing the following Android packages:
40%s
41You may be prompted to agree to the Android license. You may see some of
42output as packages are downloaded and installed.
43'''
44
45MOBILE_ANDROID_MOZCONFIG_TEMPLATE = '''
46Paste the lines between the chevrons (>>> and <<<) into your mozconfig file:
47
48<<<
49# Build Firefox for Android:
50ac_add_options --enable-application=mobile/android
51ac_add_options --target=arm-linux-androideabi
52
53{extra_lines}
54# With the following Android SDK and NDK:
55ac_add_options --with-android-sdk="{sdk_path}"
56ac_add_options --with-android-ndk="{ndk_path}"
57>>>
58'''
59
60MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE = '''
61Paste the lines between the chevrons (>>> and <<<) into your mozconfig file:
62
63<<<
64# Build Firefox for Android Artifact Mode:
65ac_add_options --enable-application=mobile/android
66ac_add_options --target=arm-linux-androideabi
67ac_add_options --enable-artifact-builds
68
69{extra_lines}
70# With the following Android SDK:
71ac_add_options --with-android-sdk="{sdk_path}"
72
73# Write build artifacts to:
74mk_add_options MOZ_OBJDIR=./objdir-frontend
75>>>
76'''
77
78
79def install_mobile_android_sdk_or_ndk(url, path):
80    '''
81    Fetch an Android SDK or NDK from |url| and unpack it into
82    the given |path|.
83
84    We expect wget to be installed and found on the system path.
85
86    We use, and wget respects, https.  We could also include SHAs for a
87    small improvement in the integrity guarantee we give. But this script is
88    bootstrapped over https anyway, so it's a really minor improvement.
89
90    We use |wget --continue| as a cheap cache of the downloaded artifacts,
91    writing into |path|/mozboot.  We don't yet clean the cache; it's better
92    to waste disk and not require a long re-download than to wipe the cache
93    prematurely.
94    '''
95
96    old_path = os.getcwd()
97    try:
98        download_path = os.path.join(path, 'mozboot')
99        try:
100            os.makedirs(download_path)
101        except OSError as e:
102            if e.errno == errno.EEXIST and os.path.isdir(download_path):
103                pass
104            else:
105                raise
106
107        os.chdir(download_path)
108        subprocess.check_call(['wget', '--continue', url])
109        file = url.split('/')[-1]
110
111        os.chdir(path)
112        abspath = os.path.join(download_path, file)
113        if file.endswith('.tar.gz') or file.endswith('.tgz'):
114            cmd = ['tar', 'zxf', abspath]
115        elif file.endswith('.tar.bz2'):
116            cmd = ['tar', 'jxf', abspath]
117        elif file.endswith('.zip'):
118            cmd = ['unzip', '-q', abspath]
119        elif file.endswith('.bin'):
120            # Execute the .bin file, which unpacks the content.
121            mode = os.stat(path).st_mode
122            os.chmod(abspath, mode | stat.S_IXUSR)
123            cmd = [abspath]
124        else:
125            raise NotImplementedError("Don't know how to unpack file: %s" % file)
126
127        print('Unpacking %s...' % abspath)
128
129        with open(os.devnull, "w") as stdout:
130            # These unpack commands produce a ton of output; ignore it.  The
131            # .bin files are 7z archives; there's no command line flag to quiet
132            # output, so we use this hammer.
133            subprocess.check_call(cmd, stdout=stdout)
134
135        print('Unpacking %s... DONE' % abspath)
136
137    finally:
138        os.chdir(old_path)
139
140
141def get_paths(os_name):
142    mozbuild_path = os.environ.get('MOZBUILD_STATE_PATH',
143                                   os.path.expanduser(os.path.join('~', '.mozbuild')))
144    sdk_path = os.environ.get('ANDROID_SDK_HOME',
145                              os.path.join(mozbuild_path, 'android-sdk-{0}'.format(os_name)))
146    ndk_path = os.environ.get('ANDROID_NDK_HOME',
147                              os.path.join(mozbuild_path, 'android-ndk-{0}'.format(NDK_VERSION)))
148    return (mozbuild_path, sdk_path, ndk_path)
149
150
151def ensure_dir(dir):
152    '''Ensures the given directory exists'''
153    if dir and not os.path.exists(dir):
154        try:
155            os.makedirs(dir)
156        except OSError as error:
157            if error.errno != errno.EEXIST:
158                raise
159
160
161def ensure_android(os_name, artifact_mode=False, ndk_only=False, no_interactive=False):
162    '''
163    Ensure the Android SDK (and NDK, if `artifact_mode` is falsy) are
164    installed.  If not, fetch and unpack the SDK and/or NDK from the
165    given URLs.  Ensure the required Android SDK packages are
166    installed.
167
168    `os_name` can be 'linux' or 'macosx'.
169    '''
170    # The user may have an external Android SDK (in which case we
171    # save them a lengthy download), or they may have already
172    # completed the download. We unpack to
173    # ~/.mozbuild/{android-sdk-$OS_NAME, android-ndk-$VER}.
174    mozbuild_path, sdk_path, ndk_path = get_paths(os_name)
175    os_tag = 'darwin' if os_name == 'macosx' else os_name
176    sdk_url = 'https://dl.google.com/android/repository/sdk-tools-{0}-3859397.zip'.format(os_tag)
177    ndk_url = android_ndk_url(os_name)
178
179    ensure_android_sdk_and_ndk(mozbuild_path, os_name,
180                               sdk_path=sdk_path, sdk_url=sdk_url,
181                               ndk_path=ndk_path, ndk_url=ndk_url,
182                               artifact_mode=artifact_mode,
183                               ndk_only=ndk_only)
184
185    if ndk_only:
186        return
187
188    # We expect the |sdkmanager| tool to be at
189    # ~/.mozbuild/android-sdk-$OS_NAME/tools/bin/sdkmanager.
190    sdkmanager_tool = os.path.join(sdk_path, 'tools', 'bin', 'sdkmanager')
191    ensure_android_packages(sdkmanager_tool=sdkmanager_tool, no_interactive=no_interactive)
192
193
194def ensure_android_sdk_and_ndk(mozbuild_path, os_name, sdk_path, sdk_url, ndk_path, ndk_url,
195                               artifact_mode, ndk_only):
196    '''
197    Ensure the Android SDK and NDK are found at the given paths.  If not, fetch
198    and unpack the SDK and/or NDK from the given URLs into
199    |mozbuild_path/{android-sdk-$OS_NAME,android-ndk-$VER}|.
200    '''
201
202    # It's not particularly bad to overwrite the NDK toolchain, but it does take
203    # a while to unpack, so let's avoid the disk activity if possible.  The SDK
204    # may prompt about licensing, so we do this first.
205    # Check for Android NDK only if we are not in artifact mode.
206    if not artifact_mode:
207        if os.path.isdir(ndk_path):
208            print(ANDROID_NDK_EXISTS % ndk_path)
209        else:
210            # The NDK archive unpacks into a top-level android-ndk-$VER directory.
211            install_mobile_android_sdk_or_ndk(ndk_url, mozbuild_path)
212
213    if ndk_only:
214        return
215
216    # We don't want to blindly overwrite, since we use the
217    # |sdkmanager| tool to install additional parts of the Android
218    # toolchain.  If we overwrite, we lose whatever Android packages
219    # the user may have already installed.
220    if os.path.isfile(os.path.join(sdk_path, 'tools', 'bin', 'sdkmanager')):
221        print(ANDROID_SDK_EXISTS % sdk_path)
222    elif os.path.isdir(sdk_path):
223        raise NotImplementedError(ANDROID_SDK_TOO_OLD % sdk_path)
224    else:
225        # The SDK archive used to include a top-level
226        # android-sdk-$OS_NAME directory; it no longer does so.  We
227        # preserve the old convention to smooth detecting existing SDK
228        # installations.
229        install_mobile_android_sdk_or_ndk(sdk_url, os.path.join(mozbuild_path,
230                                          'android-sdk-{0}'.format(os_name)))
231
232
233def ensure_android_packages(sdkmanager_tool, packages=None, no_interactive=False):
234    '''
235    Use the given sdkmanager tool (like 'sdkmanager') to install required
236    Android packages.
237    '''
238
239    # This tries to install all the required Android packages.  The user
240    # may be prompted to agree to the Android license.
241    package_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__),
242                                        'android-packages.txt'))
243    print(INSTALLING_ANDROID_PACKAGES % open(package_file_name, 'rt').read())
244
245    args = [sdkmanager_tool, '--package_file={0}'.format(package_file_name)]
246    if not no_interactive:
247        subprocess.check_call(args)
248        return
249
250    # Emulate yes.  For a discussion of passing input to check_output,
251    # see https://stackoverflow.com/q/10103551.
252    yes = '\n'.join(['y']*100)
253    proc = subprocess.Popen(args,
254                            stdout=subprocess.PIPE,
255                            stderr=subprocess.STDOUT,
256                            stdin=subprocess.PIPE)
257    output, unused_err = proc.communicate(yes)
258
259    retcode = proc.poll()
260    if retcode:
261        cmd = args[0]
262        e = subprocess.CalledProcessError(retcode, cmd)
263        e.output = output
264        raise e
265
266    print(output)
267
268
269def suggest_mozconfig(os_name, artifact_mode=False, java_bin_path=None):
270    _mozbuild_path, sdk_path, ndk_path = get_paths(os_name)
271
272    extra_lines = []
273    if java_bin_path:
274        extra_lines += [
275            '# With the following java and javac:',
276            'ac_add_options --with-java-bin-path="{}"'.format(java_bin_path),
277        ]
278    if extra_lines:
279        extra_lines.append('')
280
281    if artifact_mode:
282        template = MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE
283    else:
284        template = MOBILE_ANDROID_MOZCONFIG_TEMPLATE
285
286    kwargs = dict(
287        sdk_path=sdk_path,
288        ndk_path=ndk_path,
289        extra_lines='\n'.join(extra_lines),
290    )
291    print(template.format(**kwargs))
292
293
294def android_ndk_url(os_name, ver=NDK_VERSION):
295    # Produce a URL like
296    # 'https://dl.google.com/android/repository/android-ndk-$VER-linux-x86_64.zip
297    base_url = 'https://dl.google.com/android/repository/android-ndk'
298
299    if os_name == 'macosx':
300        # |mach bootstrap| uses 'macosx', but Google uses 'darwin'.
301        os_name = 'darwin'
302
303    if sys.maxsize > 2**32:
304        arch = 'x86_64'
305    else:
306        arch = 'x86'
307
308    return '%s-%s-%s-%s.zip' % (base_url, ver, os_name, arch)
309
310
311def main(argv):
312    import optparse  # No argparse, which is new in Python 2.7.
313    import platform
314
315    parser = optparse.OptionParser()
316    parser.add_option('-a', '--artifact-mode', dest='artifact_mode', action='store_true',
317                      help='If true, install only the Android SDK (and not the Android NDK).')
318    parser.add_option('--ndk-only', dest='ndk_only', action='store_true',
319                      help='If true, install only the Android NDK (and not the Android SDK).')
320    parser.add_option('--no-interactive', dest='no_interactive', action='store_true',
321                      help='Accept the Android SDK licenses without user interaction.')
322
323    options, _ = parser.parse_args(argv)
324
325    if options.artifact_mode and options.ndk_only:
326        raise NotImplementedError('Use no options to install the NDK and the SDK.')
327
328    os_name = None
329    if platform.system() == 'Darwin':
330        os_name = 'macosx'
331    elif platform.system() == 'Linux':
332        os_name = 'linux'
333    elif platform.system() == 'Windows':
334        os_name = 'windows'
335    else:
336        raise NotImplementedError("We don't support bootstrapping the Android SDK (or Android "
337                                  "NDK) on {0} yet!".format(platform.system()))
338
339    ensure_android(os_name, artifact_mode=options.artifact_mode,
340                   ndk_only=options.ndk_only,
341                   no_interactive=options.no_interactive)
342    suggest_mozconfig(os_name, options.artifact_mode)
343
344    return 0
345
346
347if __name__ == '__main__':
348    sys.exit(main(sys.argv))
349