import sys
if sys.version_info[0:2] < (3, 5):
print("run-task requires Python 3.5+")
sys.exit(1)
import argparse
import datetime
import errno
import io
import json
import os
import platform
import re
import shutil
import signal
import socket
import stat
import subprocess
import time
import urllib.error
import urllib.request
from pathlib import Path
from threading import Thread
from typing import Optional
SECRET_BASEURL_TPL = "{}/secrets/v1/secret/{{}}".format(os.environ.get("TASKCLUSTER_PROXY_URL", "http://taskcluster").rstrip('/'))
GITHUB_SSH_FINGERPRINT = (
b"github.com ssh-ed25519 "
b"AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\n"
b"github.com ecdsa-sha2-nistp256 "
b"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB"
b"9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n"
b"github.com ssh-rsa "
b"AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY"
b"4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDP"
b"gVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyR"
b"kQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWO"
b"WRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZ"
b"yaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+"
b"2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n"
)
CACHE_UID_GID_MISMATCH = """
There is a UID/GID mismatch on the cache. This likely means:
a) different tasks are running as a different user/group
b) different Docker images have different UID/GID for the same user/group
Our cache policy is that the UID/GID for ALL tasks must be consistent
for the lifetime of the cache. This eliminates permissions problems due
to file/directory user/group ownership.
To make this error go away, ensure that all Docker images are use
a consistent UID/GID and that all tasks using this cache are running as
the same user/group.
"""
NON_EMPTY_VOLUME = """
error: volume %s is not empty
Our Docker image policy requires volumes to be empty.
The volume was likely populated as part of building the Docker image.
Change the Dockerfile and anything run from it to not create files in
any VOLUME.
A lesser possibility is that you stumbled upon a TaskCluster platform bug
where it fails to use new volumes for tasks.
"""
FETCH_CONTENT_NOT_FOUND = """
error: fetch-content script not found
The script at `taskcluster/scripts/misc/fetch-content` could not be
detected in the current environment.
"""
EXIT_PURGE_CACHE = 72
IS_MACOSX = sys.platform == "darwin"
IS_POSIX = os.name == "posix"
IS_WINDOWS = os.name == "nt"
NULL_REVISION = "0000000000000000000000000000000000000000"
def print_line(prefix, m):
now = (
datetime.datetime.now(tz=datetime.timezone.utc)
.isoformat(timespec="milliseconds")
.encode("utf-8")
)
sys.stdout.buffer.write(b"[%s %s] %s" % (prefix, now, m))
sys.stdout.buffer.flush()
def _call_windows_retry(func, args=(), retry_max=5, retry_delay=0.5):
retry_count = 0
while True:
try:
func(*args)
except OSError as e:
if e.errno not in (errno.EACCES, errno.ENOTEMPTY, errno.ENOENT):
raise
if retry_count == retry_max:
raise
retry_count += 1
print(
'%s() failed for "%s". Reason: %s (%s). Retrying...'
% (func.__name__, args, e.strerror, e.errno)
)
time.sleep(retry_count * retry_delay)
else:
break
def remove(path):
def _update_permissions(path):
if os.path.islink(path):
return
stats = os.stat(path)
if os.path.isfile(path):
mode = stats.st_mode | stat.S_IWUSR
elif os.path.isdir(path):
mode = stats.st_mode | stat.S_IWUSR | stat.S_IXUSR
else:
return
_call_windows_retry(os.chmod, (path, mode))
if not os.path.lexists(path):
print_line(b"remove", b"WARNING: %s does not exists!\n" % path.encode("utf-8"))
return
if (
sys.platform in ("win32", "cygwin")
and len(path) >= 3
and path[1] == ":"
and path[2] in (os.pathsep, os.altsep)
):
path = "\\\\?\\%s" % path
if os.path.isfile(path) or os.path.islink(path):
_update_permissions(path)
_call_windows_retry(os.remove, (path,))
elif os.path.isdir(path):
_update_permissions(path)
for root, dirs, files in os.walk(path):
for entry in dirs + files:
_update_permissions(os.path.join(root, entry))
_call_windows_retry(shutil.rmtree, (path,))
def run_required_command(prefix, args, *, extra_env=None, cwd=None):
res = run_command(prefix, args, extra_env=extra_env, cwd=cwd)
if res:
sys.exit(res)
def retry_required_command(prefix, args, *, extra_env=None, cwd=None, retries=2):
backoff = 1
while True:
res = run_command(prefix, args, extra_env=extra_env, cwd=cwd)
if not res:
return
if not retries:
sys.exit(res)
retries -= 1
backoff *= 2
time.sleep(backoff)
def run_command(prefix, args, *, extra_env=None, cwd=None):
print_line(prefix, b"executing %r\n" % args)
env = dict(os.environ)
env.update(extra_env or {})
p = subprocess.Popen(
args,
bufsize=0,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=sys.stdin.fileno(),
cwd=cwd,
env=env,
)
stdout = io.TextIOWrapper(p.stdout, encoding="latin1")
while True:
data = stdout.readline().encode("latin1")
if data == b"":
break
print_line(prefix, data)
return p.wait()
def get_posix_user_group(user, group):
import grp
import pwd
try:
user_record = pwd.getpwnam(user)
except KeyError:
print("could not find user %s; specify a valid user with --user" % user)
sys.exit(1)
try:
group_record = grp.getgrnam(group)
except KeyError:
print("could not find group %s; specify a valid group with --group" % group)
sys.exit(1)
if user_record.pw_name == "worker" and user_record.pw_uid != 1000:
print("user `worker` must have uid=1000; got %d" % user_record.pw_uid)
sys.exit(1)
if group_record.gr_name == "worker" and group_record.gr_gid != 1000:
print("group `worker` must have gid=1000; got %d" % group_record.gr_gid)
sys.exit(1)
gids = [g.gr_gid for g in grp.getgrall() if group in g.gr_mem]
return user_record, group_record, gids
def write_audit_entry(path, msg):
now = datetime.datetime.utcnow().isoformat().encode("utf-8")
with open(path, "ab") as fh:
fh.write(b"[%sZ %s] %s\n" % (now, os.environb.get(b"TASK_ID", b"UNKNOWN"), msg))
WANTED_DIR_MODE = stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR
def set_dir_permissions(path, uid, gid):
st = os.lstat(path)
if st.st_uid != uid or st.st_gid != gid:
os.chown(path, uid, gid)
if st.st_mode & WANTED_DIR_MODE != WANTED_DIR_MODE:
os.chmod(path, st.st_mode | WANTED_DIR_MODE)
def chown_recursive(path, user, group, uid, gid):
print_line(
b"chown",
b"recursively changing ownership of %s to %s:%s\n"
% (path.encode("utf-8"), user.encode("utf-8"), group.encode("utf-8")),
)
set_dir_permissions(path, uid, gid)
for root, dirs, files in os.walk(path):
for d in dirs:
set_dir_permissions(os.path.join(root, d), uid, gid)
for f in files:
os.lchown(os.path.join(root, f), uid, gid)
def configure_cache_posix(cache, user, group, untrusted_caches, running_as_root):
our_requirements = {
b"version=1",
b"uid=%d" % user.pw_uid,
b"gid=%d" % group.gr_gid,
}
requires_path = os.path.join(cache, ".cacherequires")
audit_path = os.path.join(cache, ".cachelog")
if not os.listdir(cache):
print_line(
b"cache",
b"cache %s is empty; writing requirements: "
b"%s\n" % (cache.encode("utf-8"), b" ".join(sorted(our_requirements))),
)
with open(requires_path, "wb") as fh:
fh.write(b"\n".join(sorted(our_requirements)))
os.chmod(requires_path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
write_audit_entry(
audit_path,
b"created; requirements: %s" % b", ".join(sorted(our_requirements)),
)
set_dir_permissions(cache, user.pw_uid, group.gr_gid)
return
if os.path.exists(requires_path):
with open(requires_path, "rb") as fh:
wanted_requirements = set(fh.read().splitlines())
print_line(
b"cache",
b"cache %s exists; requirements: %s\n"
% (cache.encode("utf-8"), b" ".join(sorted(wanted_requirements))),
)
missing = wanted_requirements - our_requirements
if (
missing
and untrusted_caches
and running_as_root
and all(s.startswith((b"uid=", b"gid=")) for s in missing)
):
print_line(
b"cache",
b"cache %s uid/gid mismatch; this is acceptable "
b"because caches for this task are untrusted; "
b"changing ownership to facilitate cache use\n" % cache.encode("utf-8"),
)
chown_recursive(
cache, user.pw_name, group.gr_name, user.pw_uid, group.gr_gid
)
with open(requires_path, "wb") as fh:
fh.write(b"\n".join(sorted(our_requirements)))
write_audit_entry(
audit_path,
b"chown; requirements: %s" % b", ".join(sorted(our_requirements)),
)
elif missing:
print(
"error: requirements for populated cache %s differ from "
"this task" % cache
)
print(
"cache requirements: %s"
% " ".join(sorted(s.decode("utf-8") for s in wanted_requirements))
)
print(
"our requirements: %s"
% " ".join(sorted(s.decode("utf-8") for s in our_requirements))
)
if any(s.startswith((b"uid=", b"gid=")) for s in missing):
print(CACHE_UID_GID_MISMATCH)
write_audit_entry(
audit_path,
b"requirements mismatch; wanted: %s"
% b", ".join(sorted(our_requirements)),
)
print("")
print("audit log:")
with open(audit_path, "r") as fh:
print(fh.read())
return True
else:
write_audit_entry(audit_path, b"used")
return
print(
"error: cache %s is not empty and is missing a "
".cacherequires file; the cache names for this task are "
"likely mis-configured or TASKCLUSTER_CACHES is not set "
"properly" % cache
)
write_audit_entry(audit_path, b"missing .cacherequires")
return True
def configure_volume_posix(volume, user, group, running_as_root):
volume_files = os.listdir(volume)
if volume_files:
print(NON_EMPTY_VOLUME % volume)
print("entries in root directory: %s" % " ".join(sorted(volume_files)))
sys.exit(1)
if running_as_root:
print_line(
b"volume",
b"changing ownership of volume %s "
b"to %d:%d\n" % (volume.encode("utf-8"), user.pw_uid, group.gr_gid),
)
set_dir_permissions(volume, user.pw_uid, group.gr_gid)
def _clean_git_checkout(destination_path):
print_line(b"vcs", b"cleaning git checkout...\n")
args = [
"git",
"clean",
"-nxdff",
]
print_line(b"vcs", b"executing %r\n" % args)
p = subprocess.Popen(
args,
bufsize=0,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=sys.stdin.fileno(),
cwd=destination_path,
env=os.environ,
)
stdout = io.TextIOWrapper(p.stdout, encoding="latin1")
ret = p.wait()
if ret:
sys.exit(ret)
data = stdout.read()
prefix = "Would remove "
filenames = [
os.path.join(destination_path, line[len(prefix) :])
for line in data.splitlines()
]
print_line(b"vcs", b"removing %r\n" % filenames)
for filename in filenames:
remove(filename)
print_line(b"vcs", b"successfully cleaned git checkout!\n")
def git_checkout(
destination_path: str,
head_repo: str,
base_repo: Optional[str],
base_ref: Optional[str],
base_rev: Optional[str],
ref: Optional[str],
commit: Optional[str],
ssh_key_file: Optional[Path],
ssh_known_hosts_file: Optional[Path],
):
env = {
"GIT_HTTP_LOW_SPEED_LIMIT": "1024",
"GIT_HTTP_LOW_SPEED_TIME": "60",
"PYTHONUNBUFFERED": "1",
}
if ssh_key_file and ssh_known_hosts_file:
if not ssh_key_file.exists():
raise RuntimeError("Can't find specified ssh_key file.")
if not ssh_known_hosts_file.exists():
raise RuntimeError("Can't find specified known_hosts file.")
env["GIT_SSH_COMMAND"] = " ".join(
[
"ssh",
"-oIdentityFile={}".format(ssh_key_file.as_posix()),
"-oStrictHostKeyChecking=yes",
"-oUserKnownHostsFile={}".format(ssh_known_hosts_file.as_posix()),
]
)
elif ssh_key_file or ssh_known_hosts_file:
raise RuntimeError(
"Must specify both ssh_key_file and ssh_known_hosts_file, if either are specified",
)
args = ["git", "config", "--global", "--add", "safe.directory", Path(destination_path).as_posix()]
retry_required_command(b"vcs", args, extra_env=env)
if not os.path.exists(destination_path):
args = [
"git",
"clone",
base_repo if base_repo else head_repo,
destination_path,
]
retry_required_command(b"vcs", args, extra_env=env)
if base_ref:
args = ["git", "fetch", "origin", base_ref]
retry_required_command(b"vcs", args, cwd=destination_path, extra_env=env)
args = ["git", "checkout", base_ref]
retry_required_command(b"vcs", args, cwd=destination_path, extra_env=env)
if base_rev and base_rev != NULL_REVISION:
args = ["git", "fetch", "origin", base_rev]
retry_required_command(b"vcs", args, cwd=destination_path, extra_env=env)
if ref and base_repo == head_repo:
args = [
"git",
"fetch",
"--tags",
"--force",
base_repo,
ref,
]
retry_required_command(b"vcs", args, cwd=destination_path, extra_env=env)
args = [
"git",
"fetch",
"--no-tags",
head_repo,
ref if ref else "+refs/heads/*:refs/remotes/work/*",
]
retry_required_command(b"vcs", args, cwd=destination_path, extra_env=env)
args = [
"git",
"checkout",
"-f",
]
if ref:
args.extend(["-B", ref])
args.append(commit if commit else "FETCH_HEAD")
run_required_command(b"vcs", args, cwd=destination_path)
if os.path.exists(os.path.join(destination_path, ".gitmodules")):
args = [
"git",
"submodule",
"init",
]
run_required_command(b"vcs", args, cwd=destination_path)
args = [
"git",
"submodule",
"update",
"--force", ]
run_required_command(b"vcs", args, cwd=destination_path)
_clean_git_checkout(destination_path)
args = ["git", "rev-parse", "--verify", "HEAD"]
commit_hash = subprocess.check_output(
args, cwd=destination_path, universal_newlines=True
).strip()
assert re.match("^[a-f0-9]{40}$", commit_hash)
if head_repo.startswith("https://github.com"):
if head_repo.endswith("/"):
head_repo = head_repo[:-1]
tinderbox_link = "{}/commit/{}".format(head_repo, commit_hash)
repo_name = head_repo.split("/")[-1]
else:
tinderbox_link = head_repo
repo_name = head_repo
msg = (
"TinderboxPrint:<a href='{link}' "
"title='Built from {name} commit {commit_hash}'>"
"{commit_hash}</a>\n".format(
commit_hash=commit_hash, link=tinderbox_link, name=repo_name
)
)
print_line(b"vcs", msg.encode("utf-8"))
return commit_hash
def fetch_ssh_secret(secret_name):
secret_url = SECRET_BASEURL_TPL.format(secret_name)
try:
print_line(
b"vcs",
b"fetching secret %s from %s\n"
% (secret_name.encode("utf-8"), secret_url.encode("utf-8")),
)
res = urllib.request.urlopen(secret_url, timeout=10)
secret = res.read()
try:
secret = json.loads(secret.decode("utf-8"))
except ValueError:
print_line(b"vcs", b"invalid JSON in secret")
sys.exit(1)
except (urllib.error.URLError, socket.timeout):
print_line(b"vcs", b"Unable to retrieve ssh secret. aborting...")
sys.exit(1)
return secret["secret"]["ssh_privkey"]
def hg_checkout(
destination_path: str,
head_repo: str,
base_repo: Optional[str],
store_path: str,
sparse_profile: Optional[str],
branch: Optional[str],
revision: Optional[str],
):
if IS_MACOSX or IS_POSIX:
hg_bin = "hg"
elif IS_WINDOWS:
hg_bin = r"C:\Program Files\Mercurial\hg.exe"
if not os.path.exists(hg_bin):
print("could not find Mercurial executable: %s" % hg_bin)
sys.exit(1)
else:
raise RuntimeError("Must be running on mac, posix or windows")
args = [
hg_bin,
"robustcheckout",
"--sharebase",
store_path,
"--purge",
]
if base_repo:
args.extend(["--upstream", base_repo])
if sparse_profile:
args.extend(["--sparseprofile", sparse_profile])
args.extend(
[
"--branch" if branch else "--revision",
branch or revision,
head_repo,
destination_path,
]
)
run_required_command(b"vcs", args, extra_env={"PYTHONUNBUFFERED": "1"})
revision = subprocess.check_output(
[hg_bin, "log", "--rev", ".", "--template", "{node}"],
cwd=destination_path,
universal_newlines=True,
)
assert re.match("^[a-f0-9]{40}$", revision)
msg = (
"TinderboxPrint:<a href={head_repo}/rev/{revision} "
"title='Built from {repo_name} revision {revision}'>"
"{revision}</a>\n".format(
revision=revision, head_repo=head_repo, repo_name=head_repo.split("/")[-1]
)
)
print_line(b"vcs", msg.encode("utf-8"))
return revision
def fetch_artifacts():
print_line(b"fetches", b"fetching artifacts\n")
fetch_content = shutil.which("fetch-content")
if not fetch_content or not os.path.isfile(fetch_content):
fetch_content = os.path.join(os.path.dirname(__file__), "fetch-content")
if not os.path.isfile(fetch_content):
print(FETCH_CONTENT_NOT_FOUND)
sys.exit(1)
cmd = [sys.executable, "-u", fetch_content, "task-artifacts"]
print_line(b"fetches", b"executing %r\n" % cmd)
subprocess.run(cmd, check=True, env=os.environ)
print_line(b"fetches", b"finished fetching artifacts\n")
def add_vcs_arguments(parser, project, name):
parser.add_argument(
"--%s-checkout" % project,
help="Directory where %s checkout should be created" % name,
)
parser.add_argument(
"--%s-sparse-profile" % project,
help="Path to sparse profile for %s checkout" % name,
)
def collect_vcs_options(args, project, name):
checkout = getattr(args, "%s_checkout" % project)
sparse_profile = getattr(args, "%s_sparse_profile" % project)
env_prefix = project.upper()
repo_type = os.environ.get("%s_REPOSITORY_TYPE" % env_prefix)
base_repo = os.environ.get("%s_BASE_REPOSITORY" % env_prefix)
base_ref = os.environ.get("%s_BASE_REF" % env_prefix)
base_rev = os.environ.get("%s_BASE_REV" % env_prefix)
head_repo = os.environ.get("%s_HEAD_REPOSITORY" % env_prefix)
revision = os.environ.get("%s_HEAD_REV" % env_prefix)
ref = os.environ.get("%s_HEAD_REF" % env_prefix)
pip_requirements = os.environ.get("%s_PIP_REQUIREMENTS" % env_prefix)
private_key_secret = os.environ.get("%s_SSH_SECRET_NAME" % env_prefix)
store_path = os.environ.get("HG_STORE_PATH")
if checkout:
checkout = os.path.abspath(os.path.expanduser(checkout))
if store_path:
store_path = os.path.abspath(os.path.expanduser(store_path))
if pip_requirements:
pip_requirements = os.path.join(checkout, pip_requirements)
if base_repo == "https://hg.mozilla.org/mozilla-central":
base_repo = "https://hg.mozilla.org/mozilla-unified"
return {
"store-path": store_path,
"project": project,
"name": name,
"env-prefix": env_prefix,
"checkout": checkout,
"sparse-profile": sparse_profile,
"base-repo": base_repo,
"base-ref": base_ref,
"base-rev": base_rev,
"head-repo": head_repo,
"revision": revision,
"ref": ref,
"repo-type": repo_type,
"ssh-secret-name": private_key_secret,
"pip-requirements": pip_requirements,
}
def vcs_checkout_from_args(options):
if not options["checkout"]:
if options["ref"] and not options["revision"]:
print("task should be defined in terms of non-symbolic revision")
sys.exit(1)
return
revision = options["revision"]
ref = options["ref"]
ssh_key_file = None
ssh_known_hosts_file = None
ssh_dir = None
try:
if options.get("ssh-secret-name"):
ssh_dir = Path("~/.ssh-run-task").expanduser()
os.makedirs(ssh_dir, 0o700)
ssh_key_file = ssh_dir.joinpath("private_ssh_key")
ssh_key = fetch_ssh_secret(options["ssh-secret-name"])
ssh_key_file.write_bytes(ssh_key.encode("ascii"))
ssh_key_file.chmod(0o600)
ssh_known_hosts_file = ssh_dir.joinpath("known_hosts")
ssh_known_hosts_file.write_bytes(GITHUB_SSH_FINGERPRINT)
if options["repo-type"] == "git":
if not revision and not ref:
raise RuntimeError(
"Git requires that either a ref, a revision, or both are provided"
)
if not ref:
print("Providing a ref will improve the performance of this checkout")
revision = git_checkout(
options["checkout"],
options["head-repo"],
options["base-repo"],
options["base-ref"],
options["base-rev"],
ref,
revision,
ssh_key_file,
ssh_known_hosts_file,
)
elif options["repo-type"] == "hg":
if not revision and not ref:
raise RuntimeError(
"Hg requires that at least one of a ref or revision " "is provided"
)
revision = hg_checkout(
options["checkout"],
options["head-repo"],
options["base-repo"],
options["store-path"],
options["sparse-profile"],
ref,
revision,
)
else:
raise RuntimeError('Type of VCS must be either "git" or "hg"')
finally:
if ssh_dir:
shutil.rmtree(ssh_dir, ignore_errors=True)
pass
os.environ["%s_HEAD_REV" % options["env-prefix"]] = revision
def install_pip_requirements(repositories):
requirements = [
r["pip-requirements"] for r in repositories if r["pip-requirements"]
]
if not requirements:
return
if shutil.which("uv"):
user_site_dir = subprocess.run([sys.executable, "-msite", "--user-site"], capture_output=True, text=True).stdout.strip()
cmd = ["uv", "pip", "install", "--python", sys.executable, "--target", user_site_dir]
else:
cmd = [sys.executable, "-mpip", "install", "--user", "--break-system-packages"]
if os.environ.get("PIP_DISABLE_REQUIRE_HASHES") != "1":
cmd.append("--require-hashes")
for path in requirements:
cmd.extend(["-r", path])
run_required_command(b"pip-install", cmd)
def _display_python_version():
print_line(
b"setup", b"Python version: %s\n" % platform.python_version().encode("utf-8")
)
def main(args):
task_workdir = os.environ["TASK_WORKDIR"] = os.getcwd()
print_line(
b"setup",
b"run-task started in %s\n" % task_workdir.encode("utf-8"),
)
print_line(
b"setup",
b"Invoked by command: %s\n" % " ".join(args).encode("utf-8"),
)
_display_python_version()
running_as_root = IS_POSIX and os.getuid() == 0
try:
i = args.index("--")
our_args = args[0:i]
task_args = args[i + 1 :]
except ValueError:
our_args = args
task_args = []
parser = argparse.ArgumentParser()
parser.add_argument("--user", default="worker", help="user to run as")
parser.add_argument("--group", default="worker", help="group to run as")
parser.add_argument("--task-cwd", help="directory to run the provided command in")
repositories = os.environ.get("REPOSITORIES")
if repositories:
repositories = json.loads(repositories)
else:
repositories = {"vcs": "repository"}
for repository, name in repositories.items():
add_vcs_arguments(parser, repository, name)
parser.add_argument(
"--fetch-hgfingerprint", action="store_true", help=argparse.SUPPRESS
)
args = parser.parse_args(our_args)
repositories = [
collect_vcs_options(args, repository, name)
for (repository, name) in repositories.items()
]
repositories.sort(key=lambda repo: Path(repo["checkout"] or "/").parts)
uid = gid = gids = user = group = None
if IS_POSIX and running_as_root:
user, group, gids = get_posix_user_group(args.user, args.group)
uid = user.pw_uid
gid = group.gr_gid
if running_as_root and os.path.exists("/dev/kvm"):
st = os.stat("/dev/kvm")
if stat.S_IMODE(st.st_mode) != 0o666:
os.chmod("/dev/kvm", st.st_mode | 0o666)
if "TASKCLUSTER_CACHES" in os.environ:
caches = os.environ["TASKCLUSTER_CACHES"].split(";")
del os.environ["TASKCLUSTER_CACHES"]
else:
caches = []
if "TASKCLUSTER_UNTRUSTED_CACHES" in os.environ:
untrusted_caches = True
del os.environ["TASKCLUSTER_UNTRUSTED_CACHES"]
else:
untrusted_caches = False
for cache in caches:
if not os.path.isdir(cache):
print(
"error: cache %s is not a directory; this should never "
"happen" % cache
)
return 1
purge = configure_cache_posix(
cache, user, group, untrusted_caches, running_as_root
)
if purge:
return EXIT_PURGE_CACHE
if "TASKCLUSTER_VOLUMES" in os.environ:
volumes = os.environ["TASKCLUSTER_VOLUMES"].split(";")
del os.environ["TASKCLUSTER_VOLUMES"]
else:
volumes = []
if volumes and not IS_POSIX:
print("assertion failed: volumes not expected on Windows")
return 1
for volume in volumes:
if volume in caches:
print_line(b"volume", b"volume %s is a cache\n" % volume.encode("utf-8"))
continue
configure_volume_posix(volume, user, group, running_as_root)
all_caches_and_volumes = set(map(os.path.normpath, caches))
all_caches_and_volumes |= set(map(os.path.normpath, volumes))
def path_in_cache_or_volume(path):
path = os.path.normpath(path)
while path:
if path in all_caches_and_volumes:
return True
path, child = os.path.split(path)
if not child:
break
return False
def prepare_checkout_dir(checkout):
if not checkout:
return
if os.path.exists(os.path.join(checkout, ".cacherequires")):
print("error: cannot perform vcs checkout into cache root: %s" % checkout)
sys.exit(1)
if not path_in_cache_or_volume(checkout):
print_line(
b"vcs",
b"WARNING: vcs checkout path (%s) not in cache "
b"or volume; performance will likely suffer\n"
% checkout.encode("utf-8"),
)
try:
os.makedirs(os.path.dirname(checkout))
except OSError as e:
if e.errno != errno.EEXIST:
raise
if running_as_root:
os.chown(os.path.dirname(checkout), uid, gid)
def prepare_hg_store_path():
if "HG_STORE_PATH" not in os.environ:
print("error: HG_STORE_PATH environment variable not set")
sys.exit(1)
store_path = os.environ["HG_STORE_PATH"]
if not path_in_cache_or_volume(store_path):
print_line(
b"vcs",
b"WARNING: HG_STORE_PATH (%s) not in cache or "
b"volume; performance will likely suffer\n"
% store_path.encode("utf-8"),
)
try:
os.makedirs(store_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
if running_as_root:
os.chown(store_path, uid, gid)
repository_paths = [
Path(repo["checkout"]) for repo in repositories if repo["checkout"]
]
for repo in repositories:
if not repo["checkout"]:
continue
parents = Path(repo["checkout"]).parents
if any((path in repository_paths) for path in parents):
continue
prepare_checkout_dir(repo["checkout"])
if any(repo["checkout"] and repo["repo-type"] == "hg" for repo in repositories):
prepare_hg_store_path()
if IS_POSIX and running_as_root:
print_line(
b"setup",
b"running as %s:%s\n"
% (args.user.encode("utf-8"), args.group.encode("utf-8")),
)
os.setgroups(gids)
os.umask(0o22)
os.setresgid(gid, gid, gid)
os.setresuid(uid, uid, uid)
for repo in repositories:
vcs_checkout_from_args(repo)
for k in [
"CARGO_HOME",
"MOZ_FETCHES_DIR",
"PIP_CACHE_DIR",
"UPLOAD_DIR",
"UV_CACHE_DIR",
"npm_config_cache",
] + [
"{}_PATH".format(repository["project"].upper())
for repository in repositories
]:
if k in os.environ:
os.environ[k] = os.path.abspath(os.environ[k])
try:
if "MOZ_FETCHES" in os.environ:
fetch_artifacts()
install_pip_requirements(repositories)
return run_command(b"task", task_args, cwd=args.task_cwd)
finally:
fetches_dir = os.environ.get("MOZ_FETCHES_DIR")
if fetches_dir and os.path.isdir(fetches_dir):
print_line(b"fetches", b"removing %s\n" % fetches_dir.encode("utf-8"))
remove(fetches_dir)
print_line(b"fetches", b"finished\n")
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))