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
7 from appdirs
import user_config_dir
8 from hglib
.error
import CommandError
9 from mach
.decorators
import (
13 from mach
.base
import FailedCommandError
14 from mozrelease
.scriptworker_canary
import get_secret
15 from pathlib
import Path
16 from redo
import retry
26 description
="Run source checks on a localization.",
32 help="TOML or INI file for the project",
36 metavar
="l10n-base-dir",
37 help="Parent directory of localizations",
42 metavar
="locale-code",
43 help="Locale code and top-level directory of each localization",
50 help="""Show less data.
51 Specified once, don't show obsolete entities. Specified twice, also hide
52 missing entities. Specify thrice to exclude warnings and four times to
55 @CommandArgument("-m", "--merge", help="""Use this directory to stage merged files""")
57 "--validate", action
="store_true", help="Run compare-locales against reference"
61 help="""Serialize to JSON. Value is the name of
62 the output file, pass "-" to serialize to stdout and hide the default output.
71 help="Overwrite variables in TOML files",
74 "--full", action
="store_true", help="Compare projects that are disabled"
77 "--return-zero", action
="store_true", help="Return 0 regardless of l10n status"
79 def compare(command_context
, **kwargs
):
80 """Run compare-locales."""
81 from compare_locales
.commands
import CompareLocales
83 class ErrorHelper(object):
84 """Dummy ArgumentParser to marshall compare-locales
85 commandline errors to mach exceptions.
89 raise FailedCommandError(msg
)
91 def exit(self
, message
=None, status
=0):
92 raise FailedCommandError(message
, exit_code
=status
)
94 cmd
= CompareLocales()
95 cmd
.parser
= ErrorHelper()
96 return cmd
.handle(**kwargs
)
99 # https://stackoverflow.com/a/14117511
100 def _positive_int(value
):
103 raise argparse
.ArgumentTypeError(f
"{value} must be a positive integer.")
107 class RetryError(Exception):
111 VCT_PATH
= Path(".").resolve() / "vct"
112 VCT_URL
= "https://hg.mozilla.org/hgcustom/version-control-tools/"
113 FXTREE_PATH
= VCT_PATH
/ "hgext" / "firefoxtree"
114 HGRC_PATH
= Path(user_config_dir("hg")).joinpath("hgrc")
118 "l10n-cross-channel",
120 description
="Create cross-channel content.",
127 default
=Path("en-US"),
128 help="Path to mercurial repository for gecko-strings-quarantine",
134 help="create an outgoing() patch if there are changes",
140 help="Number of times to try (for automation)",
145 help="Taskcluster secret to use to push (for automation)",
149 choices
=("prep", "create", "push", "clean"),
151 # This help block will be poorly formatted until we fix bug 1714239
153 "prep": clone repos and pull heads.
154 "create": create the en-US strings commit an optionally create an
156 "push": push the en-US strings to the quarantine repo.
157 "clean": clean up any sub-repos.
169 """Run l10n cross-channel content generation."""
170 # This can be any path, as long as the name of the directory is en-US.
171 # Not entirely sure where this is a requirement; perhaps in l10n
172 # string manipulation logic?
173 if strings_path
.name
!= "en-US":
174 raise FailedCommandError("strings_path needs to be named `en-US`")
175 command_context
.activate_virtualenv()
176 # XXX pin python requirements
177 command_context
.virtualenv_manager
.install_pip_requirements(
178 Path(os
.path
.dirname(__file__
)) / "requirements.in"
180 strings_path
= strings_path
.resolve() # abspath
182 outgoing_path
= outgoing_path
.resolve() # abspath
183 get_config
= kwargs
.get("get_config", None)
185 with tempfile
.TemporaryDirectory() as ssh_key_dir
:
189 retry_exceptions
=(RetryError
,),
200 except RetryError
as exc
:
201 raise FailedCommandError(exc
) from exc
204 def _do_create_content(
213 from mozxchannel
import CrossChannelCreator
, get_default_config
215 get_config
= get_config
or get_default_config
217 config
= get_config(Path(command_context
.topsrcdir
), strings_path
)
218 ccc
= CrossChannelCreator(config
)
221 ssh_key_secret
= None
224 if "prep" in actions
:
226 if not os
.environ
.get("MOZ_AUTOMATION"):
228 "I don't know how to fetch the ssh secret outside of automation!"
230 ssh_key_secret
= get_secret(ssh_secret
)
231 ssh_key_file
= ssh_key_dir
.joinpath("id_rsa")
232 ssh_key_file
.write_text(ssh_key_secret
["ssh_privkey"])
233 ssh_key_file
.chmod(0o600)
234 # Set up firefoxtree for comm per bug 1659691 comment 22
235 if os
.environ
.get("MOZ_AUTOMATION") and not HGRC_PATH
.exists():
236 _clone_hg_repo(command_context
, VCT_URL
, VCT_PATH
)
239 f
"firefoxtree = {FXTREE_PATH}",
247 f
"ssh = ssh -i {ssh_key_file} -l {ssh_key_secret['user']}",
250 HGRC_PATH
.write_text("\n".join(hgrc_content
))
251 if strings_path
.exists() and _check_outgoing(command_context
, strings_path
):
252 _strip_outgoing(command_context
, strings_path
)
253 # Clone strings + source repos, pull heads
254 for repo_config
in (config
["strings"], *config
["source"].values()):
255 if not repo_config
["path"].exists():
257 command_context
, repo_config
["url"], str(repo_config
["path"])
259 for head
in repo_config
["heads"].keys():
260 command
= ["hg", "--cwd", str(repo_config
["path"]), "pull"]
262 status
= _retry_run_process(
263 command_context
, command
, ensure_exit_code
=False
265 if status
not in (0, 255): # 255 on pull with no changes
266 raise RetryError(f
"Failure on pull: status {status}!")
267 if repo_config
.get("update_on_pull"):
271 str(repo_config
["path"]),
277 status
= _retry_run_process(
278 command_context
, command
, ensure_exit_code
=False
280 if status
not in (0, 255): # 255 on pull with no changes
281 raise RetryError(f
"Failure on update: status {status}!")
285 heads
=repo_config
.get("heads", {}).keys(),
288 _check_hg_repo(command_context
, strings_path
)
289 for repo_config
in config
.get("source", {}).values():
293 heads
=repo_config
.get("heads", {}).keys(),
295 if _check_outgoing(command_context
, strings_path
):
296 raise RetryError(f
"check: Outgoing changes in {strings_path}!")
298 if "create" in actions
:
300 status
= ccc
.create_content()
302 _create_outgoing_patch(command_context
, outgoing_path
, strings_path
)
303 except CommandError
as exc
:
305 raise RetryError(exc
) from exc
306 command_context
.log(logging
.INFO
, "create", {}, "No new strings.")
308 if "push" in actions
:
319 config
["strings"]["push_url"],
324 command_context
.log(logging
.INFO
, "push", {}, "Skipping empty push.")
326 if "clean" in actions
:
327 for repo_config
in config
.get("source", {}).values():
328 if repo_config
.get("post-clobber", False):
329 _nuke_hg_repo(command_context
, str(repo_config
["path"]))
334 def _check_outgoing(command_context
, strings_path
):
335 status
= _retry_run_process(
337 ["hg", "--cwd", str(strings_path
), "out", "-r", "."],
338 ensure_exit_code
=False,
344 raise RetryError(f
"Outgoing check in {strings_path} returned unexpected {status}!")
347 def _strip_outgoing(command_context
, strings_path
):
363 def _create_outgoing_patch(command_context
, path
, strings_path
):
366 if not path
.parent
.exists():
367 os
.makedirs(path
.parent
)
368 with
open(path
, "w") as fh
:
371 fh
.write(f
"{line}\n")
385 line_handler
=writeln
,
389 def _retry_run_process(command_context
, *args
, error_msg
=None, **kwargs
):
391 return command_context
.run_process(*args
, **kwargs
)
392 except Exception as exc
:
393 raise RetryError(error_msg
or str(exc
)) from exc
396 def _check_hg_repo(command_context
, path
, heads
=None):
397 if not (path
.is_dir() and (path
/ ".hg").is_dir()):
398 raise RetryError(f
"{path} is not a Mercurial repository")
403 ["hg", "--cwd", str(path
), "log", "-r", head
],
404 error_msg
=f
"check: {path} has no head {head}!",
408 def _clone_hg_repo(command_context
, url
, path
):
409 _retry_run_process(command_context
, ["hg", "clone", url
, str(path
)])
412 def _nuke_hg_repo(command_context
, path
):
413 _retry_run_process(command_context
, ["rm", "-rf", str(path
)])