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
9 from datetime
import datetime
, timedelta
11 from operator
import itemgetter
14 from mach
.decorators
import (
20 from mozbuild
.base
import 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", [])
37 description
="Query known bugs in our tooling, and file new ones.",
39 def busted_default(command_context
):
40 unresolved
= _get_busted_bugs({"resolution": "---"})
41 creation_time
= datetime
.now() - timedelta(days
=15)
42 creation_time
= creation_time
.strftime("%Y-%m-%dT%H-%M-%SZ")
43 resolved
= _get_busted_bugs({"creation_time": creation_time
})
44 resolved
= [bug
for bug
in resolved
if bug
["resolution"]]
46 unresolved
+ resolved
, key
=itemgetter("last_change_time"), reverse
=True
54 if not bug
["resolution"]
55 else "RESOLVED - %s" % bug
["resolution"],
61 print("No known tooling issues found.")
64 @SubCommand("busted", "file", description
="File a bug for busted tooling.")
68 "The specific mach command that is busted (i.e. if you encountered "
69 "an error with `mach build`, run `mach busted file build`). If "
70 "the issue is not connected to any particular mach command, you "
71 "can also run `mach busted file general`."
74 def busted_file(command_context
, against
):
79 and against
not in command_context
._mach
_context
.commands
.command_handlers
82 "%s is not a valid value for `against`. `against` must be "
83 "the name of a `mach` command, or else the string "
84 '"general".' % against
88 if against
== "general":
89 product
= "Firefox Build System"
93 import mozpack
.path
as mozpath
95 # Look up the file implementing that command, then cross-refernce
96 # moz.build files to get the product/component.
97 handler
= command_context
._mach
_context
.commands
.command_handlers
[against
]
98 sourcefile
= mozpath
.relpath(
99 inspect
.getsourcefile(handler
.func
), command_context
.topsrcdir
101 reader
= command_context
.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.
237 @Command("pastebin", category
="misc", description
=MACH_PASTEBIN_DESCRIPTION
)
239 "--list-highlighters",
241 help="List known highlighters and exit",
244 "--highlighter", default
=None, help="Syntax highlighting to use for paste"
249 choices
=sorted(MACH_PASTEBIN_DURATIONS
.keys()),
250 help="Expire paste after given time duration (default: %(default)s)",
255 help="Print extra info such as selected syntax highlighter",
261 help="Path to file for upload to paste.mozilla.org",
263 def pastebin(command_context
, list_highlighters
, highlighter
, expires
, verbose
, path
):
266 def verbose_print(*args
, **kwargs
):
267 """Print a string if `--verbose` flag is set"""
269 print(*args
, **kwargs
)
271 # Show known highlighters and exit.
272 if list_highlighters
:
273 lexers
= set(EXTENSION_TO_HIGHLIGHTER
.values())
274 print("Available lexers:\n - %s" % "\n - ".join(sorted(lexers
)))
277 # Get a correct expiry value.
279 verbose_print("Setting expiry from %s" % expires
)
280 expires
= MACH_PASTEBIN_DURATIONS
[expires
]
281 verbose_print("Using %s as expiry" % expires
)
284 "%s is not a valid duration.\n"
285 "(hint: try one of %s)"
286 % (expires
, ", ".join(MACH_PASTEBIN_DURATIONS
.keys()))
295 # Get content to be pasted.
297 verbose_print("Reading content from %s" % path
)
299 with
open(path
, "r") as f
:
302 print("ERROR. No such file %s" % path
)
305 lexer
= guess_highlighter_from_path(path
)
307 data
["lexer"] = lexer
309 verbose_print("Reading content from stdin")
310 content
= sys
.stdin
.read()
312 # Assert the length of content to be posted does not exceed the maximum.
313 content_length
= len(content
)
314 verbose_print("Checking size of content is okay (%d)" % content_length
)
315 if content_length
> PASTEMO_MAX_CONTENT_LENGTH
:
317 "Paste content is too large (%d, maximum %d)"
318 % (content_length
, PASTEMO_MAX_CONTENT_LENGTH
)
322 data
["content"] = content
324 # Highlight as specified language, overwriting value set from filename.
326 verbose_print("Setting %s as highlighter" % highlighter
)
327 data
["lexer"] = highlighter
330 verbose_print("Sending request to %s" % PASTEMO_URL
)
331 resp
= requests
.post(PASTEMO_URL
, data
=data
)
333 # Error code should always be 400.
334 # Response content will include a helpful error message,
335 # so print it here (for example, if an invalid highlighter is
336 # provided, it will return a list of valid highlighters).
337 if resp
.status_code
>= 400:
338 print("Error code %d: %s" % (resp
.status_code
, resp
.content
))
341 verbose_print("Pasted successfully")
343 response_json
= resp
.json()
345 verbose_print("Paste highlighted as %s" % response_json
["lexer"])
346 print(response_json
["url"])
349 except Exception as e
:
350 print("ERROR. Paste failed.")
357 Helper for loading a tool that is hosted on pypi. The package is expected
358 to expose a `mach_interface` module which has `new_release_on_pypi`,
359 `parser`, and `run` functions.
362 def __init__(self
, module_name
, pypi_name
=None):
363 self
.name
= module_name
364 self
.pypi_name
= pypi_name
or module_name
367 # Lazy loading of the tools mach interface.
368 # Note that only the mach_interface module should be used from this file.
372 return importlib
.import_module("%s.mach_interface" % self
.name
)
376 def create_parser(self
, subcommand
=None):
377 # Create the command line parser.
378 # If the tool is not installed, or not up to date, it will
379 # first be installed.
380 cmd
= MozbuildObject
.from_environment()
381 cmd
.activate_virtualenv()
382 tool
= self
._import
()
384 # The tool is not here at all, install it
385 cmd
.virtualenv_manager
.install_pip_package(self
.pypi_name
)
387 "%s was installed. please re-run your"
388 " command. If you keep getting this message please "
389 " manually run: 'pip install -U %s'." % (self
.pypi_name
, self
.pypi_name
)
392 # Check if there is a new release available
393 release
= tool
.new_release_on_pypi()
396 # there is one, so install it. Note that install_pip_package
397 # does not work here, so just run pip directly.
398 subprocess
.check_call(
400 cmd
.virtualenv_manager
.python_path
,
404 f
"{self.pypi_name}=={release}",
408 "%s was updated to version %s. please"
409 " re-run your command." % (self
.pypi_name
, release
)
412 # Tool is up to date, return the parser.
414 return tool
.parser(subcommand
)
417 # exit if we updated or installed mozregression because
418 # we may have already imported mozregression and running it
419 # as this may cause issues.
422 def run(self
, **options
):
423 tool
= self
._import
()
427 def mozregression_create_parser():
428 # Create the mozregression command line parser.
429 # if mozregression is not installed, or not up to date, it will
430 # first be installed.
431 loader
= PypiBasedTool("mozregression")
432 return loader
.create_parser()
438 description
=("Regression range finder for nightly and inbound builds."),
439 parser
=mozregression_create_parser
,
441 def run(command_context
, **options
):
442 command_context
.activate_virtualenv()
443 mozregression
= PypiBasedTool("mozregression")
444 mozregression
.run(**options
)
450 description
="Run the NodeJS interpreter used for building.",
452 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
453 def node(command_context
, args
):
454 from mozbuild
.nodeutil
import find_node_executable
456 # Avoid logging the command
457 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
459 node_path
, _
= find_node_executable()
461 return command_context
.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.
471 description
="Run the npm executable from the NodeJS used for building.",
473 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
474 def npm(command_context
, args
):
475 from mozbuild
.nodeutil
import find_npm_executable
477 # Avoid logging the command
478 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
482 # Add node and npm from mozbuild to front of system path
484 # This isn't pretty, but npm currently executes itself with
485 # `#!/usr/bin/env node`, which means it just uses the node in the
486 # current PATH. As a result, stuff gets built wrong and installed
487 # in the wrong places and probably other badness too without this:
488 npm_path
, _
= find_npm_executable()
490 exit(-1, "could not find npm executable")
491 path
= os
.path
.abspath(os
.path
.dirname(npm_path
))
492 os
.environ
["PATH"] = "{}:{}".format(path
, os
.environ
["PATH"])
494 # karma-firefox-launcher needs the path to firefox binary.
495 firefox_bin
= command_context
.get_binary_path(validate_exists
=False)
496 if os
.path
.exists(firefox_bin
):
497 os
.environ
["FIREFOX_BIN"] = firefox_bin
499 return command_context
.run_process(
500 [npm_path
, "--scripts-prepend-node-path=auto"] + args
,
501 pass_thru
=True, # Avoid eating npm output/error messages
502 ensure_exit_code
=False, # Don't throw on non-zero exit code.
506 def logspam_create_parser(subcommand
):
507 # Create the logspam command line parser.
508 # if logspam is not installed, or not up to date, it will
509 # first be installed.
510 loader
= PypiBasedTool("logspam", "mozilla-log-spam")
511 return loader
.create_parser(subcommand
)
514 from functools
import partial
520 description
=("Warning categorizer for treeherder test runs."),
522 def logspam(command_context
):
526 @SubCommand("logspam", "report", parser
=partial(logspam_create_parser
, "report"))
527 def report(command_context
, **options
):
528 command_context
.activate_virtualenv()
529 logspam
= PypiBasedTool("logspam")
530 logspam
.run(command
="report", **options
)
533 @SubCommand("logspam", "bisect", parser
=partial(logspam_create_parser
, "bisect"))
534 def bisect(command_context
, **options
):
535 command_context
.activate_virtualenv()
536 logspam
= PypiBasedTool("logspam")
537 logspam
.run(command
="bisect", **options
)
540 @SubCommand("logspam", "file", parser
=partial(logspam_create_parser
, "file"))
541 def create(command_context
, **options
):
542 command_context
.activate_virtualenv()
543 logspam
= PypiBasedTool("logspam")
544 logspam
.run(command
="file", **options
)