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/.
12 from mach
.util
import get_state_dir
13 from mozbuild
.base
import MozbuildObject
14 from mozversioncontrol
import MissingVCSExtension
, get_repository_object
16 from .lando
import push_to_lando_try
17 from .util
.estimates
import duration_summary
18 from .util
.manage_estimates
import (
19 download_task_history_data
,
20 make_trimmed_taskgraph_cache
,
23 GIT_CINNABAR_NOT_FOUND
= """
24 Could not detect `git-cinnabar`.
26 The `mach try` command requires git-cinnabar to be installed when
27 pushing from git. Please install it by running:
32 HG_PUSH_TO_TRY_NOT_FOUND
= """
33 Could not detect `push-to-try`.
35 The `mach try` command requires the push-to-try extension enabled
36 when pushing from hg. Please install it by running:
42 Could not detect version control. Only `hg` or `git` are supported.
45 UNCOMMITTED_CHANGES
= """
46 ERROR please commit changes before continuing
51 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
52 build
= MozbuildObject
.from_environment(cwd
=here
)
53 vcs
= get_repository_object(build
.topsrcdir
)
55 history_path
= os
.path
.join(
56 get_state_dir(specific_to_topsrcdir
=True), "history", "try_task_configs.json"
60 def write_task_config(try_task_config
):
61 config_path
= os
.path
.join(vcs
.path
, "try_task_config.json")
62 with
open(config_path
, "w") as fh
:
63 json
.dump(try_task_config
, fh
, indent
=4, separators
=(",", ": "), sort_keys
=True)
68 def write_task_config_history(msg
, try_task_config
):
69 if not os
.path
.isfile(history_path
):
70 if not os
.path
.isdir(os
.path
.dirname(history_path
)):
71 os
.makedirs(os
.path
.dirname(history_path
))
74 with
open(history_path
) as fh
:
75 history
= fh
.read().strip().splitlines()
77 history
.insert(0, json
.dumps([msg
, try_task_config
]))
78 history
= history
[:MAX_HISTORY
]
79 with
open(history_path
, "w") as fh
:
80 fh
.write("\n".join(history
))
83 def check_working_directory(push
=True):
87 if not vcs
.working_directory_clean():
88 print(UNCOMMITTED_CHANGES
)
92 def generate_try_task_config(method
, labels
, params
=None, routes
=None):
95 # The user has explicitly requested a set of jobs, so run them all
96 # regardless of optimization (unless the selector explicitly sets this to
97 # True). Their dependencies can be optimized though.
98 params
.setdefault("optimize_target_tasks", False)
100 # Remove selected labels from 'existing_tasks' parameter if present
101 if "existing_tasks" in params
:
102 params
["existing_tasks"] = {
104 for label
, tid
in params
["existing_tasks"].items()
105 if label
not in labels
108 try_config
= params
.setdefault("try_task_config", {})
109 try_config
.setdefault("env", {})["TRY_SELECTOR"] = method
111 try_config
["tasks"] = sorted(labels
)
114 try_config
["routes"] = routes
116 try_task_config
= {"version": 2, "parameters": params
}
117 return try_task_config
120 def task_labels_from_try_config(try_task_config
):
121 if try_task_config
["version"] == 2:
122 parameters
= try_task_config
.get("parameters", {})
123 if "try_task_config" in parameters
:
124 return parameters
["try_task_config"]["tasks"]
127 elif try_task_config
["version"] == 1:
128 return try_task_config
.get("tasks", list())
133 def display_push_estimates(try_task_config
):
134 task_labels
= task_labels_from_try_config(try_task_config
)
135 if task_labels
is None:
138 cache_dir
= os
.path
.join(
139 get_state_dir(specific_to_topsrcdir
=True), "cache", "taskgraph"
145 for graph_cache_file
in ["target_task_graph", "full_task_graph"]:
146 graph_cache
= os
.path
.join(cache_dir
, graph_cache_file
)
147 if os
.path
.isfile(graph_cache
):
148 dep_cache
= graph_cache
.replace("task_graph", "task_dependencies")
149 target_file
= graph_cache
.replace("task_graph", "task_set")
155 download_task_history_data(cache_dir
=cache_dir
)
156 make_trimmed_taskgraph_cache(graph_cache
, dep_cache
, target_file
=target_file
)
158 durations
= duration_summary(dep_cache
, task_labels
, cache_dir
)
161 "estimates: Runs {} tasks ({} selected, {} dependencies)".format(
162 durations
["dependency_count"] + durations
["selected_count"],
163 durations
["selected_count"],
164 durations
["dependency_count"],
168 "estimates: Total task duration {}".format(
169 durations
["dependency_duration"] + durations
["selected_duration"]
172 if "percentile" in durations
:
173 percentile
= durations
["percentile"]
175 print("estimates: In the longest {}% of durations".format(100 - percentile
))
177 print("estimates: In the shortest {}% of durations".format(percentile
))
179 "estimates: Should take about {} (Finished around {})".format(
180 durations
["wall_duration_seconds"],
181 durations
["eta_datetime"].strftime("%Y-%m-%d %H:%M"),
189 try_task_config
=None,
193 files_to_change
=None,
194 allow_log_capture
=False,
197 push
= not stage_changes
and not dry_run
198 check_working_directory(push
)
200 if try_task_config
and method
not in ("auto", "empty"):
202 display_push_estimates(try_task_config
)
204 traceback
.print_exc()
205 print("warning: unable to display push estimates")
207 # Format the commit message
208 closed_tree_string
= " ON A CLOSED TREE" if closed_tree
else ""
209 commit_message
= "{}{}\n\nPushed via `mach try {}`".format(
218 if push
and method
not in ("again", "auto", "empty"):
219 write_task_config_history(msg
, try_task_config
)
220 config_path
= write_task_config(try_task_config
)
221 changed_files
.append(config_path
)
223 if (push
or stage_changes
) and files_to_change
:
224 for path
, content
in files_to_change
.items():
225 path
= os
.path
.join(vcs
.path
, path
)
226 with
open(path
, "wb") as fh
:
227 fh
.write(six
.ensure_binary(content
))
228 changed_files
.append(path
)
232 print("Commit message:")
233 print(commit_message
)
235 print("Calculated try_task_config.json:")
236 with
open(config_path
) as fh
:
240 vcs
.add_remove_files(*changed_files
)
244 push_to_lando_try(vcs
, commit_message
)
246 vcs
.push_to_try(commit_message
, allow_log_capture
=allow_log_capture
)
247 except MissingVCSExtension
as e
:
248 if e
.ext
== "push-to-try":
249 print(HG_PUSH_TO_TRY_NOT_FOUND
)
250 elif e
.ext
== "cinnabar":
251 print(GIT_CINNABAR_NOT_FOUND
)
256 if config_path
and os
.path
.isfile(config_path
):
257 os
.remove(config_path
)