import json import logging import os import re import stat import sys import tarfile import urllib.parse import urllib.request import zipfile from optparse import OptionParser from pathlib import Path from shutil import copy, copytree, move, rmtree, which from subprocess import Popen
# Set default values.
PARTNERS_DIR = Path("..") / ".." / "workspace" / "partners" # No platform in this path because script only supports repacking a single platform at once
DEFAULT_OUTPUT_DIR = "%(partner)s/%(partner_distro)s/%(locale)s"
TASKCLUSTER_ARTIFACTS = (
os.environ.get("TASKCLUSTER_ROOT_URL", "https://firefox-ci-tc.services.mozilla.com")
+ "/api/queue/v1/task/{taskId}/artifacts"
)
UPSTREAM_ENUS_PATH = "public/build/{filename}"
UPSTREAM_L10N_PATH = "public/build/{locale}/{filename}"
class StrictFancyURLopener(urllib.request.FancyURLopener): """Unlike FancyURLopener this class raises exceptions for generic HTTP
errors, like 404, 500. It reuses URLopener.http_error_default redefined in
FancyURLopener"""
def rmdirRecursive(directory: Path): """
This is similar to a call of shutil.rmtree(), except that it
should work better on Windows since it will more aggressively
attempt to remove files marked as"read-only". """
def rmdir_including_read_only(func, path: str, exc_info): """
Source: https://stackoverflow.com/a/4829285
path contains the path of the file that couldn't be removed.
Let's just assume that it's read-only and unlink it. """
path = Path(path)
def isValidPlatform(platform: str): return (
isLinux64(platform) or isLinux32(platform) or isMac(platform) or isWin64(platform) or isWin64Aarch64(platform) or isWin32(platform)
)
def parseRepackConfig(file: Path, platform: str): """Did you hear about this cool file format called yaml ? json ? Yeah, me neither"""
config = {}
config["platforms"] = [] for line in file.open():
line = line.rstrip("\n") # Ignore empty lines if line.strip() == "": continue # Ignore comments if line.startswith("#"): continue
[key, value] = line.split("=", 2)
value = value.strip('"') # strings that don't need special handling if key in ("dist_id", "replacement_setup_exe"):
config[key] = value continue # booleans that don't need special handling if key in ("migrationWizardDisabled", "oem", "repack_stub_installer"): if value.lower() == "true":
config[key] = True continue # special cases if key == "locales":
config["locales"] = value.split(" ") continue if key.startswith("locale."):
config[key] = value continue if key == "deb_section":
config["deb_section"] = re.sub("/", r"\/", value) continue if isValidPlatform(key):
ftp_platform = getFtpPlatform(key) if ftp_platform == getFtpPlatform(platform) and value.lower() == "true":
config["platforms"].append(ftp_platform) continue
# this only works for one locale because setup.exe is localised if config.get("replacement_setup_exe") and len(config.get("locales", [])) > 1:
log.error( "Error: replacement_setup_exe is only supported for one locale, got %s"
% config["locales"]
)
sys.exit(1) # also only works for one platform because setup.exe is platform-specific
if config["platforms"]: return config
def getFtpPlatform(platform: str): """Returns the platform in the format used in building package names.
Note: we rely on this code being idempotent
i.e. getFtpPlatform(getFtpPlatform(foo)) should work """ if isLinux64(platform): return"linux-x86_64" if isLinux(platform): return"linux-i686" if isMac(platform): return"mac" if isWin64Aarch64(platform): return"win64-aarch64" if isWin64(platform): return"win64" if isWin32(platform): return"win32"
def getFileExtension(platform: str): """The extension for the output file, which may be passed to the internal-signing task""" if isLinux(platform): return"tar.xz" elif isMac(platform): return"tar.gz" elif isWin(platform): return"zip"
def getFilename(platform: str): """Returns the filename to be repacked for the platform""" return f"target.{getFileExtension(platform)}"
def getAllFilenames(platform: str, repack_stub_installer): """Returns the full list of filenames we want to downlaod for each platform"""
file_names = [getFilename(platform)] if isWin(platform): # we want to copy forward setup.exe from upstream tasks to make it easier to repackage # windows installers later
file_names.append("setup.exe") # Same for the stub installer with setup-stub.exe, but only in win32 repack jobs if isWin32(platform) and repack_stub_installer:
file_names.append("setup-stub.exe") return tuple(file_names)
def getTaskArtifacts(taskId): try:
retrieveFile(
TASKCLUSTER_ARTIFACTS.format(taskId=taskId), Path("tc_artifacts.json")
)
tc_index = json.load(open("tc_artifacts.json")) return tc_index["artifacts"] except (ValueError, KeyError):
log.error("Failed to get task artifacts from TaskCluster") raise
artifact_ids = {} for taskId in upstream_tasks: for artifact in getTaskArtifacts(taskId):
name = artifact["name"] ifnot name.endswith(useful_artifacts): continue if name in artifact_ids:
log.error( "Duplicated artifact %s processing tasks %s & %s",
name,
taskId,
artifacts[name],
)
sys.exit(1) else:
artifact_ids[name] = taskId
log.debug( "Found artifacts: %s" % json.dumps(artifact_ids, indent=4, sort_keys=True)
) return artifact_ids
def getArtifactNames(platform: str, locale, repack_stub_installer):
file_names = getAllFilenames(platform, repack_stub_installer) if locale == "en-US":
names = [UPSTREAM_ENUS_PATH.format(filename=f) for f in file_names] else:
names = [
UPSTREAM_L10N_PATH.format(locale=locale, filename=f) for f in file_names
] return names
def getBouncerProduct(partner, partner_distro): if"RELEASE_TYPE"notin os.environ:
log.fatal("RELEASE_TYPE must be set in the environment")
sys.exit(1)
release_type = os.environ["RELEASE_TYPE"] # For X.0 releases we get 'release-rc' but the alias should use 'release' if release_type == "release-rc":
release_type = "release" return BOUNCER_PRODUCT_TEMPLATE.format(
release_type=release_type,
partner=partner,
partner_distro=partner_distro,
)
def createOverrideIni(self, partner_path: Path): """If this is a partner specific locale (like en-HK), set the
distribution.ini to use that locale, not the default locale. """ if self.locale != self.source_locale:
file_path = partner_path / "distribution" / "distribution.ini" with file_path.open(file_path.is_file() and"a"or"w") as open_file:
open_file.write("[Locale]\n")
open_file.write("locale=" + self.locale + "\n")
""" Some partners need to override the migration wizard. This is done
by adding an override.ini file to the base install dir. """ # modify distribution.ini if 44 or later and we have migrationWizardDisabled if int(options.version.split(".")[0]) >= 44:
file_path = partner_path / "distribution" / "distribution.ini" with file_path.open() as open_file:
ini = open_file.read()
if ini.find("EnableProfileMigrator") >= 0: return else:
browser_dir = partner_path / "browser" ifnot browser_dir.exists():
browser_dir.mkdir(mode=0o755, exist_ok=True, parents=True)
file_path = browser_dir / "override.ini" if"migrationWizardDisabled"in self.repack_info:
log.info("Adding EnableProfileMigrator to %r" % (file_path,)) with file_path.open(file_path.is_file() and"a"or"w") as open_file:
open_file.write("[XRE]\n")
open_file.write("EnableProfileMigrator=0\n")
def copyFiles(self, platform_dir: Path):
log.info(f"Copying files into {platform_dir}") # Check whether we've already copied files over for this partner. ifnot platform_dir.exists():
platform_dir.mkdir(mode=0o755, exist_ok=True, parents=True) for i in ["distribution", "extensions"]:
full_path = self.full_partner_path / i if full_path.exists():
copytree(str(full_path), str(platform_dir / i))
self.createOverrideIni(platform_dir)
def getAppName(self): # Cope with Firefox.app vs Firefox Nightly.app by returning the first root object/folder found
t = tarfile.open(self.build.rsplit(".", 1)[0]) for name in t.getnames():
root_object = name.split("/")[0] if root_object.endswith(".app"):
log.info(f"Found app name in tarball: {root_object}") return root_object
log.error(
f"Error: Unable to determine app name from tarball: {self.build} - Expected .app in root"
)
sys.exit(1)
# we generate the stub installer during the win32 build, so repack it on win32 too if isWin32(options.platform) and self.repack_info.get("repack_stub_installer"):
log.info("Creating target-stub.zip to hold custom urls")
dest = str(self.final_build).replace("target.zip", "target-stub.zip")
z = zipfile.ZipFile(dest, "w") # load the partner.ini template and interpolate %LOCALE% to the actual locale with (self.full_partner_path / "stub" / "partner.ini").open() as open_file:
partner_ini_template = open_file.readlines()
partner_ini = "" for l in partner_ini_template:
l = l.replace("%LOCALE%", self.locale)
l = l.replace("%BOUNCER_PRODUCT%", self.repack_info["bouncer_product"])
partner_ini += l
z.writestr("partner.ini", partner_ini) # we need an empty firefox directory to use the repackage code
d = zipfile.ZipInfo("firefox/") # https://stackoverflow.com/a/6297838, zip's representation of drwxr-xr-x permissions # is 040755 << 16L, bitwise OR with 0x10 for the MS-DOS directory flag
d.external_attr = 1106051088
z.writestr(d, "")
z.close()
# we generate the stub installer in the win32 build, so repack it on win32 too if isWin32(options.platform) and self.repack_info.get("repack_stub_installer"):
log.info( "Copying vanilla setup-stub.exe forward for stub installer creation"
)
setup_dest = Path(
str(self.final_build).replace("target.zip", "setup-stub.exe")
)
setup_source = str(self.full_build_path).replace( "target.zip", "setup-stub.exe"
)
copy(setup_source, str(setup_dest))
setup_dest.chmod(self.file_mode)
parser = OptionParser(usage="usage: %prog [options]")
parser.add_option( "-d", "--partners-dir",
dest="partners_dir",
default=str(PARTNERS_DIR),
help="Specify the directory where the partner config files are found",
)
parser.add_option( "-p", "--partner",
dest="partner",
help="Repack for a single partner, specified by name",
)
parser.add_option( "-v", "--version", dest="version", help="Set the version number for repacking"
)
parser.add_option( "-n", "--build-number",
dest="build_number",
default=1,
help="Set the build number for repacking",
)
parser.add_option("--platform", dest="platform", help="Set the platform to repack")
parser.add_option( "--include-oem",
action="store_true",
dest="include_oem",
default=False,
help="Process partners marked as OEM (these are usually one-offs)",
)
parser.add_option( "-q", "--quiet",
action="store_true",
dest="quiet",
default=False,
help="Suppress standard output from the packaging tools",
)
parser.add_option( "--taskid",
action="append",
dest="upstream_tasks",
help="Specify taskIds for upstream artifacts, using 'internal sign' tasks. Multiples " "expected, e.g. --taskid foo --taskid bar. Alternatively, use a space-separated list " "stored in UPSTREAM_TASKIDS in the environment.",
)
parser.add_option( "-l", "--limit-locale",
action="append",
dest="limit_locales",
default=[],
)
options.partners_dir = Path(options.partners_dir.rstrip("/")) ifnot options.partners_dir.is_dir():
log.error(f"Error: partners dir {options.partners_dir} is not a directory.")
error = True
ifnot options.version:
log.error("Error: you must specify a version number.")
error = True
upstream_tasks = options.upstream_tasks or os.getenv("UPSTREAM_TASKIDS") ifnot upstream_tasks:
log.error( "upstream tasks should be defined using --taskid args or " "UPSTREAM_TASKIDS in env."
)
error = True
for tool in ("tar", "bunzip2", "bzip2", "gunzip", "gzip", "zip"): ifnot which(tool):
log.error(f"Error: couldn't find the {tool} executable in PATH.")
error = True
if error:
sys.exit(1)
base_workdir = Path.cwd()
# Look up the artifacts available on our upstreams, but only if we need to
artifact_ids = {}
# Local directories for builds
script_directory = Path.cwd()
original_builds_dir = (
script_directory
/ "original_builds"
/ options.version
/ f"build{options.build_number}"
)
repack_version = f"{options.version}-{options.build_number}" if os.getenv("MOZ_AUTOMATION"): # running in production
repacked_builds_dir = Path("/builds/worker/artifacts") else: # local development
repacked_builds_dir = script_directory / "artifacts"
original_builds_dir.mkdir(mode=0o755, exist_ok=True, parents=True)
repacked_builds_dir.mkdir(mode=0o755, exist_ok=True, parents=True)
printSeparator()
# For each partner in the partners dir # Read/check the config file # Download required builds (if not already on disk) # Perform repacks
# walk the partner dirs, find valid repack.cfg configs, and load them
partner_dirs = []
need_stub_installers = False for root, _, all_files in os.walk(options.partners_dir):
root = root.lstrip("/")
partner = root[len(str(options.partners_dir)) + 1 :].split("/")[0]
partner_distro = os.path.split(root)[-1] if options.partner: if (
options.partner != partner and options.partner != partner_distro[: len(options.partner)]
): continue
for file in all_files: if file == "repack.cfg":
log.debug( "Found partner config: {} ['{}'] {}".format(
root, "', '".join(_), file
)
)
root = Path(root)
repack_cfg = root / file
repack_info = parseRepackConfig(repack_cfg, options.platform) ifnot repack_info:
log.debug( "no repack_info for platform %s in %s, skipping"
% (options.platform, repack_cfg)
) continue if repack_info.get("repack_stub_installer"):
need_stub_installers = True
repack_info["bouncer_product"] = getBouncerProduct(
partner, partner_distro
)
partner_dirs.append((partner, partner_distro, root, repack_info))
log.info("Retrieving artifact lists from upstream tasks")
artifact_ids = getUpstreamArtifacts(upstream_tasks, need_stub_installers) ifnot artifact_ids:
log.fatal("No upstream artifacts were found")
sys.exit(1)
for partner, partner_distro, full_partner_dir, repack_info in partner_dirs:
log.info( "Starting repack process for partner: %s/%s" % (partner, partner_distro)
) if"oem"in repack_info and options.include_oem isFalse:
log.info( "Skipping partner: %s - marked as OEM and --include-oem was not set"
% partner
) continue
repack_stub_installer = repack_info.get("repack_stub_installer") # where everything ends up
partner_repack_dir = repacked_builds_dir / DEFAULT_OUTPUT_DIR
# Figure out which base builds we need to repack. for locale in repack_info["locales"]: if options.limit_locales and locale notin options.limit_locales:
log.info("Skipping %s because it is not in limit_locales list", locale) continue
source_locale = locale # Partner has specified a different locale to # use as the base for their custom locale. if"locale." + locale in repack_info:
source_locale = repack_info["locale." + locale] for platform in repack_info["platforms"]: # ja-JP-mac only exists for Mac, so skip non-existent # platform/locale combos. if (source_locale == "ja"and isMac(platform)) or (
source_locale == "ja-JP-mac"andnot isMac(platform)
): continue
ftp_platform = getFtpPlatform(platform)
# for the main repacking artifact
file_name = getFilename(ftp_platform)
local_filename = local_filepath / file_name
# Check to see if this build is already on disk, i.e. # has already been downloaded.
artifacts = getArtifactNames(platform, locale, repack_stub_installer) for artifact in artifacts:
local_artifact = local_filepath / Path(artifact).name if local_artifact.exists():
log.info(f"Found {local_artifact} on disk, not downloading") continue
if artifact notin artifact_ids:
log.fatal( "Can't determine what taskID to retrieve %s from", artifact
)
sys.exit(1)
original_build_url = "%s/%s" % (
TASKCLUSTER_ARTIFACTS.format(taskId=artifact_ids[artifact]),
artifact,
)
retrieveFile(original_build_url, local_artifact)
# Make sure we have the local file now ifnot local_filename.exists():
log.info(f"Error: Unable to retrieve {file_name}\n")
sys.exit(1)
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.