Skip to content

Commit bef531d

Browse files
author
Alexandre Lissy
committed
Bug 1763188 - Add Snap support using TC builds
1 parent 8075648 commit bef531d

File tree

9 files changed

+359
-17
lines changed

9 files changed

+359
-17
lines changed

mozregression/branches.py

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def create_branches():
7878
):
7979
for alias in aliases:
8080
branches.set_alias(alias, name)
81+
8182
return branches
8283

8384

mozregression/cli.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,21 @@ def create_parser(defaults):
417417
help="Helps to write the configuration file.",
418418
)
419419

420+
parser.add_argument(
421+
"--allow-sudo",
422+
action="store_true",
423+
help=(
424+
"[Snap] Allow the use of sudo for Snap install/remove operations (otherwise,"
425+
" you will be prompted on each)"
426+
),
427+
)
428+
429+
parser.add_argument(
430+
"--disable-snap-connect",
431+
action="store_true",
432+
help="[Snap] Do not automatically perform 'snap connect'",
433+
)
434+
420435
parser.add_argument("--debug", "-d", action="store_true", help="Show the debug output.")
421436

422437
return parser
@@ -589,6 +604,11 @@ def validate(self):
589604
"x86",
590605
"x86_64",
591606
],
607+
"firefox-snap": [
608+
"aarch64", # will be morphed into arm64
609+
"arm", # will be morphed into armhf
610+
"x86_64", # will be morphed into amd64
611+
],
592612
}
593613

594614
user_defined_bits = options.bits is not None
@@ -607,7 +627,6 @@ def validate(self):
607627
self.logger.warning(
608628
"--arch ignored for Firefox for macOS as it uses unified binary."
609629
)
610-
options.arch = None
611630
elif options.arch not in arch_options[options.app]:
612631
raise MozRegressionError(
613632
f"Invalid arch ({options.arch}) specified for app ({options.app}). "
@@ -618,6 +637,21 @@ def validate(self):
618637
f"`--arch` required for specified app ({options.app}). "
619638
f"Please specify one of {', '.join(arch_options[options.app])}."
620639
)
640+
elif options.app == "firefox-snap" and options.allow_sudo is False:
641+
self.logger.warning(
642+
"Bisection on Snap package without --allow-sudo, you will be prompted for"
643+
" credential on each 'snap' command."
644+
)
645+
elif options.allow_sudo is True and options.app != "firefox-snap":
646+
raise MozRegressionError(
647+
f"--allow-sudo specified for app ({options.app}), but only valid for "
648+
f"firefox-snap. Please verify your config."
649+
)
650+
elif options.disable_snap_connect is True and options.app != "firefox-snap":
651+
raise MozRegressionError(
652+
f"--disable-snap-conncet specified for app ({options.app}), but only valid for "
653+
f"firefox-snap. Please verify your config."
654+
)
621655

