2 # Simpler reimplementation of Android's sdkmanager
3 # Extra features of this implementation are pinning and mirroring
5 # These URLs are the Google repositories containing the list of available
6 # packages and their versions. The list has been generated by listing the URLs
7 # fetched while executing `tools/bin/sdkmanager --list`
8 BASE_REPOSITORY = "https://dl.google.com/android/repository/"
10 "sys-img/android/sys-img2-1.xml",
11 "sys-img/android-wear/sys-img2-1.xml",
12 "sys-img/android-wear-cn/sys-img2-1.xml",
13 "sys-img/android-tv/sys-img2-1.xml",
14 "sys-img/google_apis/sys-img2-1.xml",
15 "sys-img/google_apis_playstore/sys-img2-1.xml",
18 "extras/intel/addon2-1.xml",
22 # Available hosts: linux, macosx and windows
26 MIRROR_BUCKET = "rust-lang-ci-mirrors"
27 MIRROR_BUCKET_REGION = "us-west-1"
28 MIRROR_BASE_DIR = "rustc/android/"
37 import xml.etree.ElementTree as ET
40 def __init__(self, path, url, sha1, deps=None):
43 self.path = path.strip()
44 self.url = url.strip()
45 self.sha1 = sha1.strip()
48 def download(self, base_url):
49 _, file = tempfile.mkstemp()
50 url = base_url + self.url
51 subprocess.run(["curl", "-o", file, url], check=True)
52 # Ensure there are no hash mismatches
53 with open(file, "rb") as f:
54 sha1 = hashlib.sha1(f.read()).hexdigest()
57 "hash mismatch for package " + self.path + ": " +
58 sha1 + " vs " + self.sha1 + " (known good)"
63 return "<Package "+self.path+" at "+self.url+" (sha1="+self.sha1+")"
66 page = urllib.request.urlopen(url)
69 def fetch_repository(base, repo_url):
71 root = ET.fromstring(fetch_url(base + repo_url))
73 if package.tag != "remotePackage":
75 path = package.attrib["path"]
77 for archive in package.find("archives"):
78 host_os = archive.find("host-os")
79 if host_os is not None and host_os.text != HOST_OS:
81 complete = archive.find("complete")
82 url = os.path.join(os.path.dirname(repo_url), complete.find("url").text)
83 sha1 = complete.find("checksum").text
86 dependencies = package.find("dependencies")
87 if dependencies is not None:
88 for dep in dependencies:
89 deps.append(dep.attrib["path"])
91 packages[path] = Package(path, url, sha1, deps)
96 def fetch_repositories():
98 for repo in REPOSITORIES:
99 packages.update(fetch_repository(BASE_REPOSITORY, repo))
103 def __init__(self, path):
106 if os.path.exists(path):
107 with open(path) as f:
109 path, url, sha1 = line.split(" ")
110 self.packages[path] = Package(path, url, sha1)
112 def add(self, packages, name, *, update=True):
113 if name not in packages:
114 raise NameError("package not found: " + name)
115 if not update and name in self.packages:
117 self.packages[name] = packages[name]
118 for dep in packages[name].deps:
119 self.add(packages, dep, update=False)
122 packages = list(sorted(self.packages.values(), key=lambda p: p.path))
123 with open(self.path, "w") as f:
124 for package in packages:
125 f.write(package.path + " " + package.url + " " + package.sha1 + "\n")
127 def cli_add_to_lockfile(args):
128 lockfile = Lockfile(args.lockfile)
129 packages = fetch_repositories()
130 for package in args.packages:
131 lockfile.add(packages, package)
134 def cli_update_mirror(args):
135 lockfile = Lockfile(args.lockfile)
136 for package in lockfile.packages.values():
137 path = package.download(BASE_REPOSITORY)
139 "aws", "s3", "mv", path,
140 "s3://" + MIRROR_BUCKET + "/" + MIRROR_BASE_DIR + package.url,
141 "--profile=" + args.awscli_profile,
144 def cli_install(args):
145 lockfile = Lockfile(args.lockfile)
146 for package in lockfile.packages.values():
147 # Download the file from the mirror into a temp file
148 url = "https://" + MIRROR_BUCKET + ".s3-" + MIRROR_BUCKET_REGION + \
149 ".amazonaws.com/" + MIRROR_BASE_DIR
150 downloaded = package.download(url)
151 # Extract the file in a temporary directory
152 extract_dir = tempfile.mkdtemp()
154 "unzip", "-q", downloaded, "-d", extract_dir,
156 # Figure out the prefix used in the zip
157 subdirs = [d for d in os.listdir(extract_dir) if not d.startswith(".")]
158 if len(subdirs) != 1:
159 raise RuntimeError("extracted directory contains more than one dir")
160 # Move the extracted files in the proper directory
161 dest = os.path.join(args.dest, package.path.replace(";", "/"))
162 os.makedirs("/".join(dest.split("/")[:-1]), exist_ok=True)
163 os.rename(os.path.join(extract_dir, subdirs[0]), dest)
164 os.unlink(downloaded)
167 parser = argparse.ArgumentParser()
168 subparsers = parser.add_subparsers()
170 add_to_lockfile = subparsers.add_parser("add-to-lockfile")
171 add_to_lockfile.add_argument("lockfile")
172 add_to_lockfile.add_argument("packages", nargs="+")
173 add_to_lockfile.set_defaults(func=cli_add_to_lockfile)
175 update_mirror = subparsers.add_parser("update-mirror")
176 update_mirror.add_argument("lockfile")
177 update_mirror.add_argument("--awscli-profile", default="default")
178 update_mirror.set_defaults(func=cli_update_mirror)
180 install = subparsers.add_parser("install")
181 install.add_argument("lockfile")
182 install.add_argument("dest")
183 install.set_defaults(func=cli_install)
185 args = parser.parse_args()
186 if not hasattr(args, "func"):
187 print("error: a subcommand is required (see --help)")
191 if __name__ == "__main__":