chromium/chrome/installer/linux/debian/update_dist_package_versions.py

#!/usr/bin/env python3
# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Downloads package lists and records package versions into
dist_package_versions.json.
"""

import binascii
import hashlib
import io
import json
import lzma
import os
import re
import subprocess
import sys
import tempfile
import urllib.request

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))

SUPPORTED_DEBIAN_RELEASES = {
    "Debian 11 (Bullseye)": "bullseye",
    "Debian 12 (Bookworm)": "bookworm",
}

SUPPORTED_UBUNTU_RELEASES = {
    "Ubuntu 18.04 (Bionic)": "bionic",
    "Ubuntu 20.04 (Focal)": "focal",
    "Ubuntu 22.04 (Jammy)": "jammy",
}

PACKAGE_FILTER = {
    "libasound2",
    "libatk-bridge2.0-0",
    "libatk1.0-0",
    "libatspi2.0-0",
    "libc6",
    "libcairo2",
    "libcups2",
    "libdbus-1-3",
    "libdrm2",
    "libexpat1",
    "libgbm1",
    # See the comment in calculate_package_deps.py about libgcc_s.
    # TODO(https://crbug.com/40549424): Add this once support for
    # Ubuntu Bionic is dropped.
    # "libgcc-s1",
    "libglib2.0-0",
    "libnspr4",
    "libnss3",
    "libpango-1.0-0",
    "libpangocairo-1.0-0",
    "libstdc++6",
    "libudev1",
    "libuuid1",
    "libx11-6",
    "libx11-xcb1",
    "libxcb-dri3-0",
    "libxcb1",
    "libxcomposite1",
    "libxcursor1",
    "libxdamage1",
    "libxext6",
    "libxfixes3",
    "libxi6",
    "libxkbcommon0",
    "libxrandr2",
    "libxrender1",
    "libxshmfence1",
    "libxss1",
    "libxtst6",
}


def create_temp_file_from_data(data):
    file = tempfile.NamedTemporaryFile()
    file.write(data)
    file.flush()
    return file


if not sys.platform.startswith("linux"):
    print("Only supported on Linux.", file=sys.stderr)
    sys.exit(1)

deb_sources = {}
for release in SUPPORTED_DEBIAN_RELEASES:
    codename = SUPPORTED_DEBIAN_RELEASES[release]
    deb_sources[release] = [{
        "base_url": url,
        "packages": ["main/binary-amd64/Packages.xz"]
    } for url in [
        "http://ftp.us.debian.org/debian/dists/%s" % codename,
        "http://ftp.us.debian.org/debian/dists/%s-updates" % codename,
        "http://security.debian.org/dists/%s-security/updates" % codename,
    ]]
for release in SUPPORTED_UBUNTU_RELEASES:
    codename = SUPPORTED_UBUNTU_RELEASES[release]
    repos = ["main", "universe"]
    deb_sources[release] = [{
        "base_url":
        url,
        "packages": ["%s/binary-amd64/Packages.xz" % repo for repo in repos],
    } for url in [
        "http://us.archive.ubuntu.com/ubuntu/dists/%s" % codename,
        "http://us.archive.ubuntu.com/ubuntu/dists/%s-updates" % codename,
        "http://security.ubuntu.com/ubuntu/dists/%s-security" % codename,
    ]]

distro_package_versions = {}
package_regex = re.compile("^Package: (.*)$")
version_regex = re.compile("^Version: (.*)$")
for distro in deb_sources:
    package_versions = {}
    for source in deb_sources[distro]:
        base_url = source["base_url"]
        with urllib.request.urlopen("%s/Release" % base_url) as response:
            release = response.read().decode("utf-8")
        with urllib.request.urlopen("%s/Release.gpg" % base_url) as response:
            release_gpg = response.read()
        keyring = os.path.join(SCRIPT_DIR, "repo_signing_keys.gpg")
        release_file = create_temp_file_from_data(release.encode("utf-8"))
        release_gpg_file = create_temp_file_from_data(release_gpg)
        subprocess.check_output([
            "gpgv",
            "--quiet",
            "--keyring",
            keyring,
            release_gpg_file.name,
            release_file.name,
        ])
        for packages_xz in source["packages"]:
            with urllib.request.urlopen("%s/%s" %
                                        (base_url, packages_xz)) as response:
                xz_data = response.read()

            sha = hashlib.sha256()
            sha.update(xz_data)
            digest = binascii.hexlify(sha.digest()).decode("utf-8")
            matches = [
                line for line in release.split("\n")
                if digest in line and packages_xz in line
            ]
            assert len(matches) == 1

            with lzma.open(io.BytesIO(xz_data), "rb") as f:
                contents = f.read().decode("utf-8")
            package = ""
            for line in contents.split("\n"):
                if line.startswith("Package: "):
                    match = re.search(package_regex, line)
                    package = match.group(1)
                elif line.startswith("Version: "):
                    match = re.search(version_regex, line)
                    version = match.group(1)
                    if package in PACKAGE_FILTER:
                        package_versions[package] = version
    distro_package_versions[distro] = package_versions

missing_any_package = False
for distro in distro_package_versions:
    missing_packages = PACKAGE_FILTER - set(distro_package_versions[distro])
    if missing_packages:
        missing_any_package = True
        print(
            "Packages are not available on %s: %s" %
            (distro, ", ".join(missing_packages)),
            file=sys.stderr,
        )
if missing_any_package:
    sys.exit(1)

with open(os.path.join(SCRIPT_DIR, "dist_package_versions.json"), "w") as f:
    json.dump(distro_package_versions,
              f,
              sort_keys=True,
              indent=4,
              separators=(",", ": "))
    f.write("\n")