622656
fetch_config = create_config(
623657
options.app, mozinfo.os, options.bits, mozinfo.processor, options.arch

mozregression/fetch_configs.py

+57
Original file line numberDiff line numberDiff line change
@@ -812,3 +812,60 @@ def build_regex(self):
812812
part = "mac"
813813
psuffix = "-asan" if "asan" in self.build_type else ""
814814
return r"jsshell-%s%s\.zip$" % (part, psuffix)
815+
816+
817+
# TODO: Update with the time when https://phabricator.services.mozilla.com/D246187 lands
818+
TIMESTAMP_SNAP_UPSTREAM = to_utc_timestamp(datetime.datetime(2025, 4, 25, 0, 0, 0))
819+
820+
821+
class FirefoxSnapIntegrationConfigMixin(IntegrationConfigMixin):
822+
def tk_routes(self, push):
823+
for build_type in self.build_types:
824+
yield "gecko.v2.{}{}.revision.{}.firefox.snap-{}-{}".format(
825+
self.integration_branch,
826+
".shippable" if build_type == "shippable" else "",
827+
push.changeset,
828+
self.arch,
829+
"opt" if build_type == "shippable" else build_type,
830+
)
831+
self._inc_used_build()
832+
return
833+
834+
835+
class SnapCommonConfig(CommonConfig):
836+
def should_use_archive(self):
837+
# We only want to use TaskCluster builds
838+
return False
839+
840+
def build_regex(self):
841+
return r"(firefox_.*)\.snap"
842+
843+
844+
@REGISTRY.register("firefox-snap")
845+
class FirefoxSnapConfig(
846+
SnapCommonConfig, FirefoxSnapIntegrationConfigMixin, FirefoxNightlyConfigMixin
847+
):
848+
BUILD_TYPES = ("shippable", "opt", "debug")
849+
BUILD_TYPE_FALLBACKS = {
850+
"shippable": ("opt",),
851+
"opt": ("shippable",),
852+
}
853+
854+
def __init__(self, os, bits, processor, arch):
855+
super(FirefoxSnapConfig, self).__init__(os, bits, processor, arch)
856+
self.set_build_type("shippable")
857+
858+
def available_archs(self):
859+
return [
860+
"aarch64",
861+
"arm",
862+
"x86_64",
863+
]
864+
865+
def set_arch(self, arch):
866+
mapping = {
867+
"aarch64": "arm64",
868+
"arm": "armhf",
869+
"x86_64": "amd64",
870+
}
871+
self.arch = mapping.get(arch, "amd64")

mozregression/launchers.py

+178-3
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44

55
from __future__ import absolute_import, print_function
66

7+
import hashlib
78
import json
89
import os
910
import stat
11+
import subprocess
1012
import sys
1113
import time
1214
import zipfile
1315
from abc import ABCMeta, abstractmethod
1416
from enum import Enum
17+
from shutil import move
1518
from subprocess import STDOUT, CalledProcessError, call, check_output
1619
from threading import Thread
1720

@@ -22,7 +25,7 @@
2225
from mozfile import remove
2326
from mozlog.structured import get_default_logger, get_proxy_logger
2427
from mozprofile import Profile, ThunderbirdProfile
25-
from mozrunner import Runner
28+
from mozrunner import GeckoRuntimeRunner, Runner
2629

2730
from mozregression.class_registry import ClassRegistry
2831
from mozregression.errors import LauncherError, LauncherNotRunnable
@@ -338,11 +341,15 @@ def get_app_info(self):
338341
REGISTRY = ClassRegistry("app_name")
339342

340343

341-
def create_launcher(buildinfo):
344+
def create_launcher(buildinfo, launcher_args=None):
342345
"""
343346
Create and returns an instance launcher for the given buildinfo.
344347
"""
345-
return REGISTRY.get(buildinfo.app_name)(buildinfo.build_file, task_id=buildinfo.task_id)
348+
return REGISTRY.get(buildinfo.app_name)(
349+
buildinfo.build_file,
350+
task_id=buildinfo.task_id,
351+
launcher_args=launcher_args,
352+
)
346353

347354

348355
class FirefoxRegressionProfile(Profile):
@@ -616,3 +623,171 @@ def cleanup(self):
616623
# always remove tempdir
617624
if self.tempdir is not None:
618625
remove(self.tempdir)
626+
627+
628+
# Should this be part of mozrunner ?
629+
class SnapRunner(GeckoRuntimeRunner):
630+
_allow_sudo = False
631+
_snap_pkg = None
632+
633+
def __init__(self, binary, cmdargs, allow_sudo=False, snap_pkg=None, **runner_args):
634+
self._allow_sudo = allow_sudo
635+
self._snap_pkg = snap_pkg
636+
super().__init__(binary, cmdargs, **runner_args)
637+
638+
@property
639+
def command(self):
640+
"""
641+
Rewrite the command for performing the actual execution with
642+
"snap run PKG", keeping everything else
643+
"""
644+
self._command = FirefoxSnapLauncher._get_snap_command(
645+
self._allow_sudo, "run", [self._snap_pkg] + super().command[1:]
646+
)
647+
return self._command
648+
649+
650+
@REGISTRY.register("firefox-snap")
651+
class FirefoxSnapLauncher(MozRunnerLauncher):
652+
profile_class = FirefoxRegressionProfile
653+
instanceKey = None
654+
snap_pkg = None
655+
binary = None
656+
allow_sudo = False
657+
disable_snap_connect = False
658+
runner = None
659+
660+
def __init__(self, dest, **kwargs):
661+
self.allow_sudo = kwargs["launcher_args"]["allow_sudo"]
662+
self.disable_snap_connect = kwargs["launcher_args"]["disable_snap_connect"]
663+
664+
if not self.allow_sudo:
665+
LOG.info(
666+
"Working with snap requires several 'sudo snap' commands. "
667+
"Not allowing the use of sudo will trigger many password confirmation dialog boxes."
668+
)
669+
else:
670+
LOG.info("Usage of sudo enabled, you should be prompted for your password once.")
671+
672+
super().__init__(dest)
673+
674+
def get_snap_command(self, action, extra):
675+
return FirefoxSnapLauncher._get_snap_command(self.allow_sudo, action, extra)
676+
677+
def _get_snap_command(allow_sudo, action, extra):
678+
if action not in ("connect", "install", "run", "refresh", "remove"):
679+
raise LauncherError(f"Snap operation {action} unsupported")
680+
681+
cmd = []
682+
if allow_sudo and action in ("connect", "install", "refresh", "remove"):
683+
cmd += ["sudo"]
684+
685+
cmd += ["snap", action]
686+
cmd += extra
687+
688+
return cmd
689+
690+
def _install(self, dest):
691+
# From https://snapcraft.io/docs/parallel-installs#heading--naming
692+
# - The instance key needs to be manually appended to the snap name,
693+
# and takes the following format: <snap>_<instance-key>
694+
# - The instance key must match the following regular expression:
695+
# ^[a-z0-9]{1,10}$.
696+
self.instanceKey = hashlib.sha1(os.path.basename(dest).encode("utf8")).hexdigest()[0:9]
697+
self.snap_pkg = "firefox_{}".format(self.instanceKey)
698+
self.binary = "/snap/{}/current/usr/lib/firefox/firefox".format(self.snap_pkg)
699+
700+
subprocess.run(
701+
self.get_snap_command(
702+
"install", ["--name", self.snap_pkg, "--dangerous", "{}".format(dest)]
703+
),
704+
check=True,
705+
)
706+
self._fix_connections()
707+
708+
self.binarydir = os.path.dirname(self.binary)
709+
self.appdir = os.path.normpath(os.path.join(self.binarydir, "..", ".."))
710+
711+
LOG.debug(f"snap package: {self.snap_pkg} {self.binary}")
712+
713+
# On Snap updates are already disabled
714+
715+
def _fix_connections(self):
716+
if self.disable_snap_connect:
717+
return
718+
719+
existing = {}
720+
for line in subprocess.getoutput("snap connections {}".format(self.snap_pkg)).splitlines()[
721+
1:
722+
]:
723+
interface, plug, slot, _ = line.split()
724+
existing[plug] = slot
725+
726+
for line in subprocess.getoutput("snap connections firefox").splitlines()[1:]:
727+
interface, plug, slot, _ = line.split()
728+
ex_plug = plug.replace("firefox:", "{}:".format(self.snap_pkg))
729+
ex_slot = slot.replace("firefox:", "{}:".format(self.snap_pkg))
730+
if existing[ex_plug] == "-":
731+
if ex_plug != "-" and ex_slot != "-":
732+
cmd = self.get_snap_command(
733+
"connect", ["{}".format(ex_plug), "{}".format(ex_slot)]
734+
)
735+
LOG.debug(f"snap connect: {cmd}")
736+
subprocess.run(cmd, check=True)
737+
738+
def _create_profile(self, profile=None, addons=(), preferences=None):
739+
"""
740+
Let's create a profile as usual, but rewrite its path to be in Snap's
741+
dir because it looks like MozProfile class will consider a profile=xxx
742+
to be a pre-existing one
743+
"""
744+
real_profile = super()._create_profile(profile, addons, preferences)
745+
snap_profile_dir = os.path.abspath(
746+
os.path.expanduser("~/snap/{}/common/.mozilla/firefox/".format(self.snap_pkg))
747+
)
748+
if not os.path.exists(snap_profile_dir):
749+
os.makedirs(snap_profile_dir)
750+
profile_dir_name = os.path.basename(real_profile.profile)
751+
snap_profile = os.path.join(snap_profile_dir, profile_dir_name)
752+
move(real_profile.profile, snap_profile_dir)
753+
real_profile.profile = snap_profile
754+
return real_profile
755+
756+
def _start(
757+
self,
758+
profile=None,
759+
addons=(),
760+
cmdargs=(),
761+
preferences=None,
762+
adb_profile_dir=None,
763+
allow_sudo=False,
764+
disable_snap_connect=False,
765+
):
766+
profile = self._create_profile(profile=profile, addons=addons, preferences=preferences)
767+
768+
LOG.info("Launching %s [%s]" % (self.binary, self.allow_sudo))
769+
self.runner = SnapRunner(
770+
binary=self.binary,
771+
cmdargs=cmdargs,
772+
profile=profile,
773+
allow_sudo=self.allow_sudo,
774+
snap_pkg=self.snap_pkg,
775+
)
776+
self.runner.start()
777+
778+
def _wait(self):
779+
self.runner.wait()
780+
781+
def _stop(self):
782+
self.runner.stop()
783+
# release the runner since it holds a profile reference
784+
del self.runner
785+
786+
def cleanup(self):
787+
try:
788+
Launcher.cleanup(self)
789+
finally:
790+
subprocess.run(self.get_snap_command("remove", [self.snap_pkg]))
791+
792+
def get_app_info(self):
793+
return safe_get_version(binary=self.binary)

mozregression/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ def test_runner(self):
8989
cmdargs=self.options.cmdargs,
9090
preferences=self.options.preferences,
9191
adb_profile_dir=self.options.adb_profile_dir,
92+
allow_sudo=self.options.allow_sudo,
93+
disable_snap_connect=self.options.disable_snap_connect,
9294
)
9395
)
9496
else:

0 commit comments

Comments
 (0)