mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 22:01:30 +00:00
aa3ebe8988
Differential Revision: https://phabricator.services.mozilla.com/D23416 --HG-- extra : moz-landing-system : lando
740 lines
34 KiB
Python
Executable File
740 lines
34 KiB
Python
Executable File
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import zipfile
|
|
import json
|
|
|
|
from zipfile import ZipFile
|
|
|
|
import mozpack.path as mozpath
|
|
|
|
from mozfile import TemporaryDirectory
|
|
|
|
from mozbuild.base import (
|
|
MachCommandBase,
|
|
MachCommandConditions as conditions,
|
|
)
|
|
|
|
from mozbuild.shellutil import (
|
|
split as shell_split,
|
|
)
|
|
|
|
from mach.decorators import (
|
|
CommandArgument,
|
|
CommandProvider,
|
|
Command,
|
|
SubCommand,
|
|
)
|
|
|
|
|
|
# NOTE python/mach/mach/commands/commandinfo.py references this function
|
|
# by name. If this function is renamed or removed, that file should
|
|
# be updated accordingly as well.
|
|
def REMOVED(cls):
|
|
"""Command no longer exists! Use the Gradle configuration rooted in the top source directory
|
|
instead.
|
|
|
|
See https://developer.mozilla.org/en-US/docs/Simple_Firefox_for_Android_build#Developing_Firefox_for_Android_in_Android_Studio_or_IDEA_IntelliJ. # NOQA: E501
|
|
"""
|
|
return False
|
|
|
|
|
|
@CommandProvider
|
|
class MachCommands(MachCommandBase):
|
|
def _root_url(self, artifactdir=None, objdir=None):
|
|
"""Generate a publicly-accessible URL for the tasks's artifacts, or an objdir path"""
|
|
if 'TASK_ID' in os.environ and 'RUN_ID' in os.environ:
|
|
import taskcluster_urls
|
|
from taskgraph.util.taskcluster import get_root_url
|
|
return taskcluster_urls.api(
|
|
get_root_url(False), 'queue', 'v1', 'task/{}/runs/{}/artifacts/{}'.format(
|
|
os.environ['TASK_ID'], os.environ['RUN_ID'], artifactdir))
|
|
else:
|
|
return os.path.join(self.topobjdir, objdir)
|
|
|
|
@Command('android', category='devenv',
|
|
description='Run Android-specific commands.',
|
|
conditions=[conditions.is_android])
|
|
def android(self):
|
|
pass
|
|
|
|
@SubCommand('android', 'assemble-app',
|
|
"""Assemble Firefox for Android.
|
|
See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_assemble_app(self, args):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_APP_TASKS'] +
|
|
['-x', 'lint'] + args, verbose=True)
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'generate-sdk-bindings',
|
|
"""Generate SDK bindings used when building GeckoView.""")
|
|
@CommandArgument('inputs', nargs='+', help='config files, '
|
|
'like [/path/to/ClassName-classes.txt]+')
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_generate_sdk_bindings(self, inputs, args):
|
|
import itertools
|
|
|
|
def stem(input):
|
|
# Turn "/path/to/ClassName-classes.txt" into "ClassName".
|
|
return os.path.basename(input).rsplit('-classes.txt', 1)[0]
|
|
|
|
bindings_inputs = list(itertools.chain(*((input, stem(input)) for input in inputs)))
|
|
bindings_args = '-Pgenerate_sdk_bindings_args={}'.format(':'.join(bindings_inputs))
|
|
|
|
ret = self.gradle(
|
|
self.substs['GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS'] + [bindings_args] + args,
|
|
verbose=True)
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'generate-generated-jni-wrappers',
|
|
"""Generate GeckoView JNI wrappers used when building GeckoView.""")
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_generate_generated_jni_wrappers(self, args):
|
|
ret = self.gradle(
|
|
self.substs['GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS'] + args,
|
|
verbose=True)
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'generate-fennec-jni-wrappers',
|
|
"""Generate Fennec-specific JNI wrappers used when building
|
|
Firefox for Android.""")
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_generate_fennec_jni_wrappers(self, args):
|
|
ret = self.gradle(
|
|
self.substs['GRADLE_ANDROID_GENERATE_FENNEC_JNI_WRAPPERS_TASKS'] + args, verbose=True)
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'api-lint',
|
|
"""Runs apilint against GeckoView.""")
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_api_lint(self, args):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_API_LINT_TASKS'] + args, verbose=True)
|
|
folder = self.substs['GRADLE_ANDROID_GECKOVIEW_APILINT_FOLDER']
|
|
|
|
with open(os.path.join(
|
|
self.topobjdir,
|
|
'{}/apilint-result.json'.format(folder))) as f:
|
|
result = json.load(f)
|
|
|
|
print('SUITE-START | android-api-lint')
|
|
for r in result['compat_failures'] + result['failures']:
|
|
print ('TEST-UNEXPECTED-FAIL | {} | {}'.format(r['detail'], r['msg']))
|
|
for r in result['api_changes']:
|
|
print ('TEST-UNEXPECTED-FAIL | {} | Unexpected api change'.format(r))
|
|
print('SUITE-END | android-api-lint')
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'test',
|
|
"""Run Android local unit tests.
|
|
See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-test""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_test(self, args):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_TEST_TASKS'] +
|
|
args, verbose=True)
|
|
|
|
ret |= self._parse_android_test_results('public/app/unittest',
|
|
'gradle/build/mobile/android/app',
|
|
(self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],))
|
|
|
|
ret |= self._parse_android_test_results('public/geckoview/unittest',
|
|
'gradle/build/mobile/android/geckoview',
|
|
(self.substs['GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME'],)) # NOQA: E501
|
|
|
|
return ret
|
|
|
|
def _parse_android_test_results(self, artifactdir, gradledir, variants):
|
|
# Unit tests produce both HTML and XML reports. Visit the
|
|
# XML report(s) to report errors and link to the HTML
|
|
# report(s) for human consumption.
|
|
import itertools
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from mozpack.files import (
|
|
FileFinder,
|
|
)
|
|
|
|
ret = 0
|
|
found_reports = False
|
|
|
|
root_url = self._root_url(
|
|
artifactdir=artifactdir,
|
|
objdir=gradledir + '/reports/tests')
|
|
|
|
def capitalize(s):
|
|
# Can't use str.capitalize because it lower cases trailing letters.
|
|
return (s[0].upper() + s[1:]) if s else ''
|
|
|
|
for variant in variants:
|
|
report = 'test{}UnitTest'.format(capitalize(variant))
|
|
finder = FileFinder(os.path.join(self.topobjdir, gradledir + '/test-results/', report))
|
|
for p, _ in finder.find('TEST-*.xml'):
|
|
found_reports = True
|
|
f = open(os.path.join(finder.base, p), 'rt')
|
|
tree = ET.parse(f)
|
|
root = tree.getroot()
|
|
|
|
# Log reports for Tree Herder "Job Details".
|
|
print('TinderboxPrint: report<br/><a href="{}/{}/index.html">HTML {} report</a>, visit "Inspect Task" link for details'.format(root_url, report, report)) # NOQA: E501
|
|
|
|
# And make the report display as soon as possible.
|
|
failed = root.findall('testcase/error') or root.findall('testcase/failure')
|
|
if failed:
|
|
print(
|
|
'TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the reports at: {}/{}/index.html'.format(root_url, report)) # NOQA: E501
|
|
|
|
print('SUITE-START | android-test | {} {}'.format(report, root.get('name')))
|
|
|
|
for testcase in root.findall('testcase'):
|
|
name = testcase.get('name')
|
|
print('TEST-START | {}'.format(name))
|
|
|
|
# Schema cribbed from
|
|
# http://llg.cubic.org/docs/junit/. There's no
|
|
# particular advantage to formatting the error, so
|
|
# for now let's just output the unexpected XML
|
|
# tag.
|
|
error_count = 0
|
|
for unexpected in itertools.chain(testcase.findall('error'),
|
|
testcase.findall('failure')):
|
|
for line in ET.tostring(unexpected).strip().splitlines():
|
|
print('TEST-UNEXPECTED-FAIL | {} | {}'.format(name, line))
|
|
error_count += 1
|
|
ret |= 1
|
|
|
|
# Skipped tests aren't unexpected at this time; we
|
|
# disable some tests that require live remote
|
|
# endpoints.
|
|
for skipped in testcase.findall('skipped'):
|
|
for line in ET.tostring(skipped).strip().splitlines():
|
|
print('TEST-INFO | {} | {}'.format(name, line))
|
|
|
|
if not error_count:
|
|
print('TEST-PASS | {}'.format(name))
|
|
|
|
print('SUITE-END | android-test | {} {}'.format(report, root.get('name')))
|
|
|
|
if not found_reports:
|
|
print('TEST-UNEXPECTED-FAIL | android-test | No reports found under {}'.format(gradledir)) # NOQA: E501
|
|
return 1
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'test-ccov',
|
|
"""Run Android local unit tests in order to get a code coverage report.
|
|
See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_test_ccov(self, args):
|
|
enable_ccov = '-Penable_code_coverage'
|
|
|
|
# Don't care if the tests are failing, we only want the coverage information.
|
|
self.android_test([enable_ccov])
|
|
|
|
self.gradle(self.substs['GRADLE_ANDROID_TEST_CCOV_REPORT_TASKS'] +
|
|
[enable_ccov] + args, verbose=True)
|
|
self._process_jacoco_reports()
|
|
return 0
|
|
|
|
def _process_jacoco_reports(self):
|
|
def run_grcov(grcov_path, input_path):
|
|
args = [grcov_path, input_path, '-t', 'lcov']
|
|
return subprocess.check_output(args)
|
|
|
|
with TemporaryDirectory() as xml_dir:
|
|
grcov = os.path.join(os.environ['MOZ_FETCHES_DIR'], 'grcov')
|
|
|
|
report_xml_template = self.topobjdir + '/gradle/build/mobile/android/%s/reports/jacoco/jacocoTestReport/jacocoTestReport.xml' # NOQA: E501
|
|
shutil.copy(report_xml_template % 'app', os.path.join(xml_dir, 'app.xml'))
|
|
shutil.copy(report_xml_template % 'geckoview', os.path.join(xml_dir, 'geckoview.xml'))
|
|
|
|
# Parse output files with grcov.
|
|
grcov_output = run_grcov(grcov, xml_dir)
|
|
grcov_zip_path = os.path.join(self.topobjdir, 'code-coverage-grcov.zip')
|
|
with zipfile.ZipFile(grcov_zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
|
z.writestr('grcov_lcov_output.info', grcov_output)
|
|
|
|
@SubCommand('android', 'lint',
|
|
"""Run Android lint.
|
|
See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-lint""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_lint(self, args):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_LINT_TASKS'] +
|
|
args, verbose=True)
|
|
|
|
# Android Lint produces both HTML and XML reports. Visit the
|
|
# XML report(s) to report errors and link to the HTML
|
|
# report(s) for human consumption.
|
|
import xml.etree.ElementTree as ET
|
|
|
|
root_url = self._root_url(
|
|
artifactdir='public/android/lint',
|
|
objdir='gradle/build/mobile/android/app/reports')
|
|
|
|
reports = (self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],)
|
|
for report in reports:
|
|
f = open(os.path.join(
|
|
self.topobjdir,
|
|
'gradle/build/mobile/android/app/reports/lint-results-{}.xml'.format(report)),
|
|
'rt')
|
|
tree = ET.parse(f)
|
|
root = tree.getroot()
|
|
|
|
# Log reports for Tree Herder "Job Details".
|
|
html_report_url = '{}/lint-results-{}.html'.format(root_url, report)
|
|
xml_report_url = '{}/lint-results-{}.xml'.format(root_url, report)
|
|
print('TinderboxPrint: report<br/><a href="{}">HTML {} report</a>, visit "Inspect Task" link for details'.format(html_report_url, report)) # NOQA: E501
|
|
print('TinderboxPrint: report<br/><a href="{}">XML {} report</a>, visit "Inspect Task" link for details'.format(xml_report_url, report)) # NOQA: E501
|
|
|
|
# And make the report display as soon as possible.
|
|
if root.findall("issue[@severity='Error']"):
|
|
print('TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: {}'.format(html_report_url)) # NOQA: E501
|
|
|
|
print('SUITE-START | android-lint | {}'.format(report))
|
|
for issue in root.findall("issue[@severity='Error']"):
|
|
# There's no particular advantage to formatting the
|
|
# error, so for now let's just output the <issue> XML
|
|
# tag.
|
|
for line in ET.tostring(issue).strip().splitlines():
|
|
print('TEST-UNEXPECTED-FAIL | {}'.format(line))
|
|
ret |= 1
|
|
print('SUITE-END | android-lint | {}'.format(report))
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'checkstyle',
|
|
"""Run Android checkstyle.
|
|
See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-checkstyle""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_checkstyle(self, args):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_CHECKSTYLE_TASKS'] +
|
|
args, verbose=True)
|
|
|
|
# Checkstyle produces both HTML and XML reports. Visit the
|
|
# XML report(s) to report errors and link to the HTML
|
|
# report(s) for human consumption.
|
|
import xml.etree.ElementTree as ET
|
|
|
|
f = open(os.path.join(self.topobjdir,
|
|
'gradle/build/mobile/android/app/reports/checkstyle/checkstyle.xml'),
|
|
'rt')
|
|
tree = ET.parse(f)
|
|
root = tree.getroot()
|
|
|
|
# Now the reports, linkified.
|
|
root_url = self._root_url(
|
|
artifactdir='public/android/checkstyle',
|
|
objdir='gradle/build/mobile/android/app/reports/checkstyle')
|
|
|
|
# Log reports for Tree Herder "Job Details".
|
|
print('TinderboxPrint: report<br/><a href="{}/checkstyle.html">HTML checkstyle report</a>, visit "Inspect Task" link for details'.format(root_url)) # NOQA: E501
|
|
print('TinderboxPrint: report<br/><a href="{}/checkstyle.xml">XML checkstyle report</a>, visit "Inspect Task" link for details'.format(root_url)) # NOQA: E501
|
|
|
|
# And make the report display as soon as possible.
|
|
if root.findall('file/error'):
|
|
ret |= 1
|
|
|
|
if ret:
|
|
print('TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: {}/checkstyle.html'.format(root_url)) # NOQA: E501
|
|
|
|
print('SUITE-START | android-checkstyle')
|
|
for file in root.findall('file'):
|
|
name = file.get('name')
|
|
|
|
print('TEST-START | {}'.format(name))
|
|
error_count = 0
|
|
for error in file.findall('error'):
|
|
# There's no particular advantage to formatting the
|
|
# error, so for now let's just output the <error> XML
|
|
# tag.
|
|
print('TEST-UNEXPECTED-FAIL | {}'.format(name))
|
|
for line in ET.tostring(error).strip().splitlines():
|
|
print('TEST-UNEXPECTED-FAIL | {}'.format(line))
|
|
error_count += 1
|
|
|
|
if not error_count:
|
|
print('TEST-PASS | {}'.format(name))
|
|
print('SUITE-END | android-checkstyle')
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'findbugs',
|
|
"""Run Android findbugs.
|
|
See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-findbugs""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_findbugs(self, dryrun=False, args=[]):
|
|
ret = self.gradle(self.substs['GRADLE_ANDROID_FINDBUGS_TASKS'] +
|
|
args, verbose=True)
|
|
|
|
# Findbug produces both HTML and XML reports. Visit the
|
|
# XML report(s) to report errors and link to the HTML
|
|
# report(s) for human consumption.
|
|
import xml.etree.ElementTree as ET
|
|
|
|
root_url = self._root_url(
|
|
artifactdir='public/android/findbugs',
|
|
objdir='gradle/build/mobile/android/app/reports/findbugs')
|
|
|
|
reports = (self.substs['GRADLE_ANDROID_APP_VARIANT_NAME'],)
|
|
for report in reports:
|
|
try:
|
|
f = open(os.path.join(
|
|
self.topobjdir, 'gradle/build/mobile/android/app/reports/findbugs',
|
|
'findbugs-{}-output.xml'.format(report)),
|
|
'rt')
|
|
except IOError:
|
|
continue
|
|
|
|
tree = ET.parse(f)
|
|
root = tree.getroot()
|
|
|
|
# Log reports for Tree Herder "Job Details".
|
|
html_report_url = '{}/findbugs-{}-output.html'.format(root_url, report)
|
|
xml_report_url = '{}/findbugs-{}-output.xml'.format(root_url, report)
|
|
print('TinderboxPrint: report<br/><a href="{}">HTML {} report</a>, visit "Inspect Task" link for details'.format(html_report_url, report)) # NOQA: E501
|
|
print('TinderboxPrint: report<br/><a href="{}">XML {} report</a>, visit "Inspect Task" link for details'.format(xml_report_url, report)) # NOQA: E501
|
|
|
|
# And make the report display as soon as possible.
|
|
if root.findall("./BugInstance"):
|
|
print('TEST-UNEXPECTED-FAIL | android-findbugs | Findbugs found issues in the project. See the report at: {}'.format(html_report_url)) # NOQA: E501
|
|
|
|
print('SUITE-START | android-findbugs | {}'.format(report))
|
|
for error in root.findall('./BugInstance'):
|
|
# There's no particular advantage to formatting the
|
|
# error, so for now let's just output the <error> XML
|
|
# tag.
|
|
print('TEST-UNEXPECTED-FAIL | {}:{} | {}'.format(report,
|
|
error.get('type'),
|
|
error.find('Class')
|
|
.get('classname')))
|
|
for line in ET.tostring(error).strip().splitlines():
|
|
print('TEST-UNEXPECTED-FAIL | {}:{} | {}'.format(report,
|
|
error.get('type'),
|
|
line))
|
|
ret |= 1
|
|
print('SUITE-END | android-findbugs | {}'.format(report))
|
|
|
|
return ret
|
|
|
|
@SubCommand('android', 'gradle-dependencies',
|
|
"""Collect Android Gradle dependencies.
|
|
See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_gradle_dependencies(self, args):
|
|
# We don't want to gate producing dependency archives on clean
|
|
# lint or checkstyle, particularly because toolchain versions
|
|
# can change the outputs for those processes.
|
|
self.gradle(self.substs['GRADLE_ANDROID_DEPENDENCIES_TASKS'] +
|
|
["--continue"] + args, verbose=True)
|
|
|
|
return 0
|
|
|
|
@SubCommand('android', 'archive-coverage-artifacts',
|
|
"""Archive compiled classfiles to be used later in generating code
|
|
coverage reports. See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_archive_classfiles(self, args):
|
|
self.gradle(self.substs['GRADLE_ANDROID_ARCHIVE_COVERAGE_ARTIFACTS_TASKS'] +
|
|
args, verbose=True)
|
|
|
|
return 0
|
|
|
|
@SubCommand('android', 'archive-geckoview',
|
|
"""Create GeckoView archives.
|
|
See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""") # NOQA: E501
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_archive_geckoview(self, args):
|
|
ret = self.gradle(
|
|
self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + args,
|
|
verbose=True)
|
|
|
|
if ret != 0:
|
|
return ret
|
|
|
|
# The zip archive is passed along in CI to ship geckoview onto a maven repo
|
|
_craft_maven_zip_archive(self.topobjdir)
|
|
|
|
return 0
|
|
|
|
@SubCommand('android', 'build-geckoview_example',
|
|
"""Build geckoview_example """)
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_build_geckoview_example(self, args):
|
|
self.gradle(self.substs['GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS'] + args,
|
|
verbose=True)
|
|
|
|
print('Execute `mach android install-geckoview_example` '
|
|
'to push the geckoview_example and test APKs to a device.')
|
|
|
|
return 0
|
|
|
|
@SubCommand('android', 'install-geckoview_example',
|
|
"""Install geckoview_example """)
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def android_install_geckoview_example(self, args):
|
|
self.gradle(self.substs['GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS'] + args,
|
|
verbose=True)
|
|
|
|
print('Execute `mach android build-geckoview_example` '
|
|
'to just build the geckoview_example and test APKs.')
|
|
|
|
return 0
|
|
|
|
@SubCommand('android', 'geckoview-docs',
|
|
"""Create GeckoView javadoc and optionally upload to Github""")
|
|
@CommandArgument('--archive', action='store_true',
|
|
help='Generate a javadoc archive.')
|
|
@CommandArgument('--upload', metavar='USER/REPO',
|
|
help='Upload generated javadoc to Github, '
|
|
'using the specified USER/REPO.')
|
|
@CommandArgument('--upload-branch', metavar='BRANCH[/PATH]',
|
|
default='gh-pages/javadoc',
|
|
help='Use the specified branch/path for commits.')
|
|
@CommandArgument('--upload-message', metavar='MSG',
|
|
default='GeckoView docs upload',
|
|
help='Use the specified message for commits.')
|
|
def android_geckoview_docs(self, archive, upload, upload_branch,
|
|
upload_message):
|
|
|
|
tasks = (self.substs['GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS'] if archive or upload
|
|
else self.substs['GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS'])
|
|
|
|
ret = self.gradle(tasks, verbose=True)
|
|
if ret or not upload:
|
|
return ret
|
|
|
|
# Upload to Github.
|
|
fmt = {
|
|
'level': os.environ.get('MOZ_SCM_LEVEL', '0'),
|
|
'project': os.environ.get('MH_BRANCH', 'unknown'),
|
|
'revision': os.environ.get('GECKO_HEAD_REV', 'tip'),
|
|
}
|
|
env = {}
|
|
|
|
# In order to push to GitHub from TaskCluster, we store a private key
|
|
# in the TaskCluster secrets store in the format {"content": "<KEY>"},
|
|
# and the corresponding public key as a writable deploy key for the
|
|
# destination repo on GitHub.
|
|
secret = os.environ.get('GECKOVIEW_DOCS_UPLOAD_SECRET', '').format(**fmt)
|
|
if secret:
|
|
# Set up a private key from the secrets store if applicable.
|
|
import requests
|
|
req = requests.get('http://taskcluster/secrets/v1/secret/' + secret)
|
|
req.raise_for_status()
|
|
|
|
keyfile = mozpath.abspath('gv-docs-upload-key')
|
|
with open(keyfile, 'w') as f:
|
|
os.chmod(keyfile, 0o600)
|
|
f.write(req.json()['secret']['content'])
|
|
|
|
# Turn off strict host key checking so ssh does not complain about
|
|
# unknown github.com host. We're not pushing anything sensitive, so
|
|
# it's okay to not check GitHub's host keys.
|
|
env['GIT_SSH_COMMAND'] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
|
|
|
|
# Clone remote repo.
|
|
branch, _, branch_path = upload_branch.partition('/')
|
|
repo_url = 'git@github.com:%s.git' % upload
|
|
repo_path = mozpath.abspath('gv-docs-repo')
|
|
self.run_process(['git', 'clone', '--branch', branch, '--depth', '1',
|
|
repo_url, repo_path], append_env=env, pass_thru=True)
|
|
env['GIT_DIR'] = mozpath.join(repo_path, '.git')
|
|
env['GIT_WORK_TREE'] = repo_path
|
|
env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = 'GeckoView Docs Bot'
|
|
env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = 'nobody@mozilla.com'
|
|
|
|
# Extract new javadoc to specified directory inside repo.
|
|
import mozfile
|
|
src_tar = mozpath.join(self.topobjdir, 'gradle', 'build', 'mobile', 'android',
|
|
'geckoview', 'libs', 'geckoview-javadoc.jar')
|
|
dst_path = mozpath.join(repo_path, branch_path.format(**fmt))
|
|
mozfile.remove(dst_path)
|
|
mozfile.extract_zip(src_tar, dst_path)
|
|
|
|
# Commit and push.
|
|
self.run_process(['git', 'add', '--all'], append_env=env, pass_thru=True)
|
|
if self.run_process(['git', 'diff', '--cached', '--quiet'],
|
|
append_env=env, pass_thru=True, ensure_exit_code=False) != 0:
|
|
# We have something to commit.
|
|
self.run_process(['git', 'commit',
|
|
'--message', upload_message.format(**fmt)],
|
|
append_env=env, pass_thru=True)
|
|
self.run_process(['git', 'push', 'origin', 'gh-pages'],
|
|
append_env=env, pass_thru=True)
|
|
|
|
mozfile.remove(repo_path)
|
|
if secret:
|
|
mozfile.remove(keyfile)
|
|
return 0
|
|
|
|
@Command('gradle', category='devenv',
|
|
description='Run gradle.',
|
|
conditions=[conditions.is_android])
|
|
@CommandArgument('-v', '--verbose', action='store_true',
|
|
help='Verbose output for what commands the build is running.')
|
|
@CommandArgument('args', nargs=argparse.REMAINDER)
|
|
def gradle(self, args, verbose=False):
|
|
if not verbose:
|
|
# Avoid logging the command
|
|
self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
|
|
|
|
# In automation, JAVA_HOME is set via mozconfig, which needs
|
|
# to be specially handled in each mach command. This turns
|
|
# $JAVA_HOME/bin/java into $JAVA_HOME.
|
|
java_home = os.path.dirname(os.path.dirname(self.substs['JAVA']))
|
|
|
|
gradle_flags = self.substs.get('GRADLE_FLAGS', '') or \
|
|
os.environ.get('GRADLE_FLAGS', '')
|
|
gradle_flags = shell_split(gradle_flags)
|
|
|
|
# We force the Gradle JVM to run with the UTF-8 encoding, since we
|
|
# filter strings.xml, which is really UTF-8; the ellipsis character is
|
|
# replaced with ??? in some encodings (including ASCII). It's not yet
|
|
# possible to filter with encodings in Gradle
|
|
# (https://github.com/gradle/gradle/pull/520) and it's challenging to
|
|
# do our filtering with Gradle's Ant support. Moreover, all of the
|
|
# Android tools expect UTF-8: see
|
|
# http://tools.android.com/knownissues/encoding. See
|
|
# http://stackoverflow.com/a/21267635 for discussion of this approach.
|
|
#
|
|
# It's not even enough to set the encoding just for Gradle; it
|
|
# needs to be for JVMs spawned by Gradle as well. This
|
|
# happens during the maven deployment generating the GeckoView
|
|
# documents; this works around "error: unmappable character
|
|
# for encoding ASCII" in exoplayer2. See
|
|
# https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11 # NOQA: E501
|
|
# and especially https://stackoverflow.com/a/21755671.
|
|
|
|
if self.substs.get('MOZ_AUTOMATION'):
|
|
gradle_flags += ['--console=plain']
|
|
|
|
return self.run_process(
|
|
[self.substs['GRADLE']] + gradle_flags + args,
|
|
append_env={
|
|
'GRADLE_OPTS': '-Dfile.encoding=utf-8',
|
|
'JAVA_HOME': java_home,
|
|
'JAVA_TOOL_OPTIONS': '-Dfile.encoding=utf-8',
|
|
},
|
|
pass_thru=True, # Allow user to run gradle interactively.
|
|
ensure_exit_code=False, # Don't throw on non-zero exit code.
|
|
cwd=mozpath.join(self.topsrcdir))
|
|
|
|
@Command('gradle-install', category='devenv',
|
|
conditions=[REMOVED])
|
|
def gradle_install(self):
|
|
pass
|
|
|
|
|
|
def _get_maven_archive_abs_and_relative_paths(maven_folder):
|
|
for subdir, _, files in os.walk(maven_folder):
|
|
for file in files:
|
|
full_path = os.path.join(subdir, file)
|
|
relative_path = os.path.relpath(full_path, maven_folder)
|
|
|
|
# maven-metadata is intended to be generated on the real maven server
|
|
if 'maven-metadata.xml' not in relative_path:
|
|
yield full_path, relative_path
|
|
|
|
|
|
def _craft_maven_zip_archive(topobjdir):
|
|
geckoview_folder = os.path.join(topobjdir, 'gradle/build/mobile/android/geckoview')
|
|
maven_folder = os.path.join(geckoview_folder, 'maven')
|
|
|
|
with ZipFile(os.path.join(geckoview_folder, 'target.maven.zip'), 'w') as target_zip:
|
|
for abs, rel in _get_maven_archive_abs_and_relative_paths(maven_folder):
|
|
target_zip.write(abs, arcname=rel)
|
|
|
|
|
|
@CommandProvider
|
|
class AndroidEmulatorCommands(MachCommandBase):
|
|
"""
|
|
Run the Android emulator with one of the AVDs used in the Mozilla
|
|
automated test environment. If necessary, the AVD is fetched from
|
|
the tooltool server and installed.
|
|
"""
|
|
@Command('android-emulator', category='devenv',
|
|
conditions=[],
|
|
description='Run the Android emulator with an AVD from test automation.')
|
|
@CommandArgument('--version', metavar='VERSION',
|
|
choices=['4.3', 'x86', 'x86-7.0'],
|
|
help='Specify Android version to run in emulator. '
|
|
'One of "4.3", "x86", or "x86-7.0".')
|
|
@CommandArgument('--wait', action='store_true',
|
|
help='Wait for emulator to be closed.')
|
|
@CommandArgument('--force-update', action='store_true',
|
|
help='Update AVD definition even when AVD is already installed.')
|
|
@CommandArgument('--verbose', action='store_true',
|
|
help='Log informative status messages.')
|
|
def emulator(self, version, wait=False, force_update=False, verbose=False):
|
|
from mozrunner.devices.android_device import AndroidEmulator
|
|
|
|
emulator = AndroidEmulator(version, verbose, substs=self.substs,
|
|
device_serial='emulator-5554')
|
|
if emulator.is_running():
|
|
# It is possible to run multiple emulators simultaneously, but:
|
|
# - if more than one emulator is using the same avd, errors may
|
|
# occur due to locked resources;
|
|
# - additional parameters must be specified when running tests,
|
|
# to select a specific device.
|
|
# To avoid these complications, allow just one emulator at a time.
|
|
self.log(logging.ERROR, "emulator", {},
|
|
"An Android emulator is already running.\n"
|
|
"Close the existing emulator and re-run this command.")
|
|
return 1
|
|
|
|
if not emulator.is_available():
|
|
self.log(logging.WARN, "emulator", {},
|
|
"Emulator binary not found.\n"
|
|
"Install the Android SDK and make sure 'emulator' is in your PATH.")
|
|
return 2
|
|
|
|
if not emulator.check_avd(force_update):
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Fetching and installing AVD. This may take a few minutes...")
|
|
emulator.update_avd(force_update)
|
|
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Starting Android emulator running %s..." %
|
|
emulator.get_avd_description())
|
|
emulator.start()
|
|
if emulator.wait_for_start():
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Android emulator is running.")
|
|
else:
|
|
# This is unusual but the emulator may still function.
|
|
self.log(logging.WARN, "emulator", {},
|
|
"Unable to verify that emulator is running.")
|
|
|
|
if conditions.is_android(self):
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Use 'mach install' to install or update Firefox on your emulator.")
|
|
else:
|
|
self.log(logging.WARN, "emulator", {},
|
|
"No Firefox for Android build detected.\n"
|
|
"Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
|
|
"to setup an Android build environment.")
|
|
|
|
if wait:
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Waiting for Android emulator to close...")
|
|
rc = emulator.wait()
|
|
if rc is not None:
|
|
self.log(logging.INFO, "emulator", {},
|
|
"Android emulator completed with return code %d." % rc)
|
|
else:
|
|
self.log(logging.WARN, "emulator", {},
|
|
"Unable to retrieve Android emulator return code.")
|
|
return 0
|