Source code

Revision control

Other Tools

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
from __future__ import absolute_import, print_function, unicode_literals
6
7
import argparse
8
import logging
9
import os
10
import json
11
12
from zipfile import ZipFile
13
14
import mozpack.path as mozpath
15
16
from mozbuild.base import (
17
MachCommandBase,
18
MachCommandConditions as conditions,
19
)
20
21
from mozbuild.shellutil import (
22
split as shell_split,
23
)
24
25
from mach.decorators import (
26
CommandArgument,
27
CommandProvider,
28
Command,
29
SubCommand,
30
)
31
32
33
# NOTE python/mach/mach/commands/commandinfo.py references this function
34
# by name. If this function is renamed or removed, that file should
35
# be updated accordingly as well.
36
def REMOVED(cls):
37
"""Command no longer exists! Use the Gradle configuration rooted in the top source directory
38
instead.
39
41
"""
42
return False
43
44
45
@CommandProvider
46
class MachCommands(MachCommandBase):
47
def _root_url(self, artifactdir=None, objdir=None):
48
"""Generate a publicly-accessible URL for the tasks's artifacts, or an objdir path"""
49
if 'TASK_ID' in os.environ and 'RUN_ID' in os.environ:
50
import taskcluster_urls
51
from taskgraph.util.taskcluster import get_root_url
52
return taskcluster_urls.api(
53
get_root_url(False), 'queue', 'v1', 'task/{}/runs/{}/artifacts/{}'.format(
54
os.environ['TASK_ID'], os.environ['RUN_ID'], artifactdir))
55
else:
56
return os.path.join(self.topobjdir, objdir)
57
58
@Command('android', category='devenv',
59
description='Run Android-specific commands.',
60
conditions=[conditions.is_android])
61
def android(self):
62
pass
63
64
@SubCommand('android', 'assemble-app',
65
"""Assemble Firefox for Android.
67
@CommandArgument('args', nargs=argparse.REMAINDER)
68
def android_assemble_app(self, args):
69
ret = self.gradle(self.substs['GRADLE_ANDROID_APP_TASKS'] +
70
['-x', 'lint'] + args, verbose=True)
71
72
return ret
73
74
@SubCommand('android', 'generate-sdk-bindings',
75
"""Generate SDK bindings used when building GeckoView.""")
76
@CommandArgument('inputs', nargs='+', help='config files, '
77
'like [/path/to/ClassName-classes.txt]+')
78
@CommandArgument('args', nargs=argparse.REMAINDER)
79
def android_generate_sdk_bindings(self, inputs, args):
80
import itertools
81
82
def stem(input):
83
# Turn "/path/to/ClassName-classes.txt" into "ClassName".
84
return os.path.basename(input).rsplit('-classes.txt', 1)[0]
85
86
bindings_inputs = list(itertools.chain(*((input, stem(input)) for input in inputs)))
87
bindings_args = '-Pgenerate_sdk_bindings_args={}'.format(':'.join(bindings_inputs))
88
89
ret = self.gradle(
90
self.substs['GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS'] + [bindings_args] + args,
91
verbose=True)
92
93
return ret
94
95
@SubCommand('android', 'generate-generated-jni-wrappers',
96
"""Generate GeckoView JNI wrappers used when building GeckoView.""")
97
@CommandArgument('args', nargs=argparse.REMAINDER)
98
def android_generate_generated_jni_wrappers(self, args):
99
ret = self.gradle(
100
self.substs['GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS'] + args,
101
verbose=True)
102
103
return ret
104
105
@SubCommand('android', 'generate-fennec-jni-wrappers',
106
"""Generate Fennec-specific JNI wrappers used when building
107
Firefox for Android.""")
108
@CommandArgument('args', nargs=argparse.REMAINDER)
109
def android_generate_fennec_jni_wrappers(self, args):
110
ret = self.gradle(
111
self.substs['GRADLE_ANDROID_GENERATE_FENNEC_JNI_WRAPPERS_TASKS'] + args, verbose=True)
112
113
return ret
114
115
@SubCommand('android', 'api-lint',
116
"""Runs apilint against GeckoView.""")
117
@CommandArgument('args', nargs=argparse.REMAINDER)
118
def android_api_lint(self, args):
119
ret = self.gradle(self.substs['GRADLE_ANDROID_API_LINT_TASKS'] + args, verbose=True)
120
folder = self.substs['GRADLE_ANDROID_GECKOVIEW_APILINT_FOLDER']
121
122
with open(os.path.join(
123
self.topobjdir,
124
'{}/apilint-result.json'.format(folder))) as f:
125
result = json.load(f)
126
127
print('SUITE-START | android-api-lint')
128
for r in result['compat_failures'] + result['failures']:
129
print ('TEST-UNEXPECTED-FAIL | {} | {}'.format(r['detail'], r['msg']))
130
for r in result['api_changes']:
131
print ('TEST-UNEXPECTED-FAIL | {} | Unexpected api change'.format(r))
132
print('SUITE-END | android-api-lint')
133
134
return ret
135
136
@SubCommand('android', 'test',
137
"""Run Android local unit tests.
139
@CommandArgument('args', nargs=argparse.REMAINDER)
140
def android_test(self, args):
141
ret = self.gradle(self.substs['GRADLE_ANDROID_TEST_TASKS'] +
142
args, verbose=True)
143
144
ret |= self._parse_android_test_results('public/app/unittest',
145
'gradle/build/mobile/android/app',
146
(self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],))
147
148
ret |= self._parse_android_test_results('public/geckoview/unittest',
149
'gradle/build/mobile/android/geckoview',
150
(self.substs['GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME'],)) # NOQA: E501
151
152
return ret
153
154
def _parse_android_test_results(self, artifactdir, gradledir, variants):
155
# Unit tests produce both HTML and XML reports. Visit the
156
# XML report(s) to report errors and link to the HTML
157
# report(s) for human consumption.
158
import itertools
159
import xml.etree.ElementTree as ET
160
161
from mozpack.files import (
162
FileFinder,
163
)
164
165
ret = 0
166
found_reports = False
167
168
root_url = self._root_url(
169
artifactdir=artifactdir,
170
objdir=gradledir + '/reports/tests')
171
172
def capitalize(s):
173
# Can't use str.capitalize because it lower cases trailing letters.
174
return (s[0].upper() + s[1:]) if s else ''
175
176
for variant in variants:
177
report = 'test{}UnitTest'.format(capitalize(variant))
178
finder = FileFinder(os.path.join(self.topobjdir, gradledir + '/test-results/', report))
179
for p, _ in finder.find('TEST-*.xml'):
180
found_reports = True
181
f = open(os.path.join(finder.base, p), 'rt')
182
tree = ET.parse(f)
183
root = tree.getroot()
184
185
# Log reports for Tree Herder "Job Details".
186
print('TinderboxPrint: report<br/><a href="{}/{}/index.html">HTML {} report</a>, visit "Inspect Task" link for details'.format(root_url, report, report)) # NOQA: E501
187
188
# And make the report display as soon as possible.
189
failed = root.findall('testcase/error') or root.findall('testcase/failure')
190
if failed:
191
print(
192
'TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the reports at: {}/{}/index.html'.format(root_url, report)) # NOQA: E501
193
194
print('SUITE-START | android-test | {} {}'.format(report, root.get('name')))
195
196
for testcase in root.findall('testcase'):
197
name = testcase.get('name')
198
print('TEST-START | {}'.format(name))
199
200
# Schema cribbed from
202
# particular advantage to formatting the error, so
203
# for now let's just output the unexpected XML
204
# tag.
205
error_count = 0
206
for unexpected in itertools.chain(testcase.findall('error'),
207
testcase.findall('failure')):
208
for line in ET.tostring(unexpected).strip().splitlines():
209
print('TEST-UNEXPECTED-FAIL | {} | {}'.format(name, line))
210
error_count += 1
211
ret |= 1
212
213
# Skipped tests aren't unexpected at this time; we
214
# disable some tests that require live remote
215
# endpoints.
216
for skipped in testcase.findall('skipped'):
217
for line in ET.tostring(skipped).strip().splitlines():
218
print('TEST-INFO | {} | {}'.format(name, line))
219
220
if not error_count:
221
print('TEST-PASS | {}'.format(name))
222
223
print('SUITE-END | android-test | {} {}'.format(report, root.get('name')))
224
225
if not found_reports:
226
print('TEST-UNEXPECTED-FAIL | android-test | No reports found under {}'.format(gradledir)) # NOQA: E501
227
return 1
228
229
return ret
230
231
@SubCommand('android', 'lint',
232
"""Run Android lint.
234
@CommandArgument('args', nargs=argparse.REMAINDER)
235
def android_lint(self, args):
236
ret = self.gradle(self.substs['GRADLE_ANDROID_LINT_TASKS'] +
237
args, verbose=True)
238
239
# Android Lint produces both HTML and XML reports. Visit the
240
# XML report(s) to report errors and link to the HTML
241
# report(s) for human consumption.
242
import xml.etree.ElementTree as ET
243
244
root_url = self._root_url(
245
artifactdir='public/android/lint',
246
objdir='gradle/build/mobile/android/app/reports')
247
248
reports = (self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],)
249
for report in reports:
250
f = open(os.path.join(
251
self.topobjdir,
252
'gradle/build/mobile/android/app/reports/lint-results-{}.xml'.format(report)),
253
'rt')
254
tree = ET.parse(f)
255
root = tree.getroot()
256
257
# Log reports for Tree Herder "Job Details".
258
html_report_url = '{}/lint-results-{}.html'.format(root_url, report)
259
xml_report_url = '{}/lint-results-{}.xml'.format(root_url, report)
260
print('TinderboxPrint: report<br/><a href="{}">HTML {} report</a>, visit "Inspect Task" link for details'.format(html_report_url, report)) # NOQA: E501
261
print('TinderboxPrint: report<br/><a href="{}">XML {} report</a>, visit "Inspect Task" link for details'.format(xml_report_url, report)) # NOQA: E501
262
263
# And make the report display as soon as possible.
264
if root.findall("issue[@severity='Error']"):
265
print('TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: {}'.format(html_report_url)) # NOQA: E501
266
267
print('SUITE-START | android-lint | {}'.format(report))
268
for issue in root.findall("issue[@severity='Error']"):
269
# There's no particular advantage to formatting the
270
# error, so for now let's just output the <issue> XML
271
# tag.
272
for line in ET.tostring(issue).strip().splitlines():
273
print('TEST-UNEXPECTED-FAIL | {}'.format(line))
274
ret |= 1
275
print('SUITE-END | android-lint | {}'.format(report))
276
277
return ret
278
279
def _parse_checkstyle_output(self, output_path):
280
ret = 0
281
# Checkstyle produces both HTML and XML reports. Visit the
282
# XML report(s) to report errors and link to the HTML
283
# report(s) for human consumption.
284
import xml.etree.ElementTree as ET
285
286
output_absolute_path = os.path.join(self.topobjdir, output_path)
287
f = open(output_absolute_path, 'rt')
288
tree = ET.parse(f)
289
root = tree.getroot()
290
291
# Now the reports, linkified.
292
report_xml = self._root_url(
293
artifactdir='public/android/checkstyle',
294
objdir=output_absolute_path)
295
report_html = self._root_url(
296
artifactdir='public/android/checkstyle',
297
objdir=os.path.splitext(output_absolute_path)[0] + '.html')
298
299
# And make the report display as soon as possible.
300
if root.findall('file/error'):
301
ret |= 1
302
303
if ret:
304
# Log reports for Tree Herder "Job Details".
305
print('TinderboxPrint: report<br/><a href="{}">HTML checkstyle report</a>, visit "Inspect Task" link for details'.format(report_xml)) # NOQA: E501
306
print('TinderboxPrint: report<br/><a href="{}">XML checkstyle report</a>, visit "Inspect Task" link for details'.format(report_html)) # NOQA: E501
307
308
print('TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: {}'.format(report_html)) # NOQA: E501
309
310
for file in root.findall('file'):
311
name = file.get('name')
312
313
error_count = 0
314
for error in file.findall('error'):
315
# There's no particular advantage to formatting the
316
# error, so for now let's just output the <error> XML
317
# tag.
318
print('TEST-UNEXPECTED-FAIL | {}'.format(name))
319
for line in ET.tostring(error).strip().splitlines():
320
print('TEST-UNEXPECTED-FAIL | {}'.format(line))
321
error_count += 1
322
323
return ret
324
325
@SubCommand('android', 'checkstyle',
326
"""Run Android checkstyle.
328
@CommandArgument('args', nargs=argparse.REMAINDER)
329
def android_checkstyle(self, args):
330
ret = self.gradle(self.substs['GRADLE_ANDROID_CHECKSTYLE_TASKS'] +
331
args, verbose=True)
332
print('SUITE-START | android-checkstyle')
333
for filePath in self.substs['GRADLE_ANDROID_CHECKSTYLE_OUTPUT_FILES']:
334
ret |= self._parse_checkstyle_output(filePath)
335
print('SUITE-END | android-checkstyle')
336
337
return ret
338
339
@SubCommand('android', 'findbugs',
340
"""Run Android findbugs.
342
@CommandArgument('args', nargs=argparse.REMAINDER)
343
def android_findbugs(self, dryrun=False, args=[]):
344
ret = self.gradle(self.substs['GRADLE_ANDROID_FINDBUGS_TASKS'] +
345
args, verbose=True)
346
347
# Findbug produces both HTML and XML reports. Visit the
348
# XML report(s) to report errors and link to the HTML
349
# report(s) for human consumption.
350
import xml.etree.ElementTree as ET
351
352
root_url = self._root_url(
353
artifactdir='public/android/findbugs',
354
objdir='gradle/build/mobile/android/app/reports/findbugs')
355
356
reports = (self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],)
357
for report in reports:
358
try:
359
f = open(os.path.join(
360
self.topobjdir, 'gradle/build/mobile/android/app/reports/findbugs',
361
'findbugs-{}-output.xml'.format(report)),
362
'rt')
363
except IOError:
364
continue
365
366
tree = ET.parse(f)
367
root = tree.getroot()
368
369
# Log reports for Tree Herder "Job Details".
370
html_report_url = '{}/findbugs-{}-output.html'.format(root_url, report)
371
xml_report_url = '{}/findbugs-{}-output.xml'.format(root_url, report)
372
print('TinderboxPrint: report<br/><a href="{}">HTML {} report</a>, visit "Inspect Task" link for details'.format(html_report_url, report)) # NOQA: E501
373
print('TinderboxPrint: report<br/><a href="{}">XML {} report</a>, visit "Inspect Task" link for details'.format(xml_report_url, report)) # NOQA: E501
374
375
# And make the report display as soon as possible.
376
if root.findall("./BugInstance"):
377
print('TEST-UNEXPECTED-FAIL | android-findbugs | Findbugs found issues in the project. See the report at: {}'.format(html_report_url)) # NOQA: E501
378
379
print('SUITE-START | android-findbugs | {}'.format(report))
380
for error in root.findall('./BugInstance'):
381
# There's no particular advantage to formatting the
382
# error, so for now let's just output the <error> XML
383
# tag.
384
print('TEST-UNEXPECTED-FAIL | {}:{} | {}'.format(report,
385
error.get('type'),
386
error.find('Class')
387
.get('classname')))
388
for line in ET.tostring(error).strip().splitlines():
389
print('TEST-UNEXPECTED-FAIL | {}:{} | {}'.format(report,
390
error.get('type'),
391
line))
392
ret |= 1
393
print('SUITE-END | android-findbugs | {}'.format(report))
394
395
return ret
396
397
@SubCommand('android', 'gradle-dependencies',
398
"""Collect Android Gradle dependencies.
400
@CommandArgument('args', nargs=argparse.REMAINDER)
401
def android_gradle_dependencies(self, args):
402
# We don't want to gate producing dependency archives on clean
403
# lint or checkstyle, particularly because toolchain versions
404
# can change the outputs for those processes.
405
self.gradle(self.substs['GRADLE_ANDROID_DEPENDENCIES_TASKS'] +
406
["--continue"] + args, verbose=True)
407
408
return 0
409
410
@SubCommand('android', 'archive-geckoview',
411
"""Create GeckoView archives.
413
@CommandArgument('args', nargs=argparse.REMAINDER)
414
def android_archive_geckoview(self, args):
415
ret = self.gradle(
416
self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + args,
417
verbose=True)
418
419
if ret != 0:
420
return ret
421
422
# TODO Bug 1563711 - Remove target.maven.zip
423
# The zip archive is passed along in CI to ship geckoview onto a maven repo
424
_craft_maven_zip_archive(self.topobjdir)
425
426
return 0
427
428
@SubCommand('android', 'build-geckoview_example',
429
"""Build geckoview_example """)
430
@CommandArgument('args', nargs=argparse.REMAINDER)
431
def android_build_geckoview_example(self, args):
432
self.gradle(self.substs['GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS'] + args,
433
verbose=True)
434
435
print('Execute `mach android install-geckoview_example` '
436
'to push the geckoview_example and test APKs to a device.')
437
438
return 0
439
440
@SubCommand('android', 'install-geckoview_example',
441
"""Install geckoview_example """)
442
@CommandArgument('args', nargs=argparse.REMAINDER)
443
def android_install_geckoview_example(self, args):
444
self.gradle(self.substs['GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS'] + args,
445
verbose=True)
446
447
print('Execute `mach android build-geckoview_example` '
448
'to just build the geckoview_example and test APKs.')
449
450
return 0
451
452
@SubCommand('android', 'geckoview-docs',
453
"""Create GeckoView javadoc and optionally upload to Github""")
454
@CommandArgument('--archive', action='store_true',
455
help='Generate a javadoc archive.')
456
@CommandArgument('--upload', metavar='USER/REPO',
457
help='Upload generated javadoc to Github, '
458
'using the specified USER/REPO.')
459
@CommandArgument('--upload-branch', metavar='BRANCH[/PATH]',
460
default='gh-pages/javadoc',
461
help='Use the specified branch/path for commits.')
462
@CommandArgument('--upload-message', metavar='MSG',
463
default='GeckoView docs upload',
464
help='Use the specified message for commits.')
465
def android_geckoview_docs(self, archive, upload, upload_branch,
466
upload_message):
467
468
tasks = (self.substs['GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS'] if archive or upload
469
else self.substs['GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS'])
470
471
ret = self.gradle(tasks, verbose=True)
472
if ret or not upload:
473
return ret
474
475
# Upload to Github.
476
fmt = {
477
'level': os.environ.get('MOZ_SCM_LEVEL', '0'),
478
'project': os.environ.get('MH_BRANCH', 'unknown'),
479
'revision': os.environ.get('GECKO_HEAD_REV', 'tip'),
480
}
481
env = {}
482
483
# In order to push to GitHub from TaskCluster, we store a private key
484
# in the TaskCluster secrets store in the format {"content": "<KEY>"},
485
# and the corresponding public key as a writable deploy key for the
486
# destination repo on GitHub.
487
secret = os.environ.get('GECKOVIEW_DOCS_UPLOAD_SECRET', '').format(**fmt)
488
if secret:
489
# Set up a private key from the secrets store if applicable.
490
import requests
491
req = requests.get('http://taskcluster/secrets/v1/secret/' + secret)
492
req.raise_for_status()
493
494
keyfile = mozpath.abspath('gv-docs-upload-key')
495
with open(keyfile, 'w') as f:
496
os.chmod(keyfile, 0o600)
497
f.write(req.json()['secret']['content'])
498
499
# Turn off strict host key checking so ssh does not complain about
500
# unknown github.com host. We're not pushing anything sensitive, so
501
# it's okay to not check GitHub's host keys.
502
env['GIT_SSH_COMMAND'] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
503
504
# Clone remote repo.
505
branch, _, branch_path = upload_branch.partition('/')
506
repo_url = 'git@github.com:%s.git' % upload
507
repo_path = mozpath.abspath('gv-docs-repo')
508
self.run_process(['git', 'clone', '--branch', branch, '--depth', '1',
509
repo_url, repo_path], append_env=env, pass_thru=True)
510
env['GIT_DIR'] = mozpath.join(repo_path, '.git')
511
env['GIT_WORK_TREE'] = repo_path
512
env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = 'GeckoView Docs Bot'
513
env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = 'nobody@mozilla.com'
514
515
# Extract new javadoc to specified directory inside repo.
516
import mozfile
517
src_tar = mozpath.join(self.topobjdir, 'gradle', 'build', 'mobile', 'android',
518
'geckoview', 'libs', 'geckoview-javadoc.jar')
519
dst_path = mozpath.join(repo_path, branch_path.format(**fmt))
520
mozfile.remove(dst_path)
521
mozfile.extract_zip(src_tar, dst_path)
522
523
# Commit and push.
524
self.run_process(['git', 'add', '--all'], append_env=env, pass_thru=True)
525
if self.run_process(['git', 'diff', '--cached', '--quiet'],
526
append_env=env, pass_thru=True, ensure_exit_code=False) != 0:
527
# We have something to commit.
528
self.run_process(['git', 'commit',
529
'--message', upload_message.format(**fmt)],
530
append_env=env, pass_thru=True)
531
self.run_process(['git', 'push', 'origin', 'gh-pages'],
532
append_env=env, pass_thru=True)
533
534
mozfile.remove(repo_path)
535
if secret:
536
mozfile.remove(keyfile)
537
return 0
538
539
@Command('gradle', category='devenv',
540
description='Run gradle.',
541
conditions=[conditions.is_android])
542
@CommandArgument('-v', '--verbose', action='store_true',
543
help='Verbose output for what commands the build is running.')
544
@CommandArgument('args', nargs=argparse.REMAINDER)
545
def gradle(self, args, verbose=False):
546
if not verbose:
547
# Avoid logging the command
548
self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
549
550
# In automation, JAVA_HOME is set via mozconfig, which needs
551
# to be specially handled in each mach command. This turns
552
# $JAVA_HOME/bin/java into $JAVA_HOME.
553
java_home = os.path.dirname(os.path.dirname(self.substs['JAVA']))
554
555
gradle_flags = self.substs.get('GRADLE_FLAGS', '') or \
556
os.environ.get('GRADLE_FLAGS', '')
557
gradle_flags = shell_split(gradle_flags)
558
559
# We force the Gradle JVM to run with the UTF-8 encoding, since we
560
# filter strings.xml, which is really UTF-8; the ellipsis character is
561
# replaced with ??? in some encodings (including ASCII). It's not yet
562
# possible to filter with encodings in Gradle
563
# (https://github.com/gradle/gradle/pull/520) and it's challenging to
564
# do our filtering with Gradle's Ant support. Moreover, all of the
565
# Android tools expect UTF-8: see
567
# http://stackoverflow.com/a/21267635 for discussion of this approach.
568
#
569
# It's not even enough to set the encoding just for Gradle; it
570
# needs to be for JVMs spawned by Gradle as well. This
571
# happens during the maven deployment generating the GeckoView
572
# documents; this works around "error: unmappable character
573
# for encoding ASCII" in exoplayer2. See
575
# and especially https://stackoverflow.com/a/21755671.
576
577
if self.substs.get('MOZ_AUTOMATION'):
578
gradle_flags += ['--console=plain']
579
580
return self.run_process(
581
[self.substs['GRADLE']] + gradle_flags + args,
582
append_env={
583
'GRADLE_OPTS': '-Dfile.encoding=utf-8',
584
'JAVA_HOME': java_home,
585
'JAVA_TOOL_OPTIONS': '-Dfile.encoding=utf-8',
586
},
587
pass_thru=True, # Allow user to run gradle interactively.
588
ensure_exit_code=False, # Don't throw on non-zero exit code.
589
cwd=mozpath.join(self.topsrcdir))
590
591
@Command('gradle-install', category='devenv',
592
conditions=[REMOVED])
593
def gradle_install(self):
594
pass
595
596
@Command('install-android', category='post-build',
597
conditional_name='install',
598
conditions=[conditions.is_android],
599
description='Install an Android package on a device or an emulator.')
600
@CommandArgument('--verbose', '-v', action='store_true',
601
help='Print verbose output when installing.')
602
def install(self, verbose=False):
603
from mozrunner.devices.android_device import verify_android_device
604
verify_android_device(self, verbose=verbose)
605
606
ret = self._run_make(directory='.', target='install', ensure_exit_code=False)
607
if ret == 0:
608
self.notify('Install complete')
609
return ret
610
611
@Command('run-android', category='post-build',
612
conditional_name='run',
613
conditions=[conditions.is_android],
614
description='Run an application on an Android device or an emulator.')
615
@CommandArgument('--app', help='Android package to run '
616
'(default: org.mozilla.geckoview_example)',
617
default='org.mozilla.geckoview_example')
618
@CommandArgument('--intent', help='Android intent action to launch with '
619
'(default: android.intent.action.VIEW)',
620
default='android.intent.action.VIEW')
621
@CommandArgument('--setenv', dest='env', action='append',
622
help='Set target environment variable, like FOO=BAR',
623
default=[])
624
@CommandArgument('--profile', '-P', help='Path to Gecko profile, like /path/to/host/profile '
625
'or /path/to/target/profile',
626
default=None)
627
@CommandArgument('--url', help='URL to open',
628
default=None)
629
@CommandArgument('--no-install', help='Do not try to install application on device before ' +
630
'running (default: False)',
631
action='store_true',
632
default=False)
633
@CommandArgument('--no-wait', help='Do not wait for application to start before returning ' +
634
'(default: False)',
635
action='store_true',
636
default=False)
637
@CommandArgument('--fail-if-running', help='Fail if application is already running ' +
638
'(default: False)',
639
action='store_true',
640
default=False)
641
@CommandArgument('--restart', help='Stop the application if it is already running ' +
642
'(default: False)',
643
action='store_true',
644
default=False)
645
def run(self, app='org.mozilla.geckoview_example', intent=None,
646
env=[], profile=None,
647
url=None, no_install=None, no_wait=None, fail_if_running=None, restart=None):
648
from mozrunner.devices.android_device import verify_android_device, _get_device
649
from six.moves import shlex_quote
650
651
if app == 'org.mozilla.geckoview_example':
652
activity_name = 'org.mozilla.geckoview_example.GeckoViewActivity'
653
elif app == 'org.mozilla.geckoview.test':
654
activity_name = 'org.mozilla.geckoview.test.TestRunnerActivity'
655
elif 'fennec' in app or 'firefox' in app:
656
activity_name = 'org.mozilla.gecko.BrowserApp'
657
else:
658
raise RuntimeError('Application not recognized: {}'.format(app))
659
660
# `verify_android_device` respects `DEVICE_SERIAL` if it is set and sets it otherwise.
661
verify_android_device(self, app=app, install=not no_install)
662
device_serial = os.environ.get('DEVICE_SERIAL')
663
if not device_serial:
664
print('No ADB devices connected.')
665
return 1
666
667
device = _get_device(self.substs, device_serial=device_serial)
668
669
args = []
670
if profile:
671
if os.path.isdir(profile):
672
host_profile = profile
673
# Always /data/local/tmp, rather than `device.test_root`, because GeckoView only
674
# takes its configuration file from /data/local/tmp, and we want to follow suit.
675
target_profile = '/data/local/tmp/{}-profile'.format(app)
676
device.rm(target_profile, recursive=True, force=True)
677
device.push(host_profile, target_profile)
678
self.log(logging.INFO, "run",
679
{'host_profile': host_profile, 'target_profile': target_profile},
680
'Pushed profile from host "{host_profile}" to target "{target_profile}"')
681
else:
682
target_profile = profile
683
self.log(logging.INFO, "run",
684
{'target_profile': target_profile},
685
'Using profile from target "{target_profile}"')
686
687
args = ['--profile', shlex_quote(target_profile)]
688
689
extras = {}
690
for i, e in enumerate(env):
691
extras['env{}'.format(i)] = e
692
if args:
693
extras['args'] = " ".join(args)
694
extras['use_multiprocess'] = True # Only GVE and TRA process this extra.
695
696
if env or args:
697
restart = True
698
699
if restart:
700
fail_if_running = False
701
self.log(logging.INFO, "run",
702
{'app': app},
703
'Stopping {app} to ensure clean restart.')
704
device.stop_application(app)
705
706
# We'd prefer to log the actual `am start ...` command, but it's not trivial to wire the
707
# device's logger to mach's logger.
708
self.log(logging.INFO, "run",
709
{'app': app, 'activity_name': activity_name},
710
'Starting {app}/{activity_name}.')
711
712
device.launch_application(
713
app_name=app,
714
activity_name=activity_name,
715
intent=intent,
716
extras=extras,
717
url=url,
718
wait=not no_wait,
719
fail_if_running=fail_if_running)
720
721
return 0
722
723
724
def _get_maven_archive_abs_and_relative_paths(maven_folder):
725
for subdir, _, files in os.walk(maven_folder):
726
for file in files:
727
full_path = os.path.join(subdir, file)
728
relative_path = os.path.relpath(full_path, maven_folder)
729
730
# maven-metadata is intended to be generated on the real maven server
731
if 'maven-metadata.xml' not in relative_path:
732
yield full_path, relative_path
733
734
735
def _craft_maven_zip_archive(topobjdir):
736
geckoview_folder = os.path.join(topobjdir, 'gradle/build/mobile/android/geckoview')
737
maven_folder = os.path.join(geckoview_folder, 'maven')
738
739
with ZipFile(os.path.join(geckoview_folder, 'target.maven.zip'), 'w') as target_zip:
740
for abs, rel in _get_maven_archive_abs_and_relative_paths(maven_folder):
741
target_zip.write(abs, arcname=rel)
742
743
744
@CommandProvider
745
class AndroidEmulatorCommands(MachCommandBase):
746
"""
747
Run the Android emulator with one of the AVDs used in the Mozilla
748
automated test environment. If necessary, the AVD is fetched from
749
the tooltool server and installed.
750
"""
751
@Command('android-emulator', category='devenv',
752
conditions=[],
753
description='Run the Android emulator with an AVD from test automation.')
754
@CommandArgument('--version', metavar='VERSION',
755
choices=['4.3', 'x86', 'x86-7.0'],
756
help='Specify Android version to run in emulator. '
757
'One of "4.3", "x86", or "x86-7.0".')
758
@CommandArgument('--wait', action='store_true',
759
help='Wait for emulator to be closed.')
760
@CommandArgument('--force-update', action='store_true',
761
help='Update AVD definition even when AVD is already installed.')
762
@CommandArgument('--verbose', action='store_true',
763
help='Log informative status messages.')
764
def emulator(self, version, wait=False, force_update=False, verbose=False):
765
from mozrunner.devices.android_device import AndroidEmulator
766
767
emulator = AndroidEmulator(version, verbose, substs=self.substs,
768
device_serial='emulator-5554')
769
if emulator.is_running():
770
# It is possible to run multiple emulators simultaneously, but:
771
# - if more than one emulator is using the same avd, errors may
772
# occur due to locked resources;
773
# - additional parameters must be specified when running tests,
774
# to select a specific device.
775
# To avoid these complications, allow just one emulator at a time.
776
self.log(logging.ERROR, "emulator", {},
777
"An Android emulator is already running.\n"
778
"Close the existing emulator and re-run this command.")
779
return 1
780
781
if not emulator.is_available():
782
self.log(logging.WARN, "emulator", {},
783
"Emulator binary not found.\n"
784
"Install the Android SDK and make sure 'emulator' is in your PATH.")
785
return 2
786
787
if not emulator.check_avd(force_update):
788
self.log(logging.INFO, "emulator", {},
789
"Fetching and installing AVD. This may take a few minutes...")
790
emulator.update_avd(force_update)
791
792
self.log(logging.INFO, "emulator", {},
793
"Starting Android emulator running %s..." %
794
emulator.get_avd_description())
795
emulator.start()
796
if emulator.wait_for_start():
797
self.log(logging.INFO, "emulator", {},
798
"Android emulator is running.")
799
else:
800
# This is unusual but the emulator may still function.
801
self.log(logging.WARN, "emulator", {},
802
"Unable to verify that emulator is running.")
803
804
if conditions.is_android(self):
805
self.log(logging.INFO, "emulator", {},
806
"Use 'mach install' to install or update Firefox on your emulator.")
807
else:
808
self.log(logging.WARN, "emulator", {},
809
"No Firefox for Android build detected.\n"
810
"Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
811
"to setup an Android build environment.")
812
813
if wait:
814
self.log(logging.INFO, "emulator", {},
815
"Waiting for Android emulator to close...")
816
rc = emulator.wait()
817
if rc is not None:
818
self.log(logging.INFO, "emulator", {},
819
"Android emulator completed with return code %d." % rc)
820
else:
821
self.log(logging.WARN, "emulator", {},
822
"Unable to retrieve Android emulator return code.")
823
return 0