1 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
2 # vim: set filetype=python:
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
15 import xml
.etree
.ElementTree
as ET
17 import mozpack
.path
as mozpath
18 from mozlint
import result
19 from mozpack
.files
import FileFinder
21 # The Gradle target invocations are serialized with a simple locking file scheme. It's fine for
22 # them to take a while, since the first will compile all the Java, etc, and then perform
23 # potentially expensive static analyses.
24 GRADLE_LOCK_MAX_WAIT_SECONDS
= 20 * 60
27 os
.path
.join("tools", "rewriting", "Generated.txt"),
28 os
.path
.join("tools", "rewriting", "ThirdPartyPaths.txt"),
32 def setup(root
, **setupargs
):
33 if setupargs
.get("substs", {}).get("MOZ_BUILD_APP") != "mobile/android":
36 if "topobjdir" not in setupargs
:
37 setupargs
["log"].debug(
38 f
"Skipping {setupargs['name']}: a configured Android build is required!"
45 def gradle(log
, topsrcdir
=None, topobjdir
=None, tasks
=[], extra_args
=[], verbose
=True):
46 sys
.path
.insert(0, os
.path
.join(topsrcdir
, "mobile", "android"))
47 from gradle
import gradle_lock
49 with
gradle_lock(topobjdir
, max_wait_seconds
=GRADLE_LOCK_MAX_WAIT_SECONDS
):
50 # The android-lint parameter can be used by gradle tasks to run special
51 # logic when they are run for a lint using
52 # project.hasProperty('android-lint')
56 os
.path
.join(topsrcdir
, "mach"),
66 cmd
= " ".join(shlex
.quote(arg
) for arg
in cmd_args
)
69 # Gradle and mozprocess do not get along well, so we use subprocess
71 proc
= subprocess
.Popen(cmd_args
, cwd
=topsrcdir
)
73 # Leave it to the subprocess to handle Ctrl+C. If it terminates as a result
74 # of Ctrl+C, proc.wait() will return a status code, and, we get out of the
75 # loop. If it doesn't, like e.g. gdb, we continue waiting.
79 except KeyboardInterrupt:
84 except KeyboardInterrupt:
88 return proc
.returncode
91 def gradlew(log
, topsrcdir
=None, topobjdir
=None, tasks
=[], cwd
=None):
92 sys
.path
.insert(0, os
.path
.join(topsrcdir
, "mobile", "android"))
93 from gradle
import gradle_lock
95 with
gradle_lock(topobjdir
, max_wait_seconds
=GRADLE_LOCK_MAX_WAIT_SECONDS
):
96 # The android-lint parameter can be used by gradle tasks to run special
97 # logic when they are run for a lint using
98 # project.hasProperty('android-lint')
99 cmd_args
= ["./gradlew"] + tasks
101 cmd
= " ".join(shlex
.quote(arg
) for arg
in cmd_args
)
104 # Gradle and mozprocess do not get along well, so we use subprocess
106 proc
= subprocess
.Popen(cmd_args
, cwd
=cwd
)
108 # Leave it to the subprocess to handle Ctrl+C. If it terminates as a result
109 # of Ctrl+C, proc.wait() will return a status code, and, we get out of the
110 # loop. If it doesn't, like e.g. gdb, we continue waiting.
111 while status
is None:
114 except KeyboardInterrupt:
119 except KeyboardInterrupt:
123 return proc
.returncode
126 def format(config
, fix
=None, **lintargs
):
127 topsrcdir
= lintargs
["root"]
128 topobjdir
= lintargs
["topobjdir"]
131 tasks
= lintargs
["substs"]["GRADLE_ANDROID_FORMAT_LINT_FIX_TASKS"]
133 tasks
= lintargs
["substs"]["GRADLE_ANDROID_FORMAT_LINT_CHECK_TASKS"]
140 extra_args
=lintargs
.get("extra_args") or [],
144 for path
in lintargs
["substs"]["GRADLE_ANDROID_FORMAT_LINT_FOLDERS"]:
145 folder
= os
.path
.join(
146 topobjdir
, "gradle", "build", path
, "spotless", "spotlessJava"
148 for filename
in glob
.iglob(folder
+ "/**/*.java", recursive
=True):
150 "rule": "spotless-java",
151 "path": os
.path
.join(
152 topsrcdir
, path
, mozpath
.relpath(filename
, folder
)
156 "message": "Formatting error, please run ./mach lint -l android-format --fix",
159 results
.append(result
.from_config(config
, **err
))
160 folder
= os
.path
.join(
161 topobjdir
, "gradle", "build", path
, "spotless", "spotlessKotlin"
163 for filename
in glob
.iglob(folder
+ "/**/*.kt", recursive
=True):
165 "rule": "spotless-kt",
166 "path": os
.path
.join(
167 topsrcdir
, path
, mozpath
.relpath(filename
, folder
)
171 "message": "Formatting error, please run ./mach lint -l android-format --fix",
174 results
.append(result
.from_config(config
, **err
))
176 if len(results
) == 0 and ret
!= 0:
177 # spotless seems to hit unfixed error.
183 "message": "Unexpected error",
186 results
.append(result
.from_config(config
, **err
))
188 # If --fix was passed, we just report the number of files that were changed
190 return {"results": [], "fixed": len(results
)}
194 def fenix_format(config
, fix
=None, **lintargs
):
195 return report_gradlew(
198 os
.path
.join("mobile", "android", "fenix"),
203 def ac_format(config
, fix
=None, **lintargs
):
204 return report_gradlew(
207 os
.path
.join("mobile", "android", "android-components"),
212 def focus_format(config
, fix
=None, **lintargs
):
213 return report_gradlew(
216 os
.path
.join("mobile", "android", "focus-android"),
221 def report_gradlew(config
, fix
, subdir
, **lintargs
):
222 topsrcdir
= lintargs
["root"]
223 topobjdir
= lintargs
["topobjdir"]
226 tasks
= ["ktlintFormat", "detekt"]
228 tasks
= ["ktlint", "detekt"]
236 cwd
=os
.path
.join(topsrcdir
, subdir
),
239 reports
= os
.path
.join(topsrcdir
, subdir
, "build", "reports")
243 for path
in EXCLUSION_FILES
:
244 with
open(os
.path
.join(topsrcdir
, path
), "r") as fh
:
245 for f
in fh
.readlines():
247 excludes
.extend(glob
.glob(f
.strip()))
248 elif f
.startswith(subdir
):
249 excludes
.append(f
.strip())
262 root
= tree
.getroot()
264 for file in root
.findall("file"):
265 name
= file.get("name")
266 if is_excluded_file(topsrcdir
, excludes
, name
):
270 "rule": error
.get("source"),
272 "lineno": int(error
.get("line") or 0),
273 "column": int(error
.get("column") or 0),
274 "message": error
.get("message"),
277 results
.append(result
.from_config(config
, **err
))
278 except FileNotFoundError
:
281 ktlint_file
= "ktlint.json"
283 ktlint_file
= "ktlintFormat.json"
298 if is_excluded_file(topsrcdir
, excludes
, name
):
300 for error
in issue
["errors"]:
302 "rule": error
["rule"],
304 "lineno": error
["line"],
305 "column": error
["column"],
306 "message": error
["message"],
309 results
.append(result
.from_config(config
, **err
))
310 except FileNotFoundError
:
316 def is_excluded_file(topsrcdir
, excludes
, file):
317 for path
in excludes
:
318 if file.startswith(os
.path
.join(topsrcdir
, path
)):
323 def api_lint(config
, **lintargs
):
324 topsrcdir
= lintargs
["root"]
325 topobjdir
= lintargs
["topobjdir"]
331 tasks
=lintargs
["substs"]["GRADLE_ANDROID_API_LINT_TASKS"],
332 extra_args
=lintargs
.get("extra_args") or [],
335 folder
= lintargs
["substs"]["GRADLE_ANDROID_GECKOVIEW_APILINT_FOLDER"]
339 with
open(os
.path
.join(topobjdir
, folder
, "apilint-result.json")) as f
:
340 issues
= json
.load(f
)
342 for rule
in ("compat_failures", "failures"):
343 for r
in issues
[rule
]:
345 "rule": r
["rule"] if rule
== "failures" else "compat_failures",
347 "lineno": int(r
["line"]),
348 "column": int(r
.get("column") or 0),
350 "level": "error" if r
["error"] else "warning",
352 results
.append(result
.from_config(config
, **err
))
354 for r
in issues
["api_changes"]:
356 "rule": "api_changes",
358 "lineno": int(r
["line"]),
359 "column": int(r
.get("column") or 0),
360 "message": "Unexpected api change. Please run ./mach gradle {} for more "
361 "information".format(
362 " ".join(lintargs
["substs"]["GRADLE_ANDROID_API_LINT_TASKS"])
365 results
.append(result
.from_config(config
, **err
))
370 def javadoc(config
, **lintargs
):
371 topsrcdir
= lintargs
["root"]
372 topobjdir
= lintargs
["topobjdir"]
378 tasks
=lintargs
["substs"]["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"],
379 extra_args
=lintargs
.get("extra_args") or [],
382 output_files
= lintargs
["substs"]["GRADLE_ANDROID_GECKOVIEW_DOCS_OUTPUT_FILES"]
386 for output_file
in output_files
:
387 with
open(os
.path
.join(topobjdir
, output_file
)) as f
:
388 # Like: '[{"path":"/absolute/path/to/topsrcdir/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlocking.java","lineno":"462","level":"warning","message":"no @return"}]'. # NOQA: E501
389 issues
= json
.load(f
)
392 # We want warnings to be errors for linting purposes.
393 # TODO: Bug 1316188 - resolve missing javadoc comments
395 "error" if issue
["message"] != ": no comment" else "warning"
397 results
.append(result
.from_config(config
, **issue
))
402 def lint(config
, **lintargs
):
403 topsrcdir
= lintargs
["root"]
404 topobjdir
= lintargs
["topobjdir"]
410 tasks
=lintargs
["substs"]["GRADLE_ANDROID_LINT_TASKS"],
411 extra_args
=lintargs
.get("extra_args") or [],
415 lintargs
["topobjdir"],
416 "gradle/build/mobile/android/geckoview/reports",
417 "lint-results-{}.xml".format(
418 lintargs
["substs"]["GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME"]
421 tree
= ET
.parse(open(path
, "rt"))
422 root
= tree
.getroot()
426 for issue
in root
.findall("issue"):
428 if "third_party" in location
.get("file") or "thirdparty" in location
.get(
433 "level": issue
.get("severity").lower(),
434 "rule": issue
.get("id"),
435 "message": issue
.get("message"),
436 "path": location
.get("file"),
437 "lineno": int(location
.get("line") or 0),
439 results
.append(result
.from_config(config
, **err
))
444 def _parse_checkstyle_output(config
, topsrcdir
=None, report_path
=None):
445 tree
= ET
.parse(open(report_path
, "rt"))
446 root
= tree
.getroot()
448 for file in root
.findall("file"):
449 for error
in file.findall("error"):
450 # Like <error column="42" line="22" message="Name 'mPorts' must match pattern 'xm[A-Z][A-Za-z]*$'." severity="error" source="com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck" />. # NOQA: E501
453 "rule": error
.get("source"),
454 "message": error
.get("message"),
455 "path": file.get("name"),
456 "lineno": int(error
.get("line") or 0),
457 "column": int(error
.get("column") or 0),
459 yield result
.from_config(config
, **err
)
462 def checkstyle(config
, **lintargs
):
463 topsrcdir
= lintargs
["root"]
464 topobjdir
= lintargs
["topobjdir"]
470 tasks
=lintargs
["substs"]["GRADLE_ANDROID_CHECKSTYLE_TASKS"],
471 extra_args
=lintargs
.get("extra_args") or [],
476 for relative_path
in lintargs
["substs"]["GRADLE_ANDROID_CHECKSTYLE_OUTPUT_FILES"]:
477 report_path
= os
.path
.join(lintargs
["topobjdir"], relative_path
)
479 _parse_checkstyle_output(
480 config
, topsrcdir
=lintargs
["root"], report_path
=report_path
487 def _parse_android_test_results(config
, topsrcdir
=None, report_dir
=None):
488 # A brute force way to turn a Java FQN into a path on disk. Assumes Java
489 # and Kotlin sources are in mobile/android for performance and simplicity.
490 sourcepath_finder
= FileFinder(os
.path
.join(topsrcdir
, "mobile", "android"))
492 finder
= FileFinder(report_dir
)
493 reports
= list(finder
.find("TEST-*.xml"))
495 raise RuntimeError("No reports found under {}".format(report_dir
))
497 for report
, _
in reports
:
498 tree
= ET
.parse(open(os
.path
.join(finder
.base
, report
), "rt"))
499 root
= tree
.getroot()
501 class_name
= root
.get(
503 ) # Like 'org.mozilla.gecko.permissions.TestPermissions'.
505 "**/" + class_name
.replace(".", "/") + ".*"
506 ) # Like '**/org/mozilla/gecko/permissions/TestPermissions.*'. # NOQA: E501
508 for testcase
in root
.findall("testcase"):
509 function_name
= testcase
.get("name")
511 # Schema cribbed from http://llg.cubic.org/docs/junit/.
512 for unexpected
in itertools
.chain(
513 testcase
.findall("error"), testcase
.findall("failure")
515 sourcepaths
= list(sourcepath_finder
.find(path
))
518 "No sourcepath found for class {class_name}".format(
519 class_name
=class_name
523 for sourcepath
, _
in sourcepaths
:
525 message
= unexpected
.get("message")
526 # Turn '... at org.mozilla.gecko.permissions.TestPermissions.testMultipleRequestsAreQueuedAndDispatchedSequentially(TestPermissions.java:118)' into 118. # NOQA: E501
527 pattern
= r
"at {class_name}\.{function_name}\(.*:(\d+)\)"
528 pattern
= pattern
.format(
529 class_name
=class_name
, function_name
=function_name
531 match
= re
.search(pattern
, message
)
533 lineno
= int(match
.group(1))
535 msg
= "No source line found for {class_name}.{function_name}".format(
536 class_name
=class_name
, function_name
=function_name
538 raise RuntimeError(msg
)
542 "rule": unexpected
.get("type"),
544 "path": os
.path
.join(
545 topsrcdir
, "mobile", "android", sourcepath
549 yield result
.from_config(config
, **err
)
552 def test(config
, **lintargs
):
553 topsrcdir
= lintargs
["root"]
554 topobjdir
= lintargs
["topobjdir"]
560 tasks
=lintargs
["substs"]["GRADLE_ANDROID_TEST_TASKS"],
561 extra_args
=lintargs
.get("extra_args") or [],
567 # Can't use str.capitalize because it lower cases trailing letters.
568 return (s
[0].upper() + s
[1:]) if s
else ""
570 pairs
= [("geckoview", lintargs
["substs"]["GRADLE_ANDROID_GECKOVIEW_VARIANT_NAME"])]
571 for project
, variant
in pairs
:
572 report_dir
= os
.path
.join(
573 lintargs
["topobjdir"],
574 "gradle/build/mobile/android/{}/test-results/test{}UnitTest".format(
575 project
, capitalize(variant
)
579 _parse_android_test_results(
580 config
, topsrcdir
=lintargs
["root"], report_dir
=report_dir