android_sdkmanager/
lib.rs

1use 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
114// instead of the path in the zip file, android sdk expects something slightly more
115// elaborate, based on the package name & version
116fn 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; // symbolic link
173
174fn 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    // make a flat list of all packages and their dependencies
200    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                        // adapted from https://github.com/zip-rs/zip/issues/77
246                        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                                // Needed to be able to call `OsString::from_vec(Vec<u8>)`
253                                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                                // TODO: Support non-UTF-8 paths (currently only works for paths which are valid UTF-8)
264                                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}