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/.
5 from __future__
import absolute_import
, print_function
, unicode_literals
8 from datetime
import datetime
, timedelta
10 from operator
import itemgetter
13 from mach
.decorators
import (
20 from mozbuild
.base
import MachCommandBase
, MozbuildObject
23 def _get_busted_bugs(payload
):
26 payload
= dict(payload
)
27 payload
["include_fields"] = "id,summary,last_change_time,resolution"
28 payload
["blocks"] = 1543241
29 response
= requests
.get("https://bugzilla.mozilla.org/rest/bug", payload
)
30 response
.raise_for_status()
31 return response
.json().get("bugs", [])
35 class BustedProvider(MachCommandBase
):
39 description
="Query known bugs in our tooling, and file new ones.",
41 def busted_default(self
):
42 unresolved
= _get_busted_bugs({"resolution": "---"})
43 creation_time
= datetime
.now() - timedelta(days
=15)
44 creation_time
= creation_time
.strftime("%Y-%m-%dT%H-%M-%SZ")
45 resolved
= _get_busted_bugs({"creation_time": creation_time
})
46 resolved
= [bug
for bug
in resolved
if bug
["resolution"]]
48 unresolved
+ resolved
, key
=itemgetter("last_change_time"), reverse
=True
56 if not bug
["resolution"]
57 else "RESOLVED - %s" % bug
["resolution"],
63 print("No known tooling issues found.")
65 @SubCommand("busted", "file", description
="File a bug for busted tooling.")
69 "The specific mach command that is busted (i.e. if you encountered "
70 "an error with `mach build`, run `mach busted file build`). If "
71 "the issue is not connected to any particular mach command, you "
72 "can also run `mach busted file general`."
75 def busted_file(self
, against
):
80 and against
not in self
._mach
_context
.commands
.command_handlers
83 "%s is not a valid value for `against`. `against` must be "
84 "the name of a `mach` command, or else the string "
85 '"general".' % against
89 if against
== "general":
90 product
= "Firefox Build System"
94 import mozpack
.path
as mozpath
96 # Look up the file implementing that command, then cross-refernce
97 # moz.build files to get the product/component.
98 handler
= self
._mach
_context
.commands
.command_handlers
[against
]
99 method
= getattr(handler
.cls
, handler
.method
)
100 sourcefile
= mozpath
.relpath(inspect
.getsourcefile(method
), self
.topsrcdir
)
101 reader
= self
.mozbuild_reader(config_mode
="empty")
103 res
= reader
.files_info([sourcefile
])[sourcefile
]["BUG_COMPONENT"]
104 product
, component
= res
.product
, res
.component
106 # The file might not have a bug set.
107 product
= "Firefox Build System"
108 component
= "General"
111 "https://bugzilla.mozilla.org/enter_bug.cgi?"
112 "product=%s&component=%s&blocked=1543241" % (product
, component
)
114 webbrowser
.open_new_tab(uri
)
117 MACH_PASTEBIN_DURATIONS
= {
118 "onetime": "onetime",
125 EXTENSION_TO_HIGHLIGHTER
= {
127 "Dockerfile": "docker",
129 "applescript": "applescript",
130 "arduino": "arduino",
134 "clojure": "clojure",
136 "coffee": "coffee-script",
137 "console": "console",
152 "handlebars": "handlebars",
153 "haskell": "haskell",
157 "ipy": "ipythonconsole",
158 "ipynb": "ipythonconsole",
167 "lisp": "common-lisp",
168 "lsp": "common-lisp",
180 "postgresql": "postgresql",
192 "typoscript": "typoscript",
201 def guess_highlighter_from_path(path
):
202 """Return a known highlighter from a given path
204 Attempt to select a highlighter by checking the file extension in the mapping
205 of extensions to highlighter. If that fails, attempt to pass the basename of
206 the file. Return `_code` as the default highlighter if that fails.
210 _name
, ext
= os
.path
.splitext(path
)
212 if ext
.startswith("."):
215 if ext
in EXTENSION_TO_HIGHLIGHTER
:
216 return EXTENSION_TO_HIGHLIGHTER
[ext
]
218 basename
= os
.path
.basename(path
)
220 return EXTENSION_TO_HIGHLIGHTER
.get(basename
, "_code")
223 PASTEMO_MAX_CONTENT_LENGTH
= 250 * 1024 * 1024
225 PASTEMO_URL
= "https://paste.mozilla.org/api/"
227 MACH_PASTEBIN_DESCRIPTION
= """
228 Command line interface to paste.mozilla.org.
230 Takes either a filename whose content should be pasted, or reads
231 content from standard input. If a highlighter is specified it will
232 be used, otherwise the file name will be used to determine an
233 appropriate highlighter.
238 class PastebinProvider(MachCommandBase
):
239 @Command("pastebin", category
="misc", description
=MACH_PASTEBIN_DESCRIPTION
)
241 "--list-highlighters",
243 help="List known highlighters and exit",
246 "--highlighter", default
=None, help="Syntax highlighting to use for paste"
251 choices
=sorted(MACH_PASTEBIN_DURATIONS
.keys()),
252 help="Expire paste after given time duration (default: %(default)s)",
257 help="Print extra info such as selected syntax highlighter",
263 help="Path to file for upload to paste.mozilla.org",
265 def pastebin(self
, list_highlighters
, highlighter
, expires
, verbose
, path
):
268 def verbose_print(*args
, **kwargs
):
269 """Print a string if `--verbose` flag is set"""
271 print(*args
, **kwargs
)
273 # Show known highlighters and exit.
274 if list_highlighters
:
275 lexers
= set(EXTENSION_TO_HIGHLIGHTER
.values())
276 print("Available lexers:\n" " - %s" % "\n - ".join(sorted(lexers
)))
279 # Get a correct expiry value.
281 verbose_print("Setting expiry from %s" % expires
)
282 expires
= MACH_PASTEBIN_DURATIONS
[expires
]
283 verbose_print("Using %s as expiry" % expires
)
286 "%s is not a valid duration.\n"
287 "(hint: try one of %s)"
288 % (expires
, ", ".join(MACH_PASTEBIN_DURATIONS
.keys()))
297 # Get content to be pasted.
299 verbose_print("Reading content from %s" % path
)
301 with
open(path
, "r") as f
:
304 print("ERROR. No such file %s" % path
)
307 lexer
= guess_highlighter_from_path(path
)
309 data
["lexer"] = lexer
311 verbose_print("Reading content from stdin")
312 content
= sys
.stdin
.read()
314 # Assert the length of content to be posted does not exceed the maximum.
315 content_length
= len(content
)
316 verbose_print("Checking size of content is okay (%d)" % content_length
)
317 if content_length
> PASTEMO_MAX_CONTENT_LENGTH
:
319 "Paste content is too large (%d, maximum %d)"
320 % (content_length
, PASTEMO_MAX_CONTENT_LENGTH
)
324 data
["content"] = content
326 # Highlight as specified language, overwriting value set from filename.
328 verbose_print("Setting %s as highlighter" % highlighter
)
329 data
["lexer"] = highlighter
332 verbose_print("Sending request to %s" % PASTEMO_URL
)
333 resp
= requests
.post(PASTEMO_URL
, data
=data
)
335 # Error code should always be 400.
336 # Response content will include a helpful error message,
337 # so print it here (for example, if an invalid highlighter is
338 # provided, it will return a list of valid highlighters).
339 if resp
.status_code
>= 400:
340 print("Error code %d: %s" % (resp
.status_code
, resp
.content
))
343 verbose_print("Pasted successfully")
345 response_json
= resp
.json()
347 verbose_print("Paste highlighted as %s" % response_json
["lexer"])
348 print(response_json
["url"])
351 except Exception as e
:
352 print("ERROR. Paste failed.")
359 Helper for loading a tool that is hosted on pypi. The package is expected
360 to expose a `mach_interface` module which has `new_release_on_pypi`,
361 `parser`, and `run` functions.
364 def __init__(self
, module_name
, pypi_name
=None):
365 self
.name
= module_name
366 self
.pypi_name
= pypi_name
or module_name
369 # Lazy loading of the tools mach interface.
370 # Note that only the mach_interface module should be used from this file.
374 return importlib
.import_module("%s.mach_interface" % self
.name
)
378 def create_parser(self
, subcommand
=None):
379 # Create the command line parser.
380 # If the tool is not installed, or not up to date, it will
381 # first be installed.
382 cmd
= MozbuildObject
.from_environment()
383 cmd
.activate_virtualenv()
384 tool
= self
._import
()
386 # The tool is not here at all, install it
387 cmd
.virtualenv_manager
.install_pip_package(self
.pypi_name
)
389 "%s was installed. please re-run your"
390 " command. If you keep getting this message please "
391 " manually run: 'pip install -U %s'." % (self
.pypi_name
, self
.pypi_name
)
394 # Check if there is a new release available
395 release
= tool
.new_release_on_pypi()
398 # there is one, so install it. Note that install_pip_package
399 # does not work here, so just run pip directly.
400 cmd
.virtualenv_manager
._run
_pip
(
401 ["install", "%s==%s" % (self
.pypi_name
, release
)]
404 "%s was updated to version %s. please"
405 " re-run your command." % (self
.pypi_name
, release
)
408 # Tool is up to date, return the parser.
410 return tool
.parser(subcommand
)
413 # exit if we updated or installed mozregression because
414 # we may have already imported mozregression and running it
415 # as this may cause issues.
418 def run(self
, **options
):
419 tool
= self
._import
()
423 def mozregression_create_parser():
424 # Create the mozregression command line parser.
425 # if mozregression is not installed, or not up to date, it will
426 # first be installed.
427 loader
= PypiBasedTool("mozregression")
428 return loader
.create_parser()
432 class MozregressionCommand(MachCommandBase
):
436 description
=("Regression range finder for nightly" " and inbound builds."),
437 parser
=mozregression_create_parser
,
439 def run(self
, **options
):
440 self
.activate_virtualenv()
441 mozregression
= PypiBasedTool("mozregression")
442 mozregression
.run(**options
)
446 class NodeCommands(MachCommandBase
):
450 description
="Run the NodeJS interpreter used for building.",
452 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
453 def node(self
, args
):
454 from mozbuild
.nodeutil
import find_node_executable
456 # Avoid logging the command
457 self
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
459 node_path
, _
= find_node_executable()
461 return self
.run_process(
463 pass_thru
=True, # Allow user to run Node interactively.
464 ensure_exit_code
=False, # Don't throw on non-zero exit code.
470 description
="Run the npm executable from the NodeJS used for building.",
472 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
474 from mozbuild
.nodeutil
import find_npm_executable
476 # Avoid logging the command
477 self
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
479 npm_path
, _
= find_npm_executable()
481 return self
.run_process(
482 [npm_path
, "--scripts-prepend-node-path=auto"] + args
,
483 pass_thru
=True, # Avoid eating npm output/error messages
484 ensure_exit_code
=False, # Don't throw on non-zero exit code.
488 def logspam_create_parser(subcommand
):
489 # Create the logspam command line parser.
490 # if logspam is not installed, or not up to date, it will
491 # first be installed.
492 loader
= PypiBasedTool("logspam", "mozilla-log-spam")
493 return loader
.create_parser(subcommand
)
496 from functools
import partial
500 class LogspamCommand(MachCommandBase
):
504 description
=("Warning categorizer for treeherder test runs."),
509 @SubCommand("logspam", "report", parser
=partial(logspam_create_parser
, "report"))
510 def report(self
, **options
):
511 self
.activate_virtualenv()
512 logspam
= PypiBasedTool("logspam")
513 logspam
.run(command
="report", **options
)
515 @SubCommand("logspam", "bisect", parser
=partial(logspam_create_parser
, "bisect"))
516 def bisect(self
, **options
):
517 self
.activate_virtualenv()
518 logspam
= PypiBasedTool("logspam")
519 logspam
.run(command
="bisect", **options
)
521 @SubCommand("logspam", "file", parser
=partial(logspam_create_parser
, "file"))
522 def create(self
, **options
):
523 self
.activate_virtualenv()
524 logspam
= PypiBasedTool("logspam")
525 logspam
.run(command
="file", **options
)