oranda/data/artifacts/
inference.rs

1use camino::Utf8PathBuf;
2
3use super::*;
4
5// Architectures
6// const ARCH_X86: &str = "i686";
7// const ARCH_X64: &str = "x86_64";
8// const ARCH_ARM64: &str = "aarch64";
9
10// OSes
11// const OS_WINDOWS: &str = "pc-windows-msvc";
12// const OS_LINUX_GNU: &str = "unknown-linux-gnu";
13// const OS_LINUX_MUSL: &str = "unknown-linux-musl";
14// const OS_MAC: &str = "apple-darwin";
15
16use axoproject::platforms::{KNOWN_LINUX_TARGETS, KNOWN_MAC_TARGETS, KNOWN_WINDOWS_TARGETS};
17
18// Various extensions for known archive formats
19const EXTS_FOR_TAR_BZIP2: &[&str] = &[".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2"];
20const EXTS_FOR_TAR_GZIP: &[&str] = &[".tar.gz", ".taz", ".tgz"];
21const EXTS_FOR_TAR_LZIP: &[&str] = &[".tar.lz"];
22const EXTS_FOR_TAR_LZMA: &[&str] = &[".tar.lzma", ".tlz"];
23const EXTS_FOR_TAR_XZ: &[&str] = &[".tar.xz", ".txz"];
24const EXTS_FOR_TAR_COMPRESS: &[&str] = &[".tar.Z", ".tZ", ".taZ"];
25const EXTS_FOR_TAR_ZSTD: &[&str] = &[".tar.zst", ".tzst"];
26const EXTS_FOR_TAR_BROTLI: &[&str] = &[".tar.br"];
27const EXTS_FOR_ZIP: &[&str] = &[".zip"];
28const EXTS_FOR_RAR: &[&str] = &[".rar"];
29const EXTS_FOR_7ZIP: &[&str] = &[".7z"];
30
31const KNOWN_ARCHIVE_EXTS: &[&[&str]] = &[
32    EXTS_FOR_TAR_BZIP2,
33    EXTS_FOR_TAR_GZIP,
34    EXTS_FOR_TAR_LZIP,
35    EXTS_FOR_TAR_LZMA,
36    EXTS_FOR_TAR_XZ,
37    EXTS_FOR_TAR_COMPRESS,
38    EXTS_FOR_TAR_ZSTD,
39    EXTS_FOR_TAR_BROTLI,
40    EXTS_FOR_ZIP,
41    EXTS_FOR_RAR,
42    EXTS_FOR_7ZIP,
43];
44
45// Various extensions for known "bundle" formats ("native installers")
46const EXT_BUNDLE_MSI: &str = ".msi";
47const EXT_BUNDLE_APP: &str = ".app";
48const EXT_BUNDLE_DMG: &str = ".dmg";
49const EXT_BUNDLE_DEB: &str = ".deb";
50const EXT_BUNDLE_RPM: &str = ".rpm";
51// annoying subtlety: pacman (arch linux) uses .pkg.tar.* files,
52// so we need to use "contains" instead of "ends_with" for bundles
53const EXT_BUNDLE_PACMAN: &str = ".pkg.tar.";
54const EXT_BUNDLE_FLATPAK: &str = ".flatpak";
55const EXT_BUNDLE_SNAP: &str = ".snap";
56
57const KNOWN_WINDOWS_BUNDLE_EXTS: &[&str] = &[EXT_BUNDLE_MSI];
58const KNOWN_MAC_BUNDLE_EXTS: &[&str] = &[EXT_BUNDLE_APP, EXT_BUNDLE_DMG];
59const KNOWN_LINUX_BUNDLE_EXTS: &[&str] = &[
60    EXT_BUNDLE_DMG,
61    EXT_BUNDLE_DEB,
62    EXT_BUNDLE_RPM,
63    EXT_BUNDLE_PACMAN,
64    EXT_BUNDLE_FLATPAK,
65    EXT_BUNDLE_SNAP,
66];
67const KNOWN_BUNDLE_EXTS: &[&str] = &[
68    EXT_BUNDLE_MSI,
69    EXT_BUNDLE_APP,
70    EXT_BUNDLE_DMG,
71    EXT_BUNDLE_DEB,
72    EXT_BUNDLE_RPM,
73    EXT_BUNDLE_FLATPAK,
74    EXT_BUNDLE_SNAP,
75    EXT_BUNDLE_PACMAN,
76];
77
78// Various extensions for
79const EXT_SCRIPT_SHELL: &str = ".sh";
80const EXT_SCRIPT_POWERSHELL: &str = ".ps1";
81// FIXME: could add windows' .bat..? or is that more like a bundle?
82
83const KNOWN_WINDOWS_SCRIPT_EXTS: &[&str] = &[EXT_SCRIPT_POWERSHELL];
84const KNOWN_UNIX_SCRIPT_EXTS: &[&str] = &[EXT_SCRIPT_SHELL];
85pub(crate) const KNOWN_SCRIPT_EXTS: &[&str] = &[EXT_SCRIPT_SHELL, EXT_SCRIPT_POWERSHELL];
86
87impl ReleaseArtifacts {
88    /// Infer installers/artifacts based solely on file names
89    pub fn add_inference(&mut self) {
90        // Gotta clone this upfront to avoid borrowing stuff
91        let app_name = self.app_name.clone();
92        for file_idx in self.file_indices() {
93            let file = self.file_mut(file_idx);
94            // Skip this
95            if !file.infer {
96                continue;
97            }
98            if let Some(app_name) = &app_name {
99                // If we're trying to restrict to a specific app, ignore files that don't contain
100                // the app name (future-proofing for multi-tenant oranda work)
101                if !file.name.contains(app_name) {
102                    continue;
103                }
104            }
105
106            // Search for target triples in the file name
107            let mut targets = vec![];
108            for target in KNOWN_TARGET_TRIPLES.iter().copied().flatten().copied() {
109                if file.name.contains(target) {
110                    targets.push(target.to_owned());
111                }
112            }
113
114            let label;
115            let description = String::new();
116            let method;
117            let preference;
118
119            // Try to detect what kind of file this is
120            if file.name.contains("install")
121                && KNOWN_SCRIPT_EXTS.iter().any(|ext| file.name.ends_with(ext))
122            {
123                // Looks like an installer script! Recommend a ~curl|sh for it.
124                //
125                // If this script doesn't have targets, infer them
126                if targets.is_empty() {
127                    targets = infer_targets_for_script(file);
128                }
129                let run_hint = infer_run_hint_for_script(file);
130                label = infer_label_for_script(file);
131                preference = InstallerPreference::Script;
132                method = InstallMethod::Run {
133                    file: Some(file_idx),
134                    run_hint,
135                };
136            } else if KNOWN_BUNDLE_EXTS.iter().any(|ext| file.name.ends_with(ext)) {
137                // Looks like an installer bundle! Recommend a download.
138                //
139                // NOTE: the above check is intentionally "contains" and not "ends_with" because
140                // arch packages are .pkg.tar.* and that's really annoying to handle.
141                //
142                // If this bundle doesn't have targets, infer them
143                if targets.is_empty() {
144                    targets = infer_targets_for_bundle(file);
145                }
146                label = infer_label_for_bundle(file);
147                preference = InstallerPreference::Native;
148                method = InstallMethod::Download { file: file_idx };
149            } else if KNOWN_ARCHIVE_EXTS
150                .iter()
151                .copied()
152                .flatten()
153                .any(|ext| file.name.ends_with(ext))
154            {
155                // Looks like this is an archive containing a binary! Recommend a download.
156                // Skip anything without a target triple, because we can't use it otherwise,
157                // and it might just be something like a source dump.
158                if targets.is_empty() {
159                    continue;
160                }
161                label = infer_label_for_archive(file);
162                preference = InstallerPreference::Archive;
163                method = InstallMethod::Download { file: file_idx };
164            } else {
165                // Nothing we recognize
166                continue;
167            }
168
169            let targets = preference_to_targets(targets, preference);
170            let installer = Installer {
171                label,
172                description,
173                app_name: self.app_name.clone(),
174                targets,
175                method,
176                display: DisplayPreference::Preferred,
177            };
178            self.add_installer(installer);
179        }
180    }
181}
182
183/// Given a file that appears to be a "bundle" but doesn't specify a target,
184/// infer the targets it applies to
185fn infer_targets_for_bundle(file: &File) -> Vec<TargetTriple> {
186    let mut targets = vec![];
187    if KNOWN_WINDOWS_BUNDLE_EXTS
188        .iter()
189        .any(|ext| file.name.contains(ext))
190    {
191        targets.extend(KNOWN_WINDOWS_TARGETS.iter().copied().map(|t| t.to_owned()));
192    }
193    if KNOWN_MAC_BUNDLE_EXTS
194        .iter()
195        .any(|ext| file.name.contains(ext))
196    {
197        targets.extend(KNOWN_MAC_TARGETS.iter().copied().map(|t| t.to_owned()));
198    }
199    if KNOWN_LINUX_BUNDLE_EXTS
200        .iter()
201        .any(|ext| file.name.contains(ext))
202    {
203        targets.extend(
204            KNOWN_LINUX_TARGETS
205                .iter()
206                .copied()
207                .flatten()
208                .copied()
209                .map(|t| t.to_owned()),
210        );
211    }
212    targets
213}
214
215/// Given a file that appears to be a "script" but doesn't specify a target, infer the targets it applies to
216fn infer_targets_for_script(file: &File) -> Vec<TargetTriple> {
217    let mut targets = vec![];
218    if KNOWN_WINDOWS_SCRIPT_EXTS
219        .iter()
220        .any(|ext| file.name.contains(ext))
221    {
222        targets.extend(KNOWN_WINDOWS_TARGETS.iter().copied().map(|t| t.to_owned()));
223    }
224    if KNOWN_UNIX_SCRIPT_EXTS
225        .iter()
226        .any(|ext| file.name.contains(ext))
227    {
228        targets.extend(
229            KNOWN_LINUX_TARGETS
230                .iter()
231                .copied()
232                .flatten()
233                .copied()
234                .map(|t| t.to_owned()),
235        );
236        targets.extend(KNOWN_MAC_TARGETS.iter().copied().map(|t| t.to_owned()));
237    }
238    targets
239}
240
241/// Infer the command to curl|sh a script
242fn infer_run_hint_for_script(file: &File) -> String {
243    if file.name.ends_with(EXT_SCRIPT_SHELL) {
244        format!(
245            "curl --proto '=https' --tlsv1.2 -LsSf {} | sh",
246            file.download_url
247        )
248    } else if file.name.ends_with(EXT_SCRIPT_POWERSHELL) {
249        format!(r#"powershell -c "irm {} | iex""#, file.download_url)
250    } else {
251        unimplemented!(
252            "Looks like someone added a new kind of script but didn't add a run hint for it?"
253        );
254    }
255}
256
257/// Infer the label for a bundle
258fn infer_label_for_bundle(file: &File) -> String {
259    // For now just use the extension
260    Utf8PathBuf::from(&file.name)
261        .extension()
262        .expect("we determined a file was a bundle based on its extension, but it had none?")
263        .to_owned()
264}
265
266/// Infer the label for a tarball/zip
267fn infer_label_for_archive(file: &File) -> String {
268    // For now just use the extension
269    if EXTS_FOR_RAR.iter().any(|ext| file.name.ends_with(ext)) {
270        "rar".to_owned()
271    } else if EXTS_FOR_7ZIP.iter().any(|ext| file.name.ends_with(ext)) {
272        "7zip".to_owned()
273    } else if EXTS_FOR_ZIP.iter().any(|ext| file.name.ends_with(ext)) {
274        "zip".to_owned()
275    } else {
276        "tarball".to_owned()
277    }
278}
279
280/// Infer the label to curl|sh a script
281fn infer_label_for_script(file: &File) -> String {
282    if file.name.ends_with(EXT_SCRIPT_POWERSHELL) {
283        "powershell".to_owned()
284    } else if file.name.ends_with(EXT_SCRIPT_SHELL) {
285        "shell".to_owned()
286    } else {
287        Utf8PathBuf::from(&file.name)
288            .extension()
289            .expect("we determined a file was a script based on its extension, but it had none?")
290            .to_owned()
291    }
292}