# Copyright 2013 The Servo Project Developers. See the COPYRIGHT # file at the top-level directory of this distribution. # # Licensed under the Apache License, Version 2.0 or the MIT license # , at your # option. This file may not be copied, modified, or distributed # except according to those terms. from __future__ import absolute_import, print_function, unicode_literals import base64 import json import os import os.path as path import platform import re import subprocess import sys import traceback import urllib2 import glob from mach.decorators import ( CommandArgument, CommandProvider, Command, ) import servo.bootstrap as bootstrap from servo.command_base import CommandBase, cd, check_call from servo.util import delete, download_bytes, download_file, extract, check_hash @CommandProvider class MachCommands(CommandBase): @Command('bootstrap', description='Install required packages for building.', category='bootstrap') @CommandArgument('--force', '-f', action='store_true', help='Boostrap without confirmation') def bootstrap(self, force=False): # This entry point isn't actually invoked, ./mach bootstrap is directly # called by mach (see mach_bootstrap.bootstrap_command_only) so that # it can install dependencies without needing mach's dependencies return bootstrap.bootstrap(self.context, force=force) @Command('bootstrap-salt', description='Install and set up the salt environment.', category='bootstrap') @CommandArgument('--force', '-f', action='store_true', help='Boostrap without confirmation') def bootstrap_salt(self, force=False): return bootstrap.bootstrap(self.context, force=force, specific="salt") @Command('bootstrap-gstreamer', description='Set up a local copy of the gstreamer libraries (linux only).', category='bootstrap') @CommandArgument('--force', '-f', action='store_true', help='Boostrap without confirmation') def bootstrap_gstreamer(self, force=False): return bootstrap.bootstrap(self.context, force=force, specific="gstreamer") @Command('bootstrap-android', description='Install the Android SDK and NDK.', category='bootstrap') @CommandArgument('--build', action='store_true', help='Install Android-specific dependencies for building') @CommandArgument('--emulator-x86', action='store_true', help='Install Android x86 emulator and system image') @CommandArgument('--accept-all-licences', action='store_true', help='For non-interactive use') def bootstrap_android(self, build=False, emulator_x86=False, accept_all_licences=False): if not (build or emulator_x86): print("Must specify `--build` or `--emulator-x86` or both.") ndk = "android-ndk-r15c-{system}-{arch}" tools = "sdk-tools-{system}-4333796" emulator_platform = "android-28" emulator_image = "system-images;%s;google_apis;x86" % emulator_platform known_sha1 = { # https://dl.google.com/android/repository/repository2-1.xml "sdk-tools-darwin-4333796.zip": "ed85ea7b59bc3483ce0af4c198523ba044e083ad", "sdk-tools-linux-4333796.zip": "8c7c28554a32318461802c1291d76fccfafde054", "sdk-tools-windows-4333796.zip": "aa298b5346ee0d63940d13609fe6bec621384510", # https://developer.android.com/ndk/downloads/older_releases "android-ndk-r15c-windows-x86.zip": "f2e47121feb73ec34ced5e947cbf1adc6b56246e", "android-ndk-r15c-windows-x86_64.zip": "970bb2496de0eada74674bb1b06d79165f725696", "android-ndk-r15c-darwin-x86_64.zip": "ea4b5d76475db84745aa8828000d009625fc1f98", "android-ndk-r15c-linux-x86_64.zip": "0bf02d4e8b85fd770fd7b9b2cdec57f9441f27a2", } toolchains = path.join(self.context.topdir, "android-toolchains") if not path.isdir(toolchains): os.makedirs(toolchains) def download(target_dir, name, flatten=False): final = path.join(toolchains, target_dir) if path.isdir(final): return base_url = "https://dl.google.com/android/repository/" filename = name + ".zip" url = base_url + filename archive = path.join(toolchains, filename) if not path.isfile(archive): download_file(filename, url, archive) check_hash(archive, known_sha1[filename], "sha1") print("Extracting " + filename) remove = True # Set to False to avoid repeated downloads while debugging this script if flatten: extracted = final + "_" extract(archive, extracted, remove=remove) contents = os.listdir(extracted) assert len(contents) == 1 os.rename(path.join(extracted, contents[0]), final) os.rmdir(extracted) else: extract(archive, final, remove=remove) system = platform.system().lower() machine = platform.machine().lower() arch = {"i386": "x86"}.get(machine, machine) if build: download("ndk", ndk.format(system=system, arch=arch), flatten=True) download("sdk", tools.format(system=system)) components = [] if emulator_x86: components += [ "platform-tools", "emulator", "platforms;" + emulator_platform, emulator_image, ] if build: components += [ "platforms;android-18", ] sdkmanager = [path.join(toolchains, "sdk", "tools", "bin", "sdkmanager")] + components if accept_all_licences: yes = subprocess.Popen(["yes"], stdout=subprocess.PIPE) process = subprocess.Popen( sdkmanager, stdin=yes.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) # Reduce progress bar spam by removing duplicate lines. # Printing the same line again with \r is a no-op in a real terminal, # but each line is shown individually in Taskcluster's log viewer. previous_line = None line = b"" while 1: # Read one byte at a time because in Python: # * readline() blocks until "\n", which doesn't come before the prompt # * read() blocks until EOF, which doesn't come before the prompt # * read(n) keeps reading until it gets n bytes or EOF, # but we don't know reliably how many bytes to read until the prompt byte = process.stdout.read(1) if len(byte) == 0: print(line) break line += byte if byte == b'\n' or byte == b'\r': if line != previous_line: print(line.decode("utf-8", "replace"), end="") sys.stdout.flush() previous_line = line line = b"" exit_code = process.wait() yes.terminate() if exit_code: return exit_code else: subprocess.check_call(sdkmanager) if emulator_x86: avd_path = path.join(toolchains, "avd", "servo-x86") process = subprocess.Popen(stdin=subprocess.PIPE, stdout=subprocess.PIPE, args=[ path.join(toolchains, "sdk", "tools", "bin", "avdmanager"), "create", "avd", "--path", avd_path, "--name", "servo-x86", "--package", emulator_image, "--force", ]) output = b"" while 1: # Read one byte at a time, see comment above. byte = process.stdout.read(1) if len(byte) == 0: break output += byte # There seems to be no way to disable this prompt: if output.endswith(b"Do you wish to create a custom hardware profile? [no]"): process.stdin.write("no\n") assert process.wait() == 0 with open(path.join(avd_path, "config.ini"), "a") as f: f.write("disk.dataPartition.size=2G\n") @Command('update-hsts-preload', description='Download the HSTS preload list', category='bootstrap') def bootstrap_hsts_preload(self, force=False): preload_filename = "hsts_preload.json" preload_path = path.join(self.context.topdir, "resources") chromium_hsts_url = "https://chromium.googlesource.com/chromium/src" + \ "/net/+/master/http/transport_security_state_static.json?format=TEXT" try: content_base64 = download_bytes("Chromium HSTS preload list", chromium_hsts_url) except urllib2.URLError: print("Unable to download chromium HSTS preload list; are you connected to the internet?") sys.exit(1) content_decoded = base64.b64decode(content_base64) # The chromium "json" has single line comments in it which, of course, # are non-standard/non-valid json. Simply strip them out before parsing content_json = re.sub(r'(^|\s+)//.*$', '', content_decoded, flags=re.MULTILINE) try: pins_and_static_preloads = json.loads(content_json) entries = { "entries": [ { "host": e["name"], "include_subdomains": e.get("include_subdomains", False) } for e in pins_and_static_preloads["entries"] ] } with open(path.join(preload_path, preload_filename), 'w') as fd: json.dump(entries, fd, indent=4) except ValueError, e: print("Unable to parse chromium HSTS preload list, has the format changed?") sys.exit(1) @Command('update-pub-domains', description='Download the public domains list and update resources/public_domains.txt', category='bootstrap') def bootstrap_pub_suffix(self, force=False): list_url = "https://publicsuffix.org/list/public_suffix_list.dat" dst_filename = path.join(self.context.topdir, "resources", "public_domains.txt") not_implemented_case = re.compile(r'^[^*]+\*') try: content = download_bytes("Public suffix list", list_url) except urllib2.URLError: print("Unable to download the public suffix list; are you connected to the internet?") sys.exit(1) lines = [l.strip() for l in content.decode("utf8").split("\n")] suffixes = [l for l in lines if not l.startswith("//") and not l == ""] with open(dst_filename, "wb") as fo: for suffix in suffixes: if not_implemented_case.match(suffix): print("Warning: the new list contains a case that servo can't handle: %s" % suffix) fo.write(suffix.encode("idna") + "\n") @Command('clean-nightlies', description='Clean unused nightly builds of Rust and Cargo', category='bootstrap') @CommandArgument('--force', '-f', action='store_true', help='Actually remove stuff') @CommandArgument('--keep', default='1', help='Keep up to this many most recent nightlies') def clean_nightlies(self, force=False, keep=None): default_toolchain = self.default_toolchain() print("Current Rust version for Servo: {}".format(default_toolchain)) old_toolchains = [] keep = int(keep) stdout = subprocess.check_output(['git', 'log', '--format=%H', 'rust-toolchain']) for i, commit_hash in enumerate(stdout.split(), 1): if i > keep: toolchain = subprocess.check_output( ['git', 'show', '%s:rust-toolchain' % commit_hash]) old_toolchains.append(toolchain.strip()) removing_anything = False stdout = subprocess.check_output(['rustup', 'toolchain', 'list']) for toolchain_with_host in stdout.split(): for old in old_toolchains: if toolchain_with_host.startswith(old): removing_anything = True if force: print("Removing {}".format(toolchain_with_host)) check_call(["rustup", "uninstall", toolchain_with_host]) else: print("Would remove {}".format(toolchain_with_host)) if not removing_anything: print("Nothing to remove.") elif not force: print("Nothing done. " "Run `./mach clean-nightlies -f` to actually remove.") @Command('clean-cargo-cache', description='Clean unused Cargo packages', category='bootstrap') @CommandArgument('--force', '-f', action='store_true', help='Actually remove stuff') @CommandArgument('--show-size', '-s', action='store_true', help='Show packages size') @CommandArgument('--keep', default='1', help='Keep up to this many most recent dependencies') def clean_cargo_cache(self, force=False, show_size=False, keep=None): def get_size(path): if os.path.isfile(path): return os.path.getsize(path) / (1024 * 1024.0) total_size = 0 for dirpath, dirnames, filenames in os.walk(path): for f in filenames: fp = os.path.join(dirpath, f) total_size += os.path.getsize(fp) return total_size / (1024 * 1024.0) removing_anything = False packages = { 'crates': {}, 'git': {}, } import toml if os.environ.get("CARGO_HOME", ""): cargo_dir = os.environ.get("CARGO_HOME") else: home_dir = os.path.expanduser("~") cargo_dir = path.join(home_dir, ".cargo") if not os.path.isdir(cargo_dir): return cargo_file = open(path.join(self.context.topdir, "Cargo.lock")) content = toml.load(cargo_file) for package in content.get("package", []): source = package.get("source", "") version = package["version"] if source == u"registry+https://github.com/rust-lang/crates.io-index": crate_name = "{}-{}".format(package["name"], version) if not packages["crates"].get(crate_name, False): packages["crates"][package["name"]] = { "current": [], "exist": [], } packages["crates"][package["name"]]["current"].append(crate_name) elif source.startswith("git+"): name = source.split("#")[0].split("/")[-1].replace(".git", "") branch = "" crate_name = "{}-{}".format(package["name"], source.split("#")[1]) crate_branch = name.split("?") if len(crate_branch) > 1: branch = crate_branch[1].replace("branch=", "") name = crate_branch[0] if not packages["git"].get(name, False): packages["git"][name] = { "current": [], "exist": [], } packages["git"][name]["current"].append(source.split("#")[1][:7]) if branch: packages["git"][name]["current"].append(branch) crates_dir = path.join(cargo_dir, "registry") crates_cache_dir = "" crates_src_dir = "" if os.path.isdir(path.join(crates_dir, "cache")): for p in os.listdir(path.join(crates_dir, "cache")): crates_cache_dir = path.join(crates_dir, "cache", p) crates_src_dir = path.join(crates_dir, "src", p) git_dir = path.join(cargo_dir, "git") git_db_dir = path.join(git_dir, "db") git_checkout_dir = path.join(git_dir, "checkouts") if os.path.isdir(git_db_dir): git_db_list = filter(lambda f: not f.startswith('.'), os.listdir(git_db_dir)) else: git_db_list = [] if os.path.isdir(git_checkout_dir): git_checkout_list = os.listdir(git_checkout_dir) else: git_checkout_list = [] for d in list(set(git_db_list + git_checkout_list)): crate_name = d.replace("-{}".format(d.split("-")[-1]), "") if not packages["git"].get(crate_name, False): packages["git"][crate_name] = { "current": [], "exist": [], } if os.path.isdir(path.join(git_checkout_dir, d)): with cd(path.join(git_checkout_dir, d)): git_crate_hash = glob.glob('*') if not git_crate_hash or not os.path.isdir(path.join(git_db_dir, d)): packages["git"][crate_name]["exist"].append(("del", d, "")) continue for d2 in git_crate_hash: dep_path = path.join(git_checkout_dir, d, d2) if os.path.isdir(dep_path): packages["git"][crate_name]["exist"].append((path.getmtime(dep_path), d, d2)) elif os.path.isdir(path.join(git_db_dir, d)): packages["git"][crate_name]["exist"].append(("del", d, "")) if crates_src_dir: for d in os.listdir(crates_src_dir): crate_name = re.sub(r"\-\d+(\.\d+){1,3}.+", "", d) if not packages["crates"].get(crate_name, False): packages["crates"][crate_name] = { "current": [], "exist": [], } packages["crates"][crate_name]["exist"].append(d) total_size = 0 for packages_type in ["git", "crates"]: sorted_packages = sorted(packages[packages_type]) for crate_name in sorted_packages: crate_count = 0 existed_crates = packages[packages_type][crate_name]["exist"] for exist in sorted(existed_crates, reverse=True): current_crate = packages[packages_type][crate_name]["current"] size = 0 exist_name = path.join(exist[1], exist[2]) if packages_type == "git" else exist exist_item = exist[2] if packages_type == "git" else exist if exist_item not in current_crate: crate_count += 1 if int(crate_count) >= int(keep) or not current_crate or \ exist[0] == "del" or exist[2] == "master": removing_anything = True crate_paths = [] if packages_type == "git": exist_checkout_path = path.join(git_checkout_dir, exist[1]) exist_db_path = path.join(git_db_dir, exist[1]) exist_path = path.join(git_checkout_dir, exist_name) if exist[0] == "del": if os.path.isdir(exist_checkout_path): crate_paths.append(exist_checkout_path) if os.path.isdir(exist_db_path): crate_paths.append(exist_db_path) crate_count += -1 else: crate_paths.append(exist_path) exist_checkout_list = glob.glob(path.join(exist_checkout_path, '*')) if len(exist_checkout_list) <= 1: crate_paths.append(exist_checkout_path) if os.path.isdir(exist_db_path): crate_paths.append(exist_db_path) else: crate_paths.append(path.join(crates_cache_dir, "{}.crate".format(exist))) crate_paths.append(path.join(crates_src_dir, exist)) size = sum(get_size(p) for p in crate_paths) if show_size else 0 total_size += size print_msg = (exist_name, " ({}MB)".format(round(size, 2)) if show_size else "", cargo_dir) if force: print("Removing `{}`{} package from {}".format(*print_msg)) for crate_path in crate_paths: if os.path.exists(crate_path): try: delete(crate_path) except: print(traceback.format_exc()) print("Delete %s failed!" % crate_path) else: print("Would remove `{}`{} package from {}".format(*print_msg)) if removing_anything and show_size: print("\nTotal size of {} MB".format(round(total_size, 2))) if not removing_anything: print("Nothing to remove.") elif not force: print("\nNothing done. " "Run `./mach clean-cargo-cache -f` to actually remove.")