android_sdkmanager/
lib.rs1use rayon::prelude::*;
2use std::collections::HashSet;
3use std::io::Cursor;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use zip::ZipArchive;
7
8#[derive(Default, Debug)]
9struct AndroidPackage {
10 archives: Vec<AndroidArchive>,
11 dependencies: Vec<String>,
12}
13
14#[derive(Default, Debug)]
15struct AndroidArchive {
16 host_os: String,
17 url: String,
18}
19
20fn list_archives<'a>(
21 root_url: &str,
22 archives_node: &'a roxmltree::Node<'a, 'a>,
23) -> Vec<AndroidArchive> {
24 let mut packages = vec![];
25
26 for archive in archives_node.children() {
27 if archive.has_tag_name("archive") {
28 let mut package = AndroidArchive::default();
29 for host_os in archive.children() {
30 if host_os.has_tag_name("host-os") {
31 package.host_os = host_os.text().unwrap().to_string();
32 }
33 }
34
35 for complete in archive.children() {
36 if complete.has_tag_name("complete") {
37 for url in complete.children() {
38 if url.has_tag_name("url") {
39 package.url = format!("{}{}", root_url, url.text().unwrap());
40 break;
41 }
42 }
43 break;
44 }
45 }
46
47 packages.push(package);
48 }
49 }
50
51 packages
52}
53
54fn list_dependencies<'a>(dependencies_node: &'a roxmltree::Node<'a, 'a>) -> Vec<String> {
55 let mut dependency_paths = vec![];
56
57 for dependency in dependencies_node.children() {
58 if dependency.has_tag_name("dependency") {
59 dependency_paths.push(dependency.attribute("path").unwrap().to_owned());
60 }
61 }
62
63 dependency_paths
64}
65
66fn find_remote_package_by_name<'a>(
67 doc: &'a roxmltree::Document,
68 root_url: &str,
69 package_name: &str,
70) -> AndroidPackage {
71 let mut android_package = AndroidPackage::default();
72
73 for dec in doc.descendants() {
74 if dec.has_tag_name("remotePackage") && dec.attribute("path") == Some(package_name) {
75 for child in dec.children() {
76 if child.has_tag_name("archives") {
77 android_package.archives = list_archives(root_url, &child);
78 }
79
80 if child.has_tag_name("dependencies") {
81 android_package.dependencies = list_dependencies(&child);
82 }
83 }
84
85 break;
86 }
87 }
88
89 android_package
90}
91
92fn download_android_sdk_archive(package: &AndroidArchive) -> ZipArchive<Cursor<Box<[u8]>>> {
93 let mut response = ureq::get(&package.url).call().unwrap().into_reader();
94 let mut data = vec![];
95 response.read_to_end(&mut data).unwrap();
96 ZipArchive::new(Cursor::new(data.into_boxed_slice())).unwrap()
97}
98
99fn recurse_dependency_tree<'a>(
100 doc: &roxmltree::Document<'a>,
101 root_url: &str,
102 package: &str,
103 output: &mut HashSet<String>,
104) {
105 output.insert(package.to_owned());
106
107 let packages = find_remote_package_by_name(doc, root_url, package);
108 for dep in packages.dependencies {
109 recurse_dependency_tree(doc, root_url, &dep, output);
110 output.insert(dep);
111 }
112}
113
114fn androidolize_zipfile_paths(zip_path: &Path, new_roots: &Path) -> PathBuf {
117 let mut path_buf = PathBuf::new();
118 for (idx, component) in zip_path.components().enumerate() {
119 if idx == 0 {
120 for root_comp in new_roots.components() {
121 path_buf.push(root_comp);
122 }
123 } else {
124 path_buf.push(component)
125 }
126 }
127
128 path_buf
129}
130
131fn is_allowed(path: &Path, allow_list: Option<&[MatchType]>) -> bool {
132 if let Some(allow_list) = allow_list {
133 for check in allow_list {
134 match check {
135 MatchType::Partial(check) => {
136 if let Some(file_stem) = path.file_stem() {
137 if file_stem.to_str().unwrap().contains(check) {
138 return true;
139 }
140 }
141 }
142 MatchType::EntireStem(check) => {
143 if let Some(file_stem) = path.file_stem() {
144 if &file_stem.to_str().unwrap() == check {
145 return true;
146 }
147 }
148 }
149 MatchType::EntireName(check) => {
150 if let Some(file_stem) = path.file_name() {
151 if &file_stem.to_str().unwrap() == check {
152 return true;
153 }
154 }
155 }
156 MatchType::EntireFolder(check) => {
157 if let Some(path) = path.to_str() {
158 if path.contains(check) {
159 return true;
160 }
161 }
162 }
163 }
164 }
165
166 false
167 } else {
168 true
169 }
170}
171
172const S_IFLNK: u32 = 0o120000; fn is_symlink(file: &zip::read::ZipFile) -> bool {
175 if let Some(mode) = file.unix_mode() {
176 if mode & S_IFLNK == S_IFLNK {
177 return true;
178 }
179 }
180
181 false
182}
183
184pub fn download_and_extract_packages(
185 install_dir: &str,
186 host_os: HostOs,
187 download_packages: &[&str],
188 allow_list: Option<&[MatchType]>,
189) {
190 let root_url = "https://dl.google.com/android/repository/";
191 let packages = ureq::get(&format!("{}/repository2-1.xml", root_url))
192 .call()
193 .unwrap()
194 .into_string()
195 .unwrap();
196
197 let doc = roxmltree::Document::parse(&packages).unwrap();
198
199 let mut all_dependencies = HashSet::new();
201 for package in download_packages {
202 recurse_dependency_tree(&doc, root_url, package, &mut all_dependencies);
203 }
204 let mut archives = vec![];
205
206 for package_name in all_dependencies {
207 let package = find_remote_package_by_name(&doc, root_url, &package_name);
208
209 for archive in package.archives {
210 if archive.host_os.contains(host_os.to_str()) || archive.host_os.is_empty() {
211 println!("Downloading `{}`", &package_name);
212 archives.push((package_name.clone(), archive));
213 }
214 }
215 }
216
217 let mut zip_archives = archives
218 .par_iter()
219 .map(|(package_name, archive)| (package_name, download_android_sdk_archive(archive)))
220 .collect::<Vec<_>>();
221
222 zip_archives
223 .par_iter_mut()
224 .for_each(|(package_name, zip_archive)| {
225 println!("Extracting `{}`", package_name);
226 for i in 0..zip_archive.len() {
227 let mut file = zip_archive.by_index(i).unwrap();
228 let filepath = file.enclosed_name().unwrap();
229
230 let outpath = PathBuf::from(install_dir).join(androidolize_zipfile_paths(
231 filepath,
232 Path::new(&package_name.replace(';', "/")),
233 ));
234
235 if is_allowed(filepath, allow_list) {
236 if file.name().ends_with('/') {
237 std::fs::create_dir_all(&outpath).unwrap();
238 } else {
239 if let Some(p) = outpath.parent() {
240 if !p.exists() {
241 std::fs::create_dir_all(&p).unwrap();
242 }
243 }
244
245 if is_symlink(&file) {
247 let mut contents = Vec::new();
248 file.read_to_end(&mut contents).unwrap();
249
250 #[cfg(target_family = "unix")]
251 {
252 use std::os::unix::ffi::OsStringExt as _;
254
255 let link_path = std::path::PathBuf::from(
256 std::ffi::OsString::from_vec(contents),
257 );
258 std::os::unix::fs::symlink(link_path, &outpath).unwrap();
259 }
260
261 #[cfg(target_family = "windows")]
262 {
263 let link_path = String::from_utf8(contents).unwrap();
265 std::os::windows::fs::symlink_file(link_path, &outpath).unwrap();
266 }
267 } else {
268 let mut outfile = std::fs::File::create(&outpath).unwrap();
269 std::io::copy(&mut file, &mut outfile).unwrap();
270
271 #[cfg(unix)]
272 {
273 use std::os::unix::fs::PermissionsExt;
274 if let Some(mode) = file.unix_mode() {
275 std::fs::set_permissions(
276 &outpath,
277 std::fs::Permissions::from_mode(mode),
278 )
279 .unwrap();
280 }
281 }
282 }
283 }
284 }
285 }
286 });
287}
288
289pub enum MatchType {
290 Partial(&'static str),
291 EntireStem(&'static str),
292 EntireName(&'static str),
293 EntireFolder(&'static str),
294}
295
296#[derive(Copy, Clone)]
297pub enum HostOs {
298 Windows,
299 MacOs,
300 Linux,
301}
302
303impl HostOs {
304 fn to_str(self) -> &'static str {
305 match self {
306 HostOs::Windows => "windows",
307 HostOs::Linux => "linux",
308 HostOs::MacOs => "macosx",
309 }
310 }
311}