From f888d5d99c873bf87e9fca6e40a35a3bd4151ab4 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Fri, 3 Mar 2023 16:32:51 +0100 Subject: [PATCH] apk: Embed configurable assets in `asset/` directory Android apps might want to ship with extra files - assets - to complement the native build. These could be single files or folders that are included recursively. the `file_name()` of the given path is added directly to the `assets/` directory, and any directory structure beyond that point is maintained. Assets are specified in the following way in `manifest.yaml`: android: assets: - some/path - { path: some/path } # Same as above - { path: some/path, optional: true } # Do not error if this path doesn't exist - { path: some/path, alignment: 4096 } # Page-aligned for optimal use with AASSET_MODE_BUFFER and AAsset_getBuffer - { path: some/path, alignment: unaligned } - { path: some/path, alignment: compressed } --- apk/src/lib.rs | 29 +++++++------ msix/src/lib.rs | 9 +++- xbuild/src/command/build.rs | 9 ++++ xbuild/src/config.rs | 87 +++++++++++++++++++++++++++++++++++++ xcommon/src/lib.rs | 22 +++++++--- 5 files changed, 134 insertions(+), 22 deletions(-) diff --git a/apk/src/lib.rs b/apk/src/lib.rs index 870e03c0..04d54732 100644 --- a/apk/src/lib.rs +++ b/apk/src/lib.rs @@ -3,7 +3,7 @@ use crate::res::Chunk; use anyhow::{Context, Result}; use std::io::Cursor; use std::path::{Path, PathBuf}; -use xcommon::{Scaler, ScalerOpts, Zip, ZipFile, ZipFileOptions}; +use xcommon::{Scaler, ScalerOpts, Zip, ZipFileOptions}; mod compiler; pub mod manifest; @@ -77,16 +77,27 @@ impl Apk { Ok(()) } + pub fn add_asset(&mut self, asset: &Path, opts: ZipFileOptions) -> Result<()> { + let file_name = asset + .file_name() + .context("Asset must have file_name component")?; + let dest = Path::new("assets").join(file_name); + if asset.is_dir() { + tracing::info!("Embedding asset directory `{}`", asset.display()); + self.zip.add_directory(asset, &dest, opts) + } else { + tracing::info!("Embedding asset file `{}`", asset.display()); + self.zip.add_file(asset, &dest, opts) + } + .with_context(|| format!("While embedding asset `{}`", asset.display())) + } + pub fn add_dex(&mut self, dex: &Path) -> Result<()> { self.zip .add_file(dex, Path::new("classes.dex"), ZipFileOptions::Compressed)?; Ok(()) } - pub fn add_zip_file(&mut self, f: ZipFile) -> Result<()> { - self.zip.add_zip_file(f) - } - pub fn add_lib(&mut self, target: Target, path: &Path) -> Result<()> { let name = path .file_name() @@ -100,14 +111,6 @@ impl Apk { ) } - pub fn add_file(&mut self, source: &Path, dest: &Path, opts: ZipFileOptions) -> Result<()> { - self.zip.add_file(source, dest, opts) - } - - pub fn add_directory(&mut self, source: &Path, dest: &Path) -> Result<()> { - self.zip.add_directory(source, dest) - } - pub fn finish(self, signer: Option) -> Result<()> { self.zip.finish()?; crate::sign::sign(&self.path, signer)?; diff --git a/msix/src/lib.rs b/msix/src/lib.rs index 0f618390..52cf3d24 100644 --- a/msix/src/lib.rs +++ b/msix/src/lib.rs @@ -73,8 +73,13 @@ impl Msix { self.zip.add_file(source, dest, opts) } - pub fn add_directory(&mut self, source: &Path, dest: &Path) -> Result<()> { - self.zip.add_directory(source, dest) + pub fn add_directory( + &mut self, + source: &Path, + dest: &Path, + opts: ZipFileOptions, + ) -> Result<()> { + self.zip.add_directory(source, dest, opts) } pub fn finish(mut self, signer: Option) -> Result<()> { diff --git a/xbuild/src/command/build.rs b/xbuild/src/command/build.rs index db9fd29f..9e7020f7 100644 --- a/xbuild/src/command/build.rs +++ b/xbuild/src/command/build.rs @@ -83,6 +83,14 @@ pub fn build(env: &BuildEnv) -> Result<()> { )?; apk.add_res(env.icon(), &env.android_jar())?; + for asset in &env.config().android().assets { + let path = env.cargo().package_root().join(asset.path()); + + if !asset.optional() || path.exists() { + apk.add_asset(&path, asset.alignment().to_zip_file_options())? + } + } + if has_lib { for target in env.target().compile_targets() { let arch_dir = platform_dir.join(target.arch().to_string()); @@ -256,6 +264,7 @@ pub fn build(env: &BuildEnv) -> Result<()> { ipa.add_directory( &app, &Path::new("Payload").join(format!("{}.app", env.name())), + ZipFileOptions::Compressed, )?; ipa.finish()?; } diff --git a/xbuild/src/config.rs b/xbuild/src/config.rs index cc93fdfd..82540acf 100644 --- a/xbuild/src/config.rs +++ b/xbuild/src/config.rs @@ -8,6 +8,7 @@ use msix::AppxManifest; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use xcommon::ZipFileOptions; #[derive(Clone, Debug, Default)] pub struct Config { @@ -313,7 +314,84 @@ impl Config { } } +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UnalignedCompressed { + /// Don't align this file + Unaligned, + /// Compressed files do not need to be aligned, as they have to be unpacked and decompressed anyway + #[default] + Compressed, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(untagged)] +pub enum ZipAlignmentOptions { + /// Align this file to the given number of bytes + Aligned(u16), + /// Used to wrap a tagged enum with an untagged alignment value + UnalignedCompressed(UnalignedCompressed), +} + +impl Default for ZipAlignmentOptions { + fn default() -> Self { + Self::UnalignedCompressed(UnalignedCompressed::Compressed) + } +} + +impl ZipAlignmentOptions { + pub fn to_zip_file_options(self) -> ZipFileOptions { + match self { + Self::Aligned(a) => ZipFileOptions::Aligned(a), + Self::UnalignedCompressed(UnalignedCompressed::Unaligned) => ZipFileOptions::Unaligned, + Self::UnalignedCompressed(UnalignedCompressed::Compressed) => { + ZipFileOptions::Compressed + } + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub enum AssetPath { + Path(PathBuf), + Extended { + path: PathBuf, + #[serde(default)] + optional: bool, + #[serde(default)] + alignment: ZipAlignmentOptions, + }, +} + +impl AssetPath { + #[inline] + pub fn path(&self) -> &Path { + match self { + AssetPath::Path(path) => path, + AssetPath::Extended { path, .. } => path, + } + } + + #[inline] + pub fn optional(&self) -> bool { + match self { + AssetPath::Path(_) => false, + AssetPath::Extended { optional, .. } => *optional, + } + } + + #[inline] + pub fn alignment(&self) -> ZipAlignmentOptions { + match self { + AssetPath::Path(_) => Default::default(), + AssetPath::Extended { alignment, .. } => *alignment, + } + } +} + #[derive(Deserialize)] +#[serde(deny_unknown_fields)] struct RawConfig { #[serde(flatten)] generic: Option, @@ -325,6 +403,7 @@ struct RawConfig { } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct GenericConfig { icon: Option, #[serde(default)] @@ -332,6 +411,7 @@ pub struct GenericConfig { } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct AndroidDebugConfig { /// Forward remote (phone) socket connection to local (host) #[serde(default)] @@ -342,6 +422,7 @@ pub struct AndroidDebugConfig { } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct AndroidConfig { #[serde(flatten)] generic: GenericConfig, @@ -353,12 +434,15 @@ pub struct AndroidConfig { pub gradle: bool, #[serde(default)] pub wry: bool, + #[serde(default)] + pub assets: Vec, /// Debug configuration for `x run` #[serde(default)] pub debug: AndroidDebugConfig, } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct IosConfig { #[serde(flatten)] generic: GenericConfig, @@ -367,6 +451,7 @@ pub struct IosConfig { } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct MacosConfig { #[serde(flatten)] generic: GenericConfig, @@ -374,12 +459,14 @@ pub struct MacosConfig { } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct LinuxConfig { #[serde(flatten)] generic: GenericConfig, } #[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct WindowsConfig { #[serde(flatten)] generic: GenericConfig, diff --git a/xcommon/src/lib.rs b/xcommon/src/lib.rs index b2ca6c52..4a3ea7f3 100644 --- a/xcommon/src/lib.rs +++ b/xcommon/src/lib.rs @@ -286,14 +286,20 @@ impl Zip { } pub fn add_file(&mut self, source: &Path, dest: &Path, opts: ZipFileOptions) -> Result<()> { - let mut f = File::open(source)?; + let mut f = File::open(source) + .with_context(|| format!("While opening file `{}`", source.display()))?; self.start_file(dest, opts)?; std::io::copy(&mut f, &mut self.zip)?; Ok(()) } - pub fn add_directory(&mut self, source: &Path, dest: &Path) -> Result<()> { - add_recursive(self, source, dest)?; + pub fn add_directory( + &mut self, + source: &Path, + dest: &Path, + opts: ZipFileOptions, + ) -> Result<()> { + add_recursive(self, source, dest, opts)?; Ok(()) } @@ -335,17 +341,19 @@ impl Zip { } } -fn add_recursive(zip: &mut Zip, source: &Path, dest: &Path) -> Result<()> { - for entry in std::fs::read_dir(source)? { +fn add_recursive(zip: &mut Zip, source: &Path, dest: &Path, opts: ZipFileOptions) -> Result<()> { + for entry in std::fs::read_dir(source) + .with_context(|| format!("While reading directory `{}`", source.display()))? + { let entry = entry?; let file_name = entry.file_name(); let source = source.join(&file_name); let dest = dest.join(&file_name); let file_type = entry.file_type()?; if file_type.is_dir() { - add_recursive(zip, &source, &dest)?; + add_recursive(zip, &source, &dest, opts)?; } else if file_type.is_file() { - zip.add_file(&source, &dest, ZipFileOptions::Compressed)?; + zip.add_file(&source, &dest, opts)?; } } Ok(())