// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use std::{borrow::Cow, collections::HashMap}; use camino::{Utf8Path, Utf8PathBuf}; use crate::{ action::BuildAction, cargo::{CargoBuild, RustOutput}, glob, input::BuildInput, inputs, Build, Result, }; #[derive(Clone, Copy, Debug)] pub struct OnlineArchive { pub url: &'static str, pub sha256: &'static str, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Platform { LinuxX64, LinuxArm, MacX64, MacArm, WindowsX64, } impl Platform { pub fn current() -> Self { if cfg!(windows) { Self::WindowsX64 } else { let os = std::env::consts::OS; let arch = std::env::consts::ARCH; match (os, arch) { ("linux", "x86_64") => Self::LinuxX64, ("linux", "aarch64") => Self::LinuxArm, ("macos", "x86_64") => Self::MacX64, ("macos", "aarch64") => Self::MacArm, _ => panic!("unsupported os/arch {os} {arch} - PR welcome!"), } } } pub fn tls_feature() -> &'static str { match Self::current() { // On Linux, wheels are not allowed to link to OpenSSL, and linking setup // caused pain for AnkiDroid in the past. On other platforms, we stick to // native libraries, for smaller binaries. Platform::LinuxX64 | Platform::LinuxArm => "rustls", _ => "native-tls", } } pub fn as_rust_triple(&self) -> &'static str { match self { Platform::MacX64 => "x86_64-apple-darwin", _ => unimplemented!(), } } } /// Append .exe to path if on Windows. pub fn with_exe(path: &str) -> Cow { if cfg!(windows) { format!("{path}.exe").into() } else { path.into() } } struct DownloadArchive { pub archive: OnlineArchive, } impl BuildAction for DownloadArchive { fn command(&self) -> &str { "$archives_bin download $url $checksum $out" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { let (_, filename) = self.archive.url.rsplit_once('/').unwrap(); let output_path = Utf8Path::new("download").join(filename); build.add_inputs("archives_bin", inputs![":build:archives"]); build.add_variable("url", self.archive.url); build.add_variable("checksum", self.archive.sha256); build.add_outputs("out", &[output_path.into_string()]) } fn on_first_instance(&self, build: &mut Build) -> Result<()> { build_archive_tool(build) } } struct ExtractArchive<'a, I> { pub archive_path: BuildInput, /// The folder that the archive should be extracted into, relative to $builddir/extracted. /// If the archive contains a single top-level folder, its contents will be extracted into the /// provided folder, so that output like tool-1.2/ can be extracted into tool/. pub extraction_folder_name: &'a str, /// Files contained inside the archive, relative to the archive root, and excluding the top-level /// folder if it is the sole top-level entry. Any files you wish to use as part of subsequent rules /// must be declared here. pub file_manifest: HashMap<&'static str, I>, } impl ExtractArchive<'_, I> { fn extraction_folder(&self) -> Utf8PathBuf { Utf8Path::new("$builddir") .join("extracted") .join(self.extraction_folder_name) } } impl BuildAction for ExtractArchive<'_, I> where I: IntoIterator, I::Item: AsRef, { fn command(&self) -> &str { "$archive_tool extract $in $extraction_folder" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { build.add_inputs("archive_tool", inputs![":build:archives"]); build.add_inputs("in", inputs![self.archive_path.clone()]); let folder = self.extraction_folder(); build.add_variable("extraction_folder", folder.to_string()); for (subgroup, files) in self.file_manifest.drain() { build.add_outputs_ext( subgroup, files .into_iter() .map(|f| folder.join(f.as_ref()).to_string()), !subgroup.is_empty(), ); } build.add_output_stamp(folder.with_extension("marker")); } fn on_first_instance(&self, build: &mut Build) -> Result<()> { build_archive_tool(build) } fn name(&self) -> &'static str { "extract" } } fn build_archive_tool(build: &mut Build) -> Result<()> { build.once_only("build_archive_tool", |build| { let features = Platform::tls_feature(); build.add( "build:archives", CargoBuild { inputs: inputs![glob!("build/archives/**/*")], outputs: &[RustOutput::Binary("archives")], target: None, extra_args: &format!("-p archives --features {features}"), release_override: Some(false), }, )?; Ok(()) }) } /// See [DownloadArchive] and [ExtractArchive]. pub fn download_and_extract( build: &mut Build, group_name: &str, archive: OnlineArchive, file_manifest: HashMap<&'static str, I>, ) -> Result<()> where I: IntoIterator, I::Item: AsRef, { let download_group = format!("download:{group_name}"); build.add(&download_group, DownloadArchive { archive })?; let extract_group = format!("extract:{group_name}"); build.add( extract_group, ExtractArchive { archive_path: inputs![format!(":{download_group}")], extraction_folder_name: group_name, file_manifest, }, )?; Ok(()) } pub fn empty_manifest() -> HashMap<&'static str, &'static [&'static str]> { Default::default() }