#!/usr/bin/env python3 # Copyright 2023 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # Script to install everything needed to build chromium # including items requiring sudo privileges. # See https://chromium.googlesource.com/chromium/src/+/main/docs/linux/build_instructions.md import argparse import functools import os import re import shutil import subprocess import sys @functools.lru_cache(maxsize=1) def build_apt_package_list(): print("Building apt package list.", file=sys.stderr) output = subprocess.check_output(["apt-cache", "dumpavail"]).decode() arch_map = {"i386": ":i386"} package_regex = re.compile(r"^Package: (.+?)$.+?^Architecture: (.+?)$", re.M | re.S) return set(package + arch_map.get(arch, "") for package, arch in re.findall(package_regex, output)) def package_exists(package_name: str) -> bool: return package_name in build_apt_package_list() def parse_args(argv): parser = argparse.ArgumentParser( description="Install Chromium build dependencies.") parser.add_argument("--syms", action="store_true", help="Enable installation of debugging symbols") parser.add_argument( "--no-syms", action="store_false", dest="syms", help="Disable installation of debugging symbols", ) parser.add_argument( "--lib32", action="store_true", help="Enable installation of 32-bit libraries, e.g. for V8 snapshot", ) parser.add_argument( "--android", action="store_true", help="Enable installation of android dependencies", ) parser.add_argument( "--no-android", action="store_false", dest="android", help="Disable installation of android dependencies", ) parser.add_argument("--arm", action="store_true", help="Enable installation of arm cross toolchain") parser.add_argument( "--no-arm", action="store_false", dest="arm", help="Disable installation of arm cross toolchain", ) parser.add_argument( "--chromeos-fonts", action="store_true", help="Enable installation of Chrome OS fonts", ) parser.add_argument( "--no-chromeos-fonts", action="store_false", dest="chromeos_fonts", help="Disable installation of Chrome OS fonts", ) parser.add_argument( "--nacl", action="store_true", help="Enable installation of prerequisites for building NaCl", ) parser.add_argument( "--no-nacl", action="store_false", dest="nacl", help="Disable installation of prerequisites for building NaCl", ) parser.add_argument( "--backwards-compatible", action="store_true", help= "Enable installation of packages that are no longer currently needed and" + "have been removed from this script. Useful for bisection.", ) parser.add_argument( "--no-backwards-compatible", action="store_false", dest="backwards_compatible", help= "Disable installation of packages that are no longer currently needed and" + "have been removed from this script.", ) parser.add_argument("--no-prompt", action="store_true", help="Automatic yes to prompts") parser.add_argument( "--quick-check", action="store_true", help="Quickly try to determine if dependencies are installed", ) parser.add_argument( "--unsupported", action="store_true", help="Attempt installation even on unsupported systems", ) options = parser.parse_args(argv) if options.arm or options.android: options.lib32 = True return options def check_lsb_release(): if not shutil.which("lsb_release"): print("ERROR: lsb_release not found in $PATH", file=sys.stderr) print("try: sudo apt-get install lsb-release", file=sys.stderr) sys.exit(1) @functools.lru_cache(maxsize=1) def distro_codename(): return subprocess.check_output(["lsb_release", "--codename", "--short"]).decode().strip() def check_distro(options): if options.unsupported or options.quick_check: return distro_id = subprocess.check_output(["lsb_release", "--id", "--short"]).decode().strip() supported_codenames = ["bionic", "focal", "jammy"] supported_ids = ["Debian"] if (distro_codename() not in supported_codenames and distro_id not in supported_ids): print( "WARNING: The following distributions are supported,", "but distributions not in the list below can also try to install", "dependencies by passing the `--unsupported` parameter", "\tUbuntu 18.04 LTS (bionic with EoL April 2028)", "\tUbuntu 20.04 LTS (focal with EoL April 2030)", "\tUbuntu 22.04 LTS (jammy with EoL April 2032)", "\tDebian 10 (buster) or later", sep="\n", file=sys.stderr, ) sys.exit(1) def check_architecture(): architecture = subprocess.check_output(["uname", "-m"]).decode().strip() if architecture not in ["i686", "x86_64"]: print("Only x86 architectures are currently supported", file=sys.stderr) sys.exit(1) def check_root(): if os.geteuid() != 0: print("Running as non-root user.", file=sys.stderr) print("You might have to enter your password one or more times for 'sudo'.", file=sys.stderr) print(file=sys.stderr) def apt_update(options): if options.lib32 or options.nacl: subprocess.check_call(["sudo", "dpkg", "--add-architecture", "i386"]) subprocess.check_call(["sudo", "apt-get", "update"]) # Packages needed for development def dev_list(): packages = [ "binutils", "bison", "bzip2", "cdbs", "curl", "dbus-x11", "devscripts", "dpkg-dev", "elfutils", "fakeroot", "flex", "git-core", "gperf", "libasound2-dev", "libatspi2.0-dev", "libbrlapi-dev", "libbz2-dev", "libc6-dev", "libcairo2-dev", "libcap-dev", "libcups2-dev", "libcurl4-gnutls-dev", "libdrm-dev", "libelf-dev", "libevdev-dev", "libffi-dev", "libfuse2", "libgbm-dev", "libglib2.0-dev", "libglu1-mesa-dev", "libgtk-3-dev", "libkrb5-dev", "libnspr4-dev", "libnss3-dev", "libpam0g-dev", "libpci-dev", "libpulse-dev", "libsctp-dev", "libspeechd-dev", "libsqlite3-dev", "libssl-dev", "libsystemd-dev", "libudev-dev", "libva-dev", "libwww-perl", "libxshmfence-dev", "libxslt1-dev", "libxss-dev", "libxt-dev", "libxtst-dev", "lighttpd", "locales", "openbox", "p7zip", "patch", "perl", "pkg-config", "rpm", "ruby", "subversion", "uuid-dev", "wdiff", "x11-utils", "xcompmgr", "xz-utils", "zip", ] # Packages needed for chromeos only packages += [ "libbluetooth-dev", "libxkbcommon-dev", "mesa-common-dev", "zstd", ] if package_exists("realpath"): packages.append("realpath") if package_exists("libjpeg-dev"): packages.append("libjpeg-dev") else: packages.append("libjpeg62-dev") if package_exists("libudev1"): packages.append("libudev1") else: packages.append("libudev0") if package_exists("libbrlapi0.8"): packages.append("libbrlapi0.8") elif package_exists("libbrlapi0.7"): packages.append("libbrlapi0.7") elif package_exists("libbrlapi0.6"): packages.append("libbrlapi0.6") else: packages.append("libbrlapi0.5") if package_exists("libav-tools"): packages.append("libav-tools") if package_exists("libvulkan-dev"): packages.append("libvulkan-dev") if package_exists("libinput-dev"): packages.append("libinput-dev") # Cross-toolchain strip is needed for building the sysroots. if package_exists("binutils-arm-linux-gnueabihf"): packages.append("binutils-arm-linux-gnueabihf") if package_exists("binutils-aarch64-linux-gnu"): packages.append("binutils-aarch64-linux-gnu") if package_exists("binutils-mipsel-linux-gnu"): packages.append("binutils-mipsel-linux-gnu") if package_exists("binutils-mips64el-linux-gnuabi64"): packages.append("binutils-mips64el-linux-gnuabi64") # 64-bit systems need a minimum set of 32-bit compat packages for the # pre-built NaCl binaries. if "ELF 64-bit" in subprocess.check_output(["file", "-L", "/sbin/init"]).decode(): packages.extend(["libc6-i386", "lib32stdc++6"]) # lib32gcc-s1 used to be called lib32gcc1 in older distros. if package_exists("lib32gcc-s1"): packages.append("lib32gcc-s1") elif package_exists("lib32gcc1"): packages.append("lib32gcc1") return packages # List of required run-time libraries def lib_list(): packages = [ "lib32z1", "libasound2", "libatk1.0-0", "libatspi2.0-0", "libc6", "libcairo2", "libcap2", "libcgi-session-perl", "libcups2", "libdrm2", "libegl1", "libevdev2", "libexpat1", "libfontconfig1", "libfreetype6", "libgbm1", "libglib2.0-0", "libgl1", "libgtk-3-0", "libncurses5", "libpam0g", "libpango-1.0-0", "libpangocairo-1.0-0", "libpci3", "libpcre3", "libpixman-1-0", "libspeechd2", "libstdc++6", "libsqlite3-0", "libuuid1", "libwayland-egl1", "libwayland-egl1-mesa", "libx11-6", "libx11-xcb1", "libxau6", "libxcb1", "libxcomposite1", "libxcursor1", "libxdamage1", "libxdmcp6", "libxext6", "libxfixes3", "libxi6", "libxinerama1", "libxrandr2", "libxrender1", "libxtst6", "x11-utils", "xvfb", "zlib1g", ] # Run-time libraries required by chromeos only packages += [ "libpulse0", "libbz2-1.0", ] if package_exists("libffi8"): packages.append("libffi8") elif package_exists("libffi7"): packages.append("libffi7") elif package_exists("libffi6"): packages.append("libffi6") if package_exists("libpng16-16"): packages.append("libpng16-16") else: packages.append("libpng12-0") if package_exists("libnspr4"): packages.extend(["libnspr4", "libnss3"]) else: packages.extend(["libnspr4-0d", "libnss3-1d"]) if package_exists("appmenu-gtk"): packages.append("appmenu-gtk") if package_exists("libgnome-keyring0"): packages.append("libgnome-keyring0") if package_exists("libgnome-keyring-dev"): packages.append("libgnome-keyring-dev") if package_exists("libvulkan1"): packages.append("libvulkan1") if package_exists("libinput10"): packages.append("libinput10") return packages def lib32_list(options): if not options.lib32: print("Skipping 32-bit libraries.", file=sys.stderr) return [] print("Including 32-bit libraries.", file=sys.stderr) packages = [ # 32-bit libraries needed for a 32-bit build # includes some 32-bit libraries required by the Android SDK # See https://developer.android.com/sdk/installing/index.html?pkg=tools "libasound2:i386", "libatk-bridge2.0-0:i386", "libatk1.0-0:i386", "libatspi2.0-0:i386", "libdbus-1-3:i386", "libegl1:i386", "libgl1:i386", "libglib2.0-0:i386", "libncurses5:i386", "libnss3:i386", "libpango-1.0-0:i386", "libpangocairo-1.0-0:i386", "libstdc++6:i386", "libwayland-egl1:i386", "libx11-xcb1:i386", "libxcomposite1:i386", "libxdamage1:i386", "libxkbcommon0:i386", "libxrandr2:i386", "libxtst6:i386", "zlib1g:i386", # 32-bit libraries needed e.g. to compile V8 snapshot for Android or armhf "linux-libc-dev:i386", "libpci3:i386", ] # When cross building for arm/Android on 64-bit systems the host binaries # that are part of v8 need to be compiled with -m32 which means # that basic multilib support is needed. if "ELF 64-bit" in subprocess.check_output(["file", "-L", "/sbin/init"]).decode(): # gcc-multilib conflicts with the arm cross compiler but # g++-X.Y-multilib gives us the 32-bit support that we need. Find out the # appropriate value of X and Y by seeing what version the current # distribution's g++-multilib package depends on. lines = subprocess.check_output( ["apt-cache", "depends", "g++-multilib", "--important"]).decode() pattern = re.compile(r"g\+\+-[0-9.]+-multilib") packages += re.findall(pattern, lines) return packages # Packages that have been removed from this script. Regardless of configuration # or options passed to this script, whenever a package is removed, it should be # added here. def backwards_compatible_list(options): if not options.backwards_compatible: print("Skipping backwards compatible packages.", file=sys.stderr) return [] print("Including backwards compatible packages.", file=sys.stderr) packages = [ "7za", "fonts-indic", "fonts-ipafont", "fonts-stix", "fonts-thai-tlwg", "fonts-tlwg-garuda", "g++", "g++-4.8-multilib-arm-linux-gnueabihf", "gcc-4.8-multilib-arm-linux-gnueabihf", "g++-9-multilib-arm-linux-gnueabihf", "gcc-9-multilib-arm-linux-gnueabihf", "gcc-arm-linux-gnueabihf", "g++-10-multilib-arm-linux-gnueabihf", "gcc-10-multilib-arm-linux-gnueabihf", "g++-10-arm-linux-gnueabihf", "gcc-10-arm-linux-gnueabihf", "git-svn", "language-pack-da", "language-pack-fr", "language-pack-he", "language-pack-zh-hant", "libappindicator-dev", "libappindicator1", "libappindicator3-1", "libappindicator3-dev", "libdconf-dev", "libdconf1", "libdconf1:i386", "libexif-dev", "libexif12", "libexif12:i386", "libgbm-dev", "libgbm-dev-lts-trusty", "libgbm-dev-lts-xenial", "libgconf-2-4:i386", "libgconf2-dev", "libgl1-mesa-dev", "libgl1-mesa-dev-lts-trusty", "libgl1-mesa-dev-lts-xenial", "libgl1-mesa-glx:i386", "libgl1-mesa-glx-lts-trusty:i386", "libgl1-mesa-glx-lts-xenial:i386", "libgles2-mesa-dev", "libgles2-mesa-dev-lts-trusty", "libgles2-mesa-dev-lts-xenial", "libgtk-3-0:i386", "libgtk2.0-0", "libgtk2.0-0:i386", "libgtk2.0-dev", "mesa-common-dev", "mesa-common-dev-lts-trusty", "mesa-common-dev-lts-xenial", "msttcorefonts", "python-dev", "python-setuptools", "snapcraft", "ttf-dejavu-core", "ttf-indic-fonts", "ttf-kochi-gothic", "ttf-kochi-mincho", "ttf-mscorefonts-installer", "xfonts-mathml", ] if package_exists("python-is-python2"): packages.extend(["python-is-python2", "python2-dev"]) else: packages.append("python") if package_exists("python-crypto"): packages.append("python-crypto") if package_exists("python-numpy"): packages.append("python-numpy") if package_exists("python-openssl"): packages.append("python-openssl") if package_exists("python-psutil"): packages.append("python-psutil") if package_exists("python-yaml"): packages.append("python-yaml") if package_exists("apache2.2-bin"): packages.append("apache2.2-bin") else: packages.append("apache2-bin") php_versions = [ ("php8.1-cgi", "libapache2-mod-php8.1"), ("php8.0-cgi", "libapache2-mod-php8.0"), ("php7.4-cgi", "libapache2-mod-php7.4"), ("php7.3-cgi", "libapache2-mod-php7.3"), ("php7.2-cgi", "libapache2-mod-php7.2"), ("php7.1-cgi", "libapache2-mod-php7.1"), ("php7.0-cgi", "libapache2-mod-php7.0"), ("php5-cgi", "libapache2-mod-php5"), ] for php_cgi, mod_php in php_versions: if package_exists(php_cgi): packages.extend([php_cgi, mod_php]) break return [package for package in packages if package_exists(package)] def arm_list(options): if not options.arm: print("Skipping ARM cross toolchain.", file=sys.stderr) return [] print("Including ARM cross toolchain.", file=sys.stderr) # arm cross toolchain packages needed to build chrome on armhf packages = [ "libc6-dev-armhf-cross", "linux-libc-dev-armhf-cross", "g++-arm-linux-gnueabihf", ] # Work around for dependency issue Ubuntu: http://crbug.com/435056 if distro_codename() == "bionic": packages.extend([ "g++-5-multilib-arm-linux-gnueabihf", "gcc-5-multilib-arm-linux-gnueabihf", "gcc-arm-linux-gnueabihf", ]) elif distro_codename() == "focal": packages.extend([ "g++-10-multilib-arm-linux-gnueabihf", "gcc-10-multilib-arm-linux-gnueabihf", "gcc-arm-linux-gnueabihf", ]) elif distro_codename() == "jammy": packages.extend([ "gcc-arm-linux-gnueabihf", "g++-11-arm-linux-gnueabihf", "gcc-11-arm-linux-gnueabihf", ]) return packages def nacl_list(options): if not options.nacl: print("Skipping NaCl, NaCl toolchain, NaCl ports dependencies.", file=sys.stderr) return [] print("Including NaCl, NaCl toolchain, NaCl ports dependencies.", file=sys.stderr) packages = [ "g++-mingw-w64-i686", "lib32z1-dev", "libasound2:i386", "libcap2:i386", "libelf-dev:i386", "libfontconfig1:i386", "libglib2.0-0:i386", "libgpm2:i386", "libncurses5:i386", "lib32ncurses5-dev", "libnss3:i386", "libpango-1.0-0:i386", "libssl-dev:i386", "libtinfo-dev", "libtinfo-dev:i386", "libtool", "libuuid1:i386", "libxcomposite1:i386", "libxcursor1:i386", "libxdamage1:i386", "libxi6:i386", "libxrandr2:i386", "libxss1:i386", "libxtst6:i386", "texinfo", "xvfb", # Packages to build NaCl, its toolchains, and its ports. "ant", "autoconf", "bison", "cmake", "gawk", "intltool", "xutils-dev", "xsltproc", ] # Some package names have changed over time if package_exists("libssl-dev"): packages.append("libssl-dev:i386") elif package_exists("libssl1.1"): packages.append("libssl1.1:i386") elif package_exists("libssl1.0.2"): packages.append("libssl1.0.2:i386") else: packages.append("libssl1.0.0:i386") if package_exists("libtinfo5"): packages.append("libtinfo5") if package_exists("libudev1"): packages.append("libudev1:i386") else: packages.append("libudev0:i386") return packages # Debian is in the process of transitioning to automatic debug packages, which # have the -dbgsym suffix (https://wiki.debian.org/AutomaticDebugPackages). # Untransitioned packages have the -dbg suffix. And on some systems, neither # will be available, so exclude the ones that are missing. def dbg_package_name(package): if package_exists(package + "-dbgsym"): return [package + "-dbgsym"] if package_exists(package + "-dbg"): return [package + "-dbg"] return [] def dbg_list(options): if not options.syms: print("Skipping debugging symbols.", file=sys.stderr) return [] print("Including debugging symbols.", file=sys.stderr) packages = [ dbg_package for package in lib_list() for dbg_package in dbg_package_name(package) ] # Debugging symbols packages not following common naming scheme if not dbg_package_name("libstdc++6"): for version in ["8", "7", "6", "5", "4.9", "4.8", "4.7", "4.6"]: if package_exists("libstdc++6-%s-dbg" % version): packages.append("libstdc++6-%s-dbg" % version) break if not dbg_package_name("libatk1.0-0"): packages.extend(dbg_package_name("libatk1.0")) if not dbg_package_name("libpango-1.0-0"): packages.extend(dbg_package_name("libpango1.0-dev")) return packages def package_list(options): packages = (dev_list() + lib_list() + dbg_list(options) + lib32_list(options) + arm_list(options) + nacl_list(options) + backwards_compatible_list(options)) # Sort all the :i386 packages to the front, to avoid confusing dpkg-query # (https://crbug.com/446172). return sorted(set(packages), key=lambda x: (not x.endswith(":i386"), x)) def missing_packages(packages): try: subprocess.run( ["dpkg-query", "-W", "-f", " "] + packages, check=True, capture_output=True, ) return [] except subprocess.CalledProcessError as e: return [ line.split(" ")[-1] for line in e.stderr.decode().strip().splitlines() ] def package_is_installable(package): result = subprocess.run(["apt-cache", "show", package], capture_output=True) return result.returncode == 0 def quick_check(options): if not options.quick_check: return missing = missing_packages(package_list(options)) if not missing: sys.exit(0) not_installed = [] unknown = [] for p in missing: if package_is_installable(p): not_installed.append(p) else: unknown.append(p) if not_installed: print("WARNING: The following packages are not installed:", file=sys.stderr) print(" ".join(not_installed), file=sys.stderr) if unknown: print("WARNING: The following packages are unknown to your system", file=sys.stderr) print("(maybe missing a repo or need to 'sudo apt-get update'):", file=sys.stderr) print(" ".join(unknown), file=sys.stderr) sys.exit(1) def find_missing_packages(options): print("Finding missing packages...", file=sys.stderr) packages = package_list(options) packages_str = " ".join(packages) print("Packages required: " + packages_str, file=sys.stderr) query_cmd = ["apt-get", "--just-print", "install"] + packages env = os.environ.copy() env["LANGUAGE"] = "en" env["LANG"] = "C" cmd_output = subprocess.check_output(query_cmd, env=env).decode() lines = cmd_output.splitlines() install = [] for pattern in ( "The following NEW packages will be installed:", "The following packages will be upgraded:", ): if pattern in lines: for line in lines[lines.index(pattern) + 1:]: if not line.startswith(" "): break install += line.strip().split(" ") return install def install_packages(options): try: packages = find_missing_packages(options) if packages: quiet = ["-qq", "--assume-yes"] if options.no_prompt else [] subprocess.check_call(["sudo", "apt-get", "install"] + quiet + packages) print(file=sys.stderr) else: print("No missing packages, and the packages are up to date.", file=sys.stderr) except subprocess.CalledProcessError as e: # An apt-get exit status of 100 indicates that a real error has occurred. print("`apt-get --just-print install ...` failed", file=sys.stderr) print("It produced the following output:", file=sys.stderr) print(e.output.decode(), file=sys.stderr) print(file=sys.stderr) print("You will have to install the above packages yourself.", file=sys.stderr) print(file=sys.stderr) sys.exit(100) # Install the Chrome OS default fonts. This must go after running # apt-get, since install-chromeos-fonts depends on curl. def install_chromeos_fonts(options): if not options.chromeos_fonts: print("Skipping installation of Chrome OS fonts.", file=sys.stderr) return print("Installing Chrome OS fonts.", file=sys.stderr) dir = os.path.abspath(os.path.dirname(__file__)) try: subprocess.check_call( ["sudo", os.path.join(dir, "linux", "install-chromeos-fonts.py")]) except subprocess.CalledProcessError: print("ERROR: The installation of the Chrome OS default fonts failed.", file=sys.stderr) if (subprocess.check_output( ["stat", "-f", "-c", "%T", dir], ).decode().startswith("nfs")): print( "The reason is that your repo is installed on a remote file system.", file=sys.stderr) else: print( "This is expected if your repo is installed on a remote file system.", file=sys.stderr) print("It is recommended to install your repo on a local file system.", file=sys.stderr) print("You can skip the installation of the Chrome OS default fonts with", file=sys.stderr) print("the command line option: --no-chromeos-fonts.", file=sys.stderr) sys.exit(1) def install_locales(): print("Installing locales.", file=sys.stderr) CHROMIUM_LOCALES = [ "da_DK.UTF-8", "fr_FR.UTF-8", "he_IL.UTF-8", "zh_TW.UTF-8" ] LOCALE_GEN = "/etc/locale.gen" if os.path.exists(LOCALE_GEN): old_locale_gen = open(LOCALE_GEN).read() for locale in CHROMIUM_LOCALES: subprocess.check_call( ["sudo", "sed", "-i", "s/^# %s/%s/" % (locale, locale), LOCALE_GEN]) # Regenerating locales can take a while, so only do it if we need to. locale_gen = open(LOCALE_GEN).read() if locale_gen != old_locale_gen: subprocess.check_call(["sudo", "locale-gen"]) else: print("Locales already up-to-date.", file=sys.stderr) else: for locale in CHROMIUM_LOCALES: subprocess.check_call(["sudo", "locale-gen", locale]) def main(): options = parse_args(sys.argv[1:]) check_lsb_release() check_distro(options) check_architecture() quick_check(options) check_root() apt_update(options) install_packages(options) install_chromeos_fonts(options) install_locales() return 0 if __name__ == "__main__": sys.exit(main())