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
, command_context
):
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
, command_context
, against
):
80 and against
not in command_context
._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
= command_context
._mach
_context
.commands
.command_handlers
[against
]
99 method
= getattr(handler
.cls
, handler
.method
)
100 sourcefile
= mozpath
.relpath(
101 inspect
.getsourcefile(method
), command_context
.topsrcdir
103 reader
= command_context
.mozbuild_reader(config_mode
="empty")
105 res
= reader
.files_info([sourcefile
])[sourcefile
]["BUG_COMPONENT"]
106 product
, component
= res
.product
, res
.component
108 # The file might not have a bug set.
109 product
= "Firefox Build System"
110 component
= "General"
113 "https://bugzilla.mozilla.org/enter_bug.cgi?"
114 "product=%s&component=%s&blocked=1543241" % (product
, component
)
116 webbrowser
.open_new_tab(uri
)
119 MACH_PASTEBIN_DURATIONS
= {
120 "onetime": "onetime",
127 EXTENSION_TO_HIGHLIGHTER
= {
129 "Dockerfile": "docker",
131 "applescript": "applescript",
132 "arduino": "arduino",
136 "clojure": "clojure",
138 "coffee": "coffee-script",
139 "console": "console",
154 "handlebars": "handlebars",
155 "haskell": "haskell",
159 "ipy": "ipythonconsole",
160 "ipynb": "ipythonconsole",
169 "lisp": "common-lisp",
170 "lsp": "common-lisp",
182 "postgresql": "postgresql",
194 "typoscript": "typoscript",
203 def guess_highlighter_from_path(path
):
204 """Return a known highlighter from a given path
206 Attempt to select a highlighter by checking the file extension in the mapping
207 of extensions to highlighter. If that fails, attempt to pass the basename of
208 the file. Return `_code` as the default highlighter if that fails.
212 _name
, ext
= os
.path
.splitext(path
)
214 if ext
.startswith("."):
217 if ext
in EXTENSION_TO_HIGHLIGHTER
:
218 return EXTENSION_TO_HIGHLIGHTER
[ext
]
220 basename
= os
.path
.basename(path
)
222 return EXTENSION_TO_HIGHLIGHTER
.get(basename
, "_code")
225 PASTEMO_MAX_CONTENT_LENGTH
= 250 * 1024 * 1024
227 PASTEMO_URL
= "https://paste.mozilla.org/api/"
229 MACH_PASTEBIN_DESCRIPTION
= """
230 Command line interface to paste.mozilla.org.
232 Takes either a filename whose content should be pasted, or reads
233 content from standard input. If a highlighter is specified it will
234 be used, otherwise the file name will be used to determine an
235 appropriate highlighter.
240 class PastebinProvider(MachCommandBase
):
241 @Command("pastebin", category
="misc", description
=MACH_PASTEBIN_DESCRIPTION
)
243 "--list-highlighters",
245 help="List known highlighters and exit",
248 "--highlighter", default
=None, help="Syntax highlighting to use for paste"
253 choices
=sorted(MACH_PASTEBIN_DURATIONS
.keys()),
254 help="Expire paste after given time duration (default: %(default)s)",
259 help="Print extra info such as selected syntax highlighter",
265 help="Path to file for upload to paste.mozilla.org",
268 self
, command_context
, list_highlighters
, highlighter
, expires
, verbose
, path
272 def verbose_print(*args
, **kwargs
):
273 """Print a string if `--verbose` flag is set"""
275 print(*args
, **kwargs
)
277 # Show known highlighters and exit.
278 if list_highlighters
:
279 lexers
= set(EXTENSION_TO_HIGHLIGHTER
.values())
280 print("Available lexers:\n" " - %s" % "\n - ".join(sorted(lexers
)))
283 # Get a correct expiry value.
285 verbose_print("Setting expiry from %s" % expires
)
286 expires
= MACH_PASTEBIN_DURATIONS
[expires
]
287 verbose_print("Using %s as expiry" % expires
)
290 "%s is not a valid duration.\n"
291 "(hint: try one of %s)"
292 % (expires
, ", ".join(MACH_PASTEBIN_DURATIONS
.keys()))
301 # Get content to be pasted.
303 verbose_print("Reading content from %s" % path
)
305 with
open(path
, "r") as f
:
308 print("ERROR. No such file %s" % path
)
311 lexer
= guess_highlighter_from_path(path
)
313 data
["lexer"] = lexer
315 verbose_print("Reading content from stdin")
316 content
= sys
.stdin
.read()
318 # Assert the length of content to be posted does not exceed the maximum.
319 content_length
= len(content
)
320 verbose_print("Checking size of content is okay (%d)" % content_length
)
321 if content_length
> PASTEMO_MAX_CONTENT_LENGTH
:
323 "Paste content is too large (%d, maximum %d)"
324 % (content_length
, PASTEMO_MAX_CONTENT_LENGTH
)
328 data
["content"] = content
330 # Highlight as specified language, overwriting value set from filename.
332 verbose_print("Setting %s as highlighter" % highlighter
)
333 data
["lexer"] = highlighter
336 verbose_print("Sending request to %s" % PASTEMO_URL
)
337 resp
= requests
.post(PASTEMO_URL
, data
=data
)
339 # Error code should always be 400.
340 # Response content will include a helpful error message,
341 # so print it here (for example, if an invalid highlighter is
342 # provided, it will return a list of valid highlighters).
343 if resp
.status_code
>= 400:
344 print("Error code %d: %s" % (resp
.status_code
, resp
.content
))
347 verbose_print("Pasted successfully")
349 response_json
= resp
.json()
351 verbose_print("Paste highlighted as %s" % response_json
["lexer"])
352 print(response_json
["url"])
355 except Exception as e
:
356 print("ERROR. Paste failed.")
363 Helper for loading a tool that is hosted on pypi. The package is expected
364 to expose a `mach_interface` module which has `new_release_on_pypi`,
365 `parser`, and `run` functions.
368 def __init__(self
, module_name
, pypi_name
=None):
369 self
.name
= module_name
370 self
.pypi_name
= pypi_name
or module_name
373 # Lazy loading of the tools mach interface.
374 # Note that only the mach_interface module should be used from this file.
378 return importlib
.import_module("%s.mach_interface" % self
.name
)
382 def create_parser(self
, subcommand
=None):
383 # Create the command line parser.
384 # If the tool is not installed, or not up to date, it will
385 # first be installed.
386 cmd
= MozbuildObject
.from_environment()
387 cmd
.activate_virtualenv()
388 tool
= self
._import
()
390 # The tool is not here at all, install it
391 cmd
.virtualenv_manager
.install_pip_package(self
.pypi_name
)
393 "%s was installed. please re-run your"
394 " command. If you keep getting this message please "
395 " manually run: 'pip install -U %s'." % (self
.pypi_name
, self
.pypi_name
)
398 # Check if there is a new release available
399 release
= tool
.new_release_on_pypi()
402 # there is one, so install it. Note that install_pip_package
403 # does not work here, so just run pip directly.
404 cmd
.virtualenv_manager
._run
_pip
(
405 ["install", "%s==%s" % (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()
436 class MozregressionCommand(MachCommandBase
):
440 description
=("Regression range finder for nightly" " and inbound builds."),
441 parser
=mozregression_create_parser
,
443 def run(self
, command_context
, **options
):
444 command_context
.activate_virtualenv()
445 mozregression
= PypiBasedTool("mozregression")
446 mozregression
.run(**options
)
450 class NodeCommands(MachCommandBase
):
454 description
="Run the NodeJS interpreter used for building.",
456 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
457 def node(self
, command_context
, args
):
458 from mozbuild
.nodeutil
import find_node_executable
460 # Avoid logging the command
461 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
463 node_path
, _
= find_node_executable()
465 return command_context
.run_process(
467 pass_thru
=True, # Allow user to run Node interactively.
468 ensure_exit_code
=False, # Don't throw on non-zero exit code.
474 description
="Run the npm executable from the NodeJS used for building.",
476 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
477 def npm(self
, command_context
, args
):
478 from mozbuild
.nodeutil
import find_npm_executable
480 # Avoid logging the command
481 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
485 # Add node and npm from mozbuild to front of system path
487 # This isn't pretty, but npm currently executes itself with
488 # `#!/usr/bin/env node`, which means it just uses the node in the
489 # current PATH. As a result, stuff gets built wrong and installed
490 # in the wrong places and probably other badness too without this:
491 npm_path
, _
= find_npm_executable()
493 exit(-1, "could not find npm executable")
494 path
= os
.path
.abspath(os
.path
.dirname(npm_path
))
495 os
.environ
["PATH"] = "{}:{}".format(path
, os
.environ
["PATH"])
497 return command_context
.run_process(
498 [npm_path
, "--scripts-prepend-node-path=auto"] + args
,
499 pass_thru
=True, # Avoid eating npm output/error messages
500 ensure_exit_code
=False, # Don't throw on non-zero exit code.
504 def logspam_create_parser(subcommand
):
505 # Create the logspam command line parser.
506 # if logspam is not installed, or not up to date, it will
507 # first be installed.
508 loader
= PypiBasedTool("logspam", "mozilla-log-spam")
509 return loader
.create_parser(subcommand
)
512 from functools
import partial
516 class LogspamCommand(MachCommandBase
):
520 description
=("Warning categorizer for treeherder test runs."),
522 def logspam(self
, command_context
):
525 @SubCommand("logspam", "report", parser
=partial(logspam_create_parser
, "report"))
526 def report(self
, command_context
, **options
):
527 command_context
.activate_virtualenv()
528 logspam
= PypiBasedTool("logspam")
529 logspam
.run(command
="report", **options
)
531 @SubCommand("logspam", "bisect", parser
=partial(logspam_create_parser
, "bisect"))
532 def bisect(self
, command_context
, **options
):
533 command_context
.activate_virtualenv()
534 logspam
= PypiBasedTool("logspam")
535 logspam
.run(command
="bisect", **options
)
537 @SubCommand("logspam", "file", parser
=partial(logspam_create_parser
, "file"))
538 def create(self
, command_context
, **options
):
539 command_context
.activate_virtualenv()
540 logspam
= PypiBasedTool("logspam")
541 logspam
.run(command
="file", **options
)