1 # -*- coding: utf-8 -*-
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/.
8 from __future__
import absolute_import
, print_function
, unicode_literals
17 from mach
.decorators
import (
24 from mozbuild
.base
import MachCommandBase
27 class ShowTaskGraphSubCommand(SubCommand
):
28 """A SubCommand with TaskGraph-specific arguments"""
30 def __call__(self
, func
):
31 after
= SubCommand
.__call
__(self
, func
)
33 CommandArgument('--root', '-r',
34 help="root of the taskgraph definition relative to topsrcdir"),
35 CommandArgument('--quiet', '-q', action
="store_true",
36 help="suppress all logging output"),
37 CommandArgument('--verbose', '-v', action
="store_true",
38 help="include debug-level logging output"),
39 CommandArgument('--json', '-J', action
="store_const",
40 dest
="format", const
="json",
41 help="Output task graph as a JSON object"),
42 CommandArgument('--labels', '-L', action
="store_const",
43 dest
="format", const
="labels",
44 help="Output the label for each task in the task graph (default)"),
45 CommandArgument('--parameters', '-p', default
="project=mozilla-central",
46 help="parameters file (.yml or .json; see "
47 "`taskcluster/docs/parameters.rst`)`"),
48 CommandArgument('--no-optimize', dest
="optimize", action
="store_false",
50 help="do not remove tasks from the graph that are found in the "
51 "index (a.k.a. optimize the graph)"),
52 CommandArgument('--tasks-regex', '--tasks', default
=None,
53 help="only return tasks with labels matching this regular "
55 CommandArgument('-F', '--fast', dest
='fast', default
=False, action
='store_true',
56 help="enable fast task generation for local debugging.")
65 class MachCommands(MachCommandBase
):
67 @Command('taskgraph', category
="ci",
68 description
="Manipulate TaskCluster task graphs defined in-tree")
70 """The taskgraph subcommands all relate to the generation of task graphs
71 for Gecko continuous integration. A task graph is a set of tasks linked
72 by dependencies: for example, a binary must be built before it is tested,
73 and that build may further depend on various toolchains, libraries, etc.
76 @ShowTaskGraphSubCommand('taskgraph', 'tasks',
77 description
="Show all tasks in the taskgraph")
78 def taskgraph_tasks(self
, **options
):
79 return self
.show_taskgraph('full_task_set', options
)
81 @ShowTaskGraphSubCommand('taskgraph', 'full',
82 description
="Show the full taskgraph")
83 def taskgraph_full(self
, **options
):
84 return self
.show_taskgraph('full_task_graph', options
)
86 @ShowTaskGraphSubCommand('taskgraph', 'target',
87 description
="Show the target task set")
88 def taskgraph_target(self
, **options
):
89 return self
.show_taskgraph('target_task_set', options
)
91 @ShowTaskGraphSubCommand('taskgraph', 'target-graph',
92 description
="Show the target taskgraph")
93 def taskgraph_target_taskgraph(self
, **options
):
94 return self
.show_taskgraph('target_task_graph', options
)
96 @ShowTaskGraphSubCommand('taskgraph', 'optimized',
97 description
="Show the optimized taskgraph")
98 def taskgraph_optimized(self
, **options
):
99 return self
.show_taskgraph('optimized_task_graph', options
)
101 @ShowTaskGraphSubCommand('taskgraph', 'morphed',
102 description
="Show the morphed taskgraph")
103 def taskgraph_morphed(self
, **options
):
104 return self
.show_taskgraph('morphed_task_graph', options
)
106 @SubCommand('taskgraph', 'decision',
107 description
="Run the decision task")
108 @CommandArgument('--root', '-r',
109 help="root of the taskgraph definition relative to topsrcdir")
110 @CommandArgument('--base-repository',
112 help='URL for "base" repository to clone')
113 @CommandArgument('--head-repository',
115 help='URL for "head" repository to fetch revision from')
116 @CommandArgument('--head-ref',
118 help='Reference (this is same as rev usually for hg)')
119 @CommandArgument('--head-rev',
121 help='Commit revision to use from head repository')
122 @CommandArgument('--comm-base-repository',
124 help='URL for "base" comm-* repository to clone')
125 @CommandArgument('--comm-head-repository',
127 help='URL for "head" comm-* repository to fetch revision from')
128 @CommandArgument('--comm-head-ref',
130 help='comm-* Reference (this is same as rev usually for hg)')
131 @CommandArgument('--comm-head-rev',
133 help='Commit revision to use from head comm-* repository')
134 @CommandArgument('--message',
136 help='Commit message to be parsed. Example: "try: -b do -p all -u all"')
137 @CommandArgument('--project',
139 help='Project to use for creating task graph. Example: --project=try')
140 @CommandArgument('--pushlog-id',
144 @CommandArgument('--pushdate',
149 @CommandArgument('--owner',
151 help='email address of who owns this graph')
152 @CommandArgument('--level',
154 help='SCM level of this repository')
155 @CommandArgument('--target-tasks-method',
156 help='method for selecting the target tasks to generate')
157 @CommandArgument('--try-task-config-file',
158 help='path to try task configuration file')
159 def taskgraph_decision(self
, **options
):
160 """Run the decision task: generate a task graph and submit to
161 TaskCluster. This is only meant to be called within decision tasks,
162 and requires a great many arguments. Commands like `mach taskgraph
163 optimized` are better suited to use on the command line, and can take
164 the parameters file generated by a decision task. """
166 import taskgraph
.decision
169 return taskgraph
.decision
.taskgraph_decision(options
)
171 traceback
.print_exc()
174 @SubCommand('taskgraph', 'cron',
175 description
="Run the cron task")
176 @CommandArgument('--base-repository',
179 @CommandArgument('--head-repository',
181 help='URL for "head" repository to fetch')
182 @CommandArgument('--head-ref',
185 @CommandArgument('--project',
187 help='Project to use for creating tasks. Example: --project=mozilla-central')
188 @CommandArgument('--level',
190 help='SCM level of this repository')
191 @CommandArgument('--force-run',
193 help='If given, force this cronjob to run regardless of time, '
195 @CommandArgument('--no-create',
198 help='Do not actually create tasks')
199 @CommandArgument('--root', '-r',
201 help="root of the repository to get cron task definitions from")
202 def taskgraph_cron(self
, **options
):
203 """Run the cron task; this task creates zero or more decision tasks. It is run
204 from the hooks service on a regular basis."""
205 import taskgraph
.cron
208 return taskgraph
.cron
.taskgraph_cron(options
)
210 traceback
.print_exc()
213 @SubCommand('taskgraph', 'action-callback',
214 description
='Run action callback used by action tasks')
215 @CommandArgument('--root', '-r', default
='taskcluster/ci',
216 help="root of the taskgraph definition relative to topsrcdir")
217 def action_callback(self
, **options
):
218 import taskgraph
.actions
222 task_group_id
= os
.environ
.get('ACTION_TASK_GROUP_ID', None)
223 task_id
= json
.loads(os
.environ
.get('ACTION_TASK_ID', 'null'))
224 task
= json
.loads(os
.environ
.get('ACTION_TASK', 'null'))
225 input = json
.loads(os
.environ
.get('ACTION_INPUT', 'null'))
226 callback
= os
.environ
.get('ACTION_CALLBACK', None)
227 parameters
= json
.loads(os
.environ
.get('ACTION_PARAMETERS', '{}'))
228 root
= options
['root']
230 return taskgraph
.actions
.trigger_action_callback(
231 task_group_id
=task_group_id
,
236 parameters
=parameters
,
240 traceback
.print_exc()
243 @SubCommand('taskgraph', 'test-action-callback',
244 description
='Run an action callback in a testing mode')
245 @CommandArgument('--root', '-r', default
='taskcluster/ci',
246 help="root of the taskgraph definition relative to topsrcdir")
247 @CommandArgument('--parameters', '-p', default
='project=mozilla-central',
248 help='parameters file (.yml or .json; see '
249 '`taskcluster/docs/parameters.rst`)`')
250 @CommandArgument('--task-id', default
=None,
251 help='TaskId to which the action applies')
252 @CommandArgument('--task-group-id', default
=None,
253 help='TaskGroupId to which the action applies')
254 @CommandArgument('--input', default
=None,
255 help='Action input (.yml or .json)')
256 @CommandArgument('--task', default
=None,
257 help='Task definition (.yml or .json; if omitted, the task will be'
258 'fetched from the queue)')
259 @CommandArgument('callback', default
=None,
260 help='Action callback name (Python function name)')
261 def test_action_callback(self
, **options
):
262 import taskgraph
.parameters
263 from taskgraph
.util
.taskcluster
import get_task_definition
264 import taskgraph
.actions
267 def load_data(filename
):
268 with
open(filename
) as f
:
269 if filename
.endswith('.yml'):
270 return yaml
.safe_load(f
)
271 elif filename
.endswith('.json'):
274 raise Exception("unknown filename {}".format(filename
))
278 task_id
= options
['task_id']
280 task
= load_data(options
['task'])
282 task
= get_task_definition(task_id
)
287 input = load_data(options
['input'])
291 parameters
= taskgraph
.parameters
.load_parameters_file(options
['parameters'])
294 root
= options
['root']
296 return taskgraph
.actions
.trigger_action_callback(
297 task_group_id
=options
['task_group_id'],
301 callback
=options
['callback'],
302 parameters
=parameters
,
306 traceback
.print_exc()
309 def setup_logging(self
, quiet
=False, verbose
=True):
311 Set up Python logging for all loggers, sending results to stderr (so
312 that command output can be redirected easily) and adding the typical
315 # remove the old terminal handler
316 old
= self
.log_manager
.replace_terminal_handler(None)
318 # re-add it, with level and fh set appropriately
320 level
= logging
.DEBUG
if verbose
else logging
.INFO
321 self
.log_manager
.add_terminal_logging(
322 fh
=sys
.stderr
, level
=level
,
323 write_interval
=old
.formatter
.write_interval
,
324 write_times
=old
.formatter
.write_times
)
326 # all of the taskgraph logging is unstructured logging
327 self
.log_manager
.enable_unstructured()
329 def show_taskgraph(self
, graph_attr
, options
):
330 import taskgraph
.parameters
331 import taskgraph
.target_tasks
332 import taskgraph
.generator
335 taskgraph
.fast
= True
338 self
.setup_logging(quiet
=options
['quiet'], verbose
=options
['verbose'])
339 parameters
= taskgraph
.parameters
.load_parameters_file(options
['parameters'])
342 tgg
= taskgraph
.generator
.TaskGraphGenerator(
343 root_dir
=options
.get('root'),
344 parameters
=parameters
)
346 tg
= getattr(tgg
, graph_attr
)
348 show_method
= getattr(self
, 'show_taskgraph_' + (options
['format'] or 'labels'))
349 tg
= self
.get_filtered_taskgraph(tg
, options
["tasks_regex"])
352 traceback
.print_exc()
355 def show_taskgraph_labels(self
, taskgraph
):
356 for index
in taskgraph
.graph
.visit_postorder():
357 print(taskgraph
.tasks
[index
].label
)
359 def show_taskgraph_json(self
, taskgraph
):
360 print(json
.dumps(taskgraph
.to_json(),
361 sort_keys
=True, indent
=2, separators
=(',', ': ')))
363 def get_filtered_taskgraph(self
, taskgraph
, tasksregex
):
364 from taskgraph
.graph
import Graph
365 from taskgraph
.taskgraph
import TaskGraph
367 This class method filters all the tasks on basis of a regular expression
368 and returns a new TaskGraph object
370 # return original taskgraph if no regular expression is passed
373 named_links_dict
= taskgraph
.graph
.named_links_dict()
375 filterededges
= set()
376 regexprogram
= re
.compile(tasksregex
)
378 for key
in taskgraph
.graph
.visit_postorder():
379 task
= taskgraph
.tasks
[key
]
380 if regexprogram
.match(task
.label
):
381 filteredtasks
[key
] = task
382 for depname
, dep
in named_links_dict
[key
].iteritems():
383 if regexprogram
.match(dep
):
384 filterededges
.add((key
, dep
, depname
))
385 filtered_taskgraph
= TaskGraph(filteredtasks
, Graph(set(filteredtasks
), filterededges
))
386 return filtered_taskgraph
390 class TaskClusterImagesProvider(MachCommandBase
):
391 def _ensure_zstd(self
):
394 # There are two zstd libraries that exist in the wild, ensure we
395 # have the right one.
397 zstd
.ZstdDecompressor
398 except (ImportError, AttributeError):
399 self
._activate
_virtualenv
()
400 self
.virtualenv_manager
.install_pip_package('zstandard==0.8.1')
402 @Command('taskcluster-load-image', category
="ci",
403 description
="Load a pre-built Docker image")
404 @CommandArgument('--task-id',
405 help="Load the image at public/image.tar.zst in this task,"
406 "rather than searching the index")
407 @CommandArgument('-t', '--tag',
408 help="tag that the image should be loaded as. If not "
409 "image will be loaded with tag from the tarball",
411 @CommandArgument('image_name', nargs
='?',
412 help="Load the image of this name based on the current"
413 "contents of the tree (as built for mozilla-central"
414 "or mozilla-inbound)")
415 def load_image(self
, image_name
, task_id
, tag
):
417 from taskgraph
.docker
import load_image_by_name
, load_image_by_task_id
418 if not image_name
and not task_id
:
419 print("Specify either IMAGE-NAME or TASK-ID")
423 ok
= load_image_by_task_id(task_id
, tag
)
425 ok
= load_image_by_name(image_name
, tag
)
429 traceback
.print_exc()
432 @Command('taskcluster-build-image', category
='ci',
433 description
='Build a Docker image')
434 @CommandArgument('image_name',
435 help='Name of the image to build')
436 @CommandArgument('-t', '--tag',
437 help="tag that the image should be built as.",
439 @CommandArgument('--context-only',
440 help="File name the context tarball should be written to."
441 "with this option it will only build the context.tar.",
442 metavar
='context.tar')
443 def build_image(self
, image_name
, tag
, context_only
):
444 from taskgraph
.docker
import build_image
, build_context
446 if context_only
is None:
447 build_image(image_name
, tag
, os
.environ
)
449 build_context(image_name
, context_only
, os
.environ
)
451 traceback
.print_exc()
456 class TaskClusterPartialsData(object):
457 @Command('release-history', category
="ci",
458 description
="Query balrog for release history used by enable partials generation")
459 @CommandArgument('-b', '--branch',
460 help="The gecko project branch used in balrog, such as "
461 "mozilla-central, release, date")
462 @CommandArgument('--product', default
='Firefox',
463 help="The product identifier, such as 'Firefox'")
464 def generate_partials_builds(self
, product
, branch
):
465 from taskgraph
.util
.partials
import populate_release_history
468 release_history
= {'release_history': populate_release_history(product
, branch
)}
469 print(yaml
.safe_dump(release_history
, allow_unicode
=True, default_flow_style
=False))
471 traceback
.print_exc()