| #!/usr/bin/env python3 |
| # Simpler reimplementation of Android's sdkmanager |
| # Extra features of this implementation are pinning and mirroring |
| |
| import argparse |
| import hashlib |
| import os |
| import subprocess |
| import tempfile |
| import urllib.request |
| import xml.etree.ElementTree as ET |
| |
| # These URLs are the Google repositories containing the list of available |
| # packages and their versions. The list has been generated by listing the URLs |
| # fetched while executing `tools/bin/sdkmanager --list` |
| BASE_REPOSITORY = "https://dl.google.com/android/repository/" |
| REPOSITORIES = [ |
| "sys-img/android/sys-img2-1.xml", |
| "sys-img/android-wear/sys-img2-1.xml", |
| "sys-img/android-wear-cn/sys-img2-1.xml", |
| "sys-img/android-tv/sys-img2-1.xml", |
| "sys-img/google_apis/sys-img2-1.xml", |
| "sys-img/google_apis_playstore/sys-img2-1.xml", |
| "addon2-1.xml", |
| "glass/addon2-1.xml", |
| "extras/intel/addon2-1.xml", |
| "repository2-1.xml", |
| ] |
| |
| # Available hosts: linux, macosx and windows |
| HOST_OS = "linux" |
| |
| # Mirroring options |
| MIRROR_BUCKET = "rust-lang-ci-mirrors" |
| MIRROR_BUCKET_REGION = "us-west-1" |
| MIRROR_BASE_DIR = "rustc/android/" |
| |
| class Package: |
| def __init__(self, path, url, sha1, deps=None): |
| if deps is None: |
| deps = [] |
| self.path = path.strip() |
| self.url = url.strip() |
| self.sha1 = sha1.strip() |
| self.deps = deps |
| |
| def download(self, base_url): |
| _, file = tempfile.mkstemp() |
| url = base_url + self.url |
| subprocess.run(["curl", "-o", file, url], check=True) |
| # Ensure there are no hash mismatches |
| with open(file, "rb") as f: |
| sha1 = hashlib.sha1(f.read()).hexdigest() |
| if sha1 != self.sha1: |
| raise RuntimeError( |
| "hash mismatch for package " + self.path + ": " + |
| sha1 + " vs " + self.sha1 + " (known good)" |
| ) |
| return file |
| |
| def __repr__(self): |
| return "<Package "+self.path+" at "+self.url+" (sha1="+self.sha1+")" |
| |
| def fetch_url(url): |
| page = urllib.request.urlopen(url) |
| return page.read() |
| |
| def fetch_repository(base, repo_url): |
| packages = {} |
| root = ET.fromstring(fetch_url(base + repo_url)) |
| for package in root: |
| if package.tag != "remotePackage": |
| continue |
| path = package.attrib["path"] |
| |
| for archive in package.find("archives"): |
| host_os = archive.find("host-os") |
| if host_os is not None and host_os.text != HOST_OS: |
| continue |
| complete = archive.find("complete") |
| url = os.path.join(os.path.dirname(repo_url), complete.find("url").text) |
| sha1 = complete.find("checksum").text |
| |
| deps = [] |
| dependencies = package.find("dependencies") |
| if dependencies is not None: |
| for dep in dependencies: |
| deps.append(dep.attrib["path"]) |
| |
| packages[path] = Package(path, url, sha1, deps) |
| break |
| |
| return packages |
| |
| def fetch_repositories(): |
| packages = {} |
| for repo in REPOSITORIES: |
| packages.update(fetch_repository(BASE_REPOSITORY, repo)) |
| return packages |
| |
| class Lockfile: |
| def __init__(self, path): |
| self.path = path |
| self.packages = {} |
| if os.path.exists(path): |
| with open(path) as f: |
| for line in f: |
| path, url, sha1 = line.split(" ") |
| self.packages[path] = Package(path, url, sha1) |
| |
| def add(self, packages, name, *, update=True): |
| if name not in packages: |
| raise NameError("package not found: " + name) |
| if not update and name in self.packages: |
| return |
| self.packages[name] = packages[name] |
| for dep in packages[name].deps: |
| self.add(packages, dep, update=False) |
| |
| def save(self): |
| packages = list(sorted(self.packages.values(), key=lambda p: p.path)) |
| with open(self.path, "w") as f: |
| for package in packages: |
| f.write(package.path + " " + package.url + " " + package.sha1 + "\n") |
| |
| def cli_add_to_lockfile(args): |
| lockfile = Lockfile(args.lockfile) |
| packages = fetch_repositories() |
| for package in args.packages: |
| lockfile.add(packages, package) |
| lockfile.save() |
| |
| def cli_update_mirror(args): |
| lockfile = Lockfile(args.lockfile) |
| for package in lockfile.packages.values(): |
| path = package.download(BASE_REPOSITORY) |
| subprocess.run([ |
| "aws", "s3", "mv", path, |
| "s3://" + MIRROR_BUCKET + "/" + MIRROR_BASE_DIR + package.url, |
| "--profile=" + args.awscli_profile, |
| ], check=True) |
| |
| def cli_install(args): |
| lockfile = Lockfile(args.lockfile) |
| for package in lockfile.packages.values(): |
| # Download the file from the mirror into a temp file |
| url = "https://" + MIRROR_BUCKET + ".s3-" + MIRROR_BUCKET_REGION + \ |
| ".amazonaws.com/" + MIRROR_BASE_DIR |
| downloaded = package.download(url) |
| # Extract the file in a temporary directory |
| extract_dir = tempfile.mkdtemp() |
| subprocess.run([ |
| "unzip", "-q", downloaded, "-d", extract_dir, |
| ], check=True) |
| # Figure out the prefix used in the zip |
| subdirs = [d for d in os.listdir(extract_dir) if not d.startswith(".")] |
| if len(subdirs) != 1: |
| raise RuntimeError("extracted directory contains more than one dir") |
| # Move the extracted files in the proper directory |
| dest = os.path.join(args.dest, package.path.replace(";", "/")) |
| os.makedirs("/".join(dest.split("/")[:-1]), exist_ok=True) |
| os.rename(os.path.join(extract_dir, subdirs[0]), dest) |
| os.unlink(downloaded) |
| |
| def cli(): |
| parser = argparse.ArgumentParser() |
| subparsers = parser.add_subparsers() |
| |
| add_to_lockfile = subparsers.add_parser("add-to-lockfile") |
| add_to_lockfile.add_argument("lockfile") |
| add_to_lockfile.add_argument("packages", nargs="+") |
| add_to_lockfile.set_defaults(func=cli_add_to_lockfile) |
| |
| update_mirror = subparsers.add_parser("update-mirror") |
| update_mirror.add_argument("lockfile") |
| update_mirror.add_argument("--awscli-profile", default="default") |
| update_mirror.set_defaults(func=cli_update_mirror) |
| |
| install = subparsers.add_parser("install") |
| install.add_argument("lockfile") |
| install.add_argument("dest") |
| install.set_defaults(func=cli_install) |
| |
| args = parser.parse_args() |
| if not hasattr(args, "func"): |
| print("error: a subcommand is required (see --help)") |
| exit(1) |
| args.func(args) |
| |
| if __name__ == "__main__": |
| cli() |