Skip to main content

apk/
build.rs

1//! Building APKs.
2
3use std::borrow::Cow;
4use std::collections::{BTreeMap, BTreeSet};
5use std::io::{BufWriter, Write};
6use std::path::Path;
7
8use crate::android::attr;
9use crate::archive::{ApkWriter, Compression, Content, EntryOptions, ZipError};
10use crate::error::{Error, FileOperation};
11use crate::manifest::{self, Manifest, ManifestError};
12use crate::native::{ElfInfo, LibraryStorage, NATIVE_ACTIVITY_ENTRY, NativeError, NativePolicy, PageSize};
13use crate::res::{Config, Density, Locale, ResourceRef, ResourceType, Resources, Value};
14use crate::sign::Signer;
15use crate::types::{Abi, ApiLevel, ArchivePath, LibraryName, PackageName, PermissionName, VersionCode};
16
17/// Why an APK's configuration is inconsistent.
18#[derive(Debug, thiserror::Error)]
19#[non_exhaustive]
20pub enum BuildError {
21    #[error("minSdkVersion {min} is above targetSdkVersion {target}")]
22    MinSdkAboveTarget { min: ApiLevel, target: ApiLevel },
23    #[error("maxSdkVersion {max} is below minSdkVersion {min}")]
24    MaxSdkBelowMin { min: ApiLevel, max: u32 },
25    #[error("the NativeActivity library is named by {value}, which the builder cannot check; name it literally")]
26    NativeLibraryNotLiteral { value: Value },
27    #[error("NativeActivity requires minSdkVersion 9 or later")]
28    NativeActivityRequiresApi9,
29    #[error("the app has no native libraries for its NativeActivity to load")]
30    NoNativeLibraries,
31    #[error("hasCode is true but no DEX files were added")]
32    MissingCode,
33    #[error("DEX files were added but the manifest declares hasCode=\"false\"")]
34    CodeDeclaredAbsent,
35    #[error("{path} is written by the builder; use the dedicated method instead of `file`")]
36    ReservedPath { path: ArchivePath },
37    #[error("android:extractNativeLibs={manifest} conflicts with {storage:?} library storage")]
38    ExtractNativeLibsConflict { manifest: bool, storage: LibraryStorage },
39    #[error("{component} has intent filters, so targetSdkVersion 31+ requires android:exported")]
40    ExportedRequired { component: String },
41}
42
43/// A native library added to the build.
44#[derive(Clone, Debug)]
45struct NativeInput {
46    abi: Abi,
47    name: LibraryName,
48    content: Content,
49}
50
51/// An APK under construction.
52///
53/// Settings apply to one [`Manifest`] document, which is checked and then
54/// compiled exactly as it stands when the APK is written.
55///
56/// ```no_run
57/// use apk::build::ApkBuilder;
58/// use apk::res::Density;
59/// use apk::sign::{Signer, SigningIdentity};
60/// use apk::{Abi, ApiLevel, LibraryName, PackageName};
61///
62/// let mut app = ApkBuilder::native_activity(PackageName::try_new("org.example.game")?, LibraryName::try_new("game")?);
63/// app.label("Game")
64///     .version(3.try_into()?, "1.2.0")
65///     .min_sdk(ApiLevel::LOLLIPOP)
66///     .permission("android.permission.INTERNET".parse()?)
67///     .localized_string("app_name", "fr".parse()?, "Jeu")?
68///     .icon(Density::XHDPI, std::fs::read("icon.png")?)?
69///     .asset("levels/1.json", std::fs::read("levels/1.json")?)?
70///     .native_library(Abi::Arm64V8a, "target/aarch64-linux-android/release/libgame.so")?
71///     .sign_with(Signer::new(SigningIdentity::from_pem_file("release.pem")?));
72/// app.write("game.apk")?;
73/// # Ok::<(), Box<dyn std::error::Error>>(())
74/// ```
75#[derive(Clone, Debug)]
76pub struct ApkBuilder {
77    manifest: Manifest,
78    resources: Resources,
79    label: Option<String>,
80    files: BTreeMap<ArchivePath, (Content, Option<EntryOptions>)>,
81    dex: Vec<Content>,
82    native: Vec<NativeInput>,
83    native_policy: NativePolicy,
84    signer: Option<Signer>,
85}
86
87impl ApkBuilder {
88    /// Starts an APK from a complete manifest.
89    pub fn new(manifest: Manifest) -> Self {
90        Self {
91            manifest,
92            resources: Resources::new(),
93            label: None,
94            files: BTreeMap::new(),
95            dex: Vec::new(),
96            native: Vec::new(),
97            native_policy: NativePolicy::default(),
98            signer: None,
99        }
100    }
101
102    /// Starts a launchable app whose `android.app.NativeActivity` runs
103    /// `lib<library>.so`. The app has no Java code.
104    pub fn native_activity(package: PackageName, library: LibraryName) -> Self {
105        let mut manifest = Manifest::new(&package);
106        manifest.application_mut().set_attr(attr::HAS_CODE, false).push_child(manifest::native_activity(&library));
107        Self::new(manifest)
108    }
109
110    pub fn manifest(&self) -> &Manifest {
111        &self.manifest
112    }
113
114    /// The manifest, for settings without a builder method.
115    pub fn manifest_mut(&mut self) -> &mut Manifest {
116        &mut self.manifest
117    }
118
119    pub fn resources(&self) -> &Resources {
120        &self.resources
121    }
122
123    /// The resources, for values and files without a builder method.
124    pub fn resources_mut(&mut self) -> &mut Resources {
125        &mut self.resources
126    }
127
128    /// Sets the app name shown by the launcher (string resource `app_name`,
129    /// which can be translated with [`localized_string`](Self::localized_string)).
130    pub fn label(&mut self, label: impl Into<String>) -> &mut Self {
131        self.label = Some(label.into());
132        self
133    }
134
135    pub fn version(&mut self, code: VersionCode, name: &str) -> &mut Self {
136        self.manifest.set_version(code, name);
137        self
138    }
139
140    pub fn min_sdk(&mut self, level: ApiLevel) -> &mut Self {
141        self.manifest.set_min_sdk(level);
142        self
143    }
144
145    pub fn target_sdk(&mut self, level: ApiLevel) -> &mut Self {
146        self.manifest.set_target_sdk(level);
147        self
148    }
149
150    pub fn permission(&mut self, name: PermissionName) -> &mut Self {
151        self.manifest.add_permission(&name);
152        self
153    }
154
155    pub fn debuggable(&mut self, debuggable: bool) -> &mut Self {
156        self.manifest.application_mut().set_attr(attr::DEBUGGABLE, debuggable);
157        self
158    }
159
160    /// Adds the default value of string resource `name`.
161    pub fn string(&mut self, name: &str, value: impl Into<String>) -> crate::Result<&mut Self> {
162        self.resources.string(name, value)?;
163        Ok(self)
164    }
165
166    /// Adds a translation of string resource `name`.
167    pub fn localized_string(
168        &mut self,
169        name: &str,
170        locale: Locale,
171        value: impl Into<String>,
172    ) -> crate::Result<&mut Self> {
173        self.resources.localized_string(name, locale, value)?;
174        Ok(self)
175    }
176
177    /// Adds a PNG launcher icon (`@mipmap/ic_launcher`) for one density.
178    pub fn icon(&mut self, density: Density, png: impl Into<Content>) -> crate::Result<&mut Self> {
179        self.resources.file(ResourceType::Mipmap, "ic_launcher", Config::for_density(density), "png", png)?;
180        Ok(self)
181    }
182
183    /// Adds a file under `assets/`, read by apps through `AAssetManager`.
184    pub fn asset(&mut self, path: &str, content: impl Into<Content>) -> crate::Result<&mut Self> {
185        let path = ArchivePath::try_new(format!("assets/{path}"))?;
186        self.file(path, content, None)
187    }
188
189    /// Adds any file at an exact archive path, stored with `options` (or the
190    /// default for its extension).
191    pub fn file(
192        &mut self,
193        path: ArchivePath,
194        content: impl Into<Content>,
195        options: Option<EntryOptions>,
196    ) -> crate::Result<&mut Self> {
197        if is_reserved(&path) {
198            return Err(BuildError::ReservedPath { path }.into());
199        }
200        if self.files.contains_key(&path) {
201            return Err(ZipError::DuplicateEntry { name: path.to_string() }.into());
202        }
203        self.files.insert(path, (content.into(), options));
204        Ok(self)
205    }
206
207    /// Adds a DEX file (`classes.dex`, then `classes2.dex`, ...).
208    pub fn dex(&mut self, content: impl Into<Content>) -> &mut Self {
209        self.dex.push(content.into());
210        self
211    }
212
213    /// Adds a native library from a file named `lib<name>.so`.
214    pub fn native_library(&mut self, abi: Abi, path: impl AsRef<Path>) -> crate::Result<&mut Self> {
215        let path = path.as_ref();
216        let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default();
217        let name = LibraryName::from_file_name(file_name)?;
218        self.add_native(abi, name, Content::File(path.to_owned()))
219    }
220
221    /// Adds a native library from memory as `lib<name>.so`.
222    pub fn native_library_bytes(
223        &mut self,
224        abi: Abi,
225        name: LibraryName,
226        bytes: impl Into<Content>,
227    ) -> crate::Result<&mut Self> {
228        self.add_native(abi, name, bytes.into())
229    }
230
231    fn add_native(&mut self, abi: Abi, name: LibraryName, content: Content) -> crate::Result<&mut Self> {
232        if self.native.iter().any(|n| n.abi == abi && n.name == name) {
233            return Err(NativeError::DuplicateLibrary { library: name, abi }.into());
234        }
235        self.native.push(NativeInput { abi, name, content });
236        Ok(self)
237    }
238
239    /// How native libraries are stored and checked.
240    pub fn native_policy(&mut self, policy: NativePolicy) -> &mut Self {
241        self.native_policy = policy;
242        self
243    }
244
245    /// Signs the APK when it is written, for the manifest's SDK range.
246    pub fn sign_with(&mut self, signer: Signer) -> &mut Self {
247        self.signer = Some(signer);
248        self
249    }
250
251    /// Completes the manifest, checks the whole configuration, and writes the
252    /// APK to `output` atomically. The target is replaced only after validation
253    /// and writing succeed.
254    pub fn write(mut self, output: impl AsRef<Path>) -> Result<(), Error> {
255        let output = output.as_ref();
256        self.complete_manifest()?;
257        let app = self.check()?;
258        self.check_native(&app)?;
259        let plan =
260            self.signer.as_ref().map(|signer| signer.plan(Some(app.min_sdk), Some(app.target_sdk))).transpose()?;
261        let compiled = self.resources.compile(&app.package, app.min_sdk)?;
262        let manifest = self.manifest.document().encode(&compiled)?;
263
264        if let Some(parent) = output.parent().filter(|p| !p.as_os_str().is_empty()) {
265            std::fs::create_dir_all(parent).map_err(Error::file(FileOperation::Create, parent))?;
266        }
267        let mut staged = crate::fs::Staged::beside(output)?;
268        let staged_path = staged.path().to_owned();
269        let mut writer = ApkWriter::new(BufWriter::new(staged.file()));
270        if let Some(digest) = plan.as_ref().and_then(|plan| plan.jar_digest()) {
271            writer = writer.digest_entries(digest);
272        }
273        writer.add(&ArchivePath::manifest(), &manifest, EntryOptions::DEFLATED)?;
274        for (index, dex) in self.dex.iter().enumerate() {
275            let name = if index == 0 { "classes.dex".to_owned() } else { format!("classes{}.dex", index + 1) };
276            writer.add_content(&ArchivePath::try_new(name)?, dex, EntryOptions::DEFLATED)?;
277        }
278        // Android 11+ requires an uncompressed, 4-byte aligned resource table.
279        writer.add(&ArchivePath::resource_table(), &compiled.table, EntryOptions::STORED)?;
280        for (path, content) in &compiled.files {
281            writer.add_content(path, content, EntryOptions::for_path(path))?;
282        }
283        for (path, (content, options)) in &self.files {
284            writer.add_content(path, content, options.unwrap_or_else(|| EntryOptions::for_path(path)))?;
285        }
286        let library_options = match self.native_policy.storage {
287            LibraryStorage::Mapped => EntryOptions::page_aligned(PageSize::SixteenKiB.bytes() as u16),
288            LibraryStorage::Extracted => EntryOptions { compression: Compression::Deflated, alignment: 1 },
289        };
290        // Grouped by ABI; each library is read, checked, and written in turn,
291        // so the bytes checked are the bytes written.
292        let mut libraries: Vec<&NativeInput> = self.native.iter().collect();
293        libraries.sort_by(|left, right| (left.abi, &left.name).cmp(&(right.abi, &right.name)));
294        for library in libraries {
295            let bytes = self.read_native(library, &app)?;
296            let path = ArchivePath::try_new(format!("lib/{}/{}", library.abi, library.name.file_name()))?;
297            writer.add(&path, &bytes, library_options)?;
298        }
299        if let Some(plan) = &plan {
300            for (path, data) in plan.jar_signature_files(writer.digests())? {
301                writer.add(&path, &data, EntryOptions::STORED)?;
302            }
303        }
304        writer.finish()?.flush().map_err(Error::file(FileOperation::Write, staged_path))?;
305        if let Some(plan) = &plan {
306            plan.apply_block(staged.file(), &[])?;
307        }
308        staged.persist(output)
309    }
310
311    /// Fills in what the builder owns wherever the manifest is silent: the
312    /// label and icon, `hasCode`, and `extractNativeLibs`.
313    fn complete_manifest(&mut self) -> Result<(), Error> {
314        let labeled = self.label.is_some();
315        if let Some(label) = self.label.take()
316            && !self.resources.contains_variant(ResourceType::String, "app_name", Config::default())
317        {
318            self.resources.string("app_name", label)?;
319        }
320        let icon = self.resources.contains(ResourceType::Mipmap, "ic_launcher");
321        let defaults = [
322            (attr::LABEL, labeled.then(|| app_ref(ResourceType::String, "app_name"))),
323            (attr::ICON, icon.then(|| app_ref(ResourceType::Mipmap, "ic_launcher"))),
324            (attr::HAS_CODE, Some((!self.dex.is_empty()).into())),
325            (
326                attr::EXTRACT_NATIVE_LIBS,
327                (!self.native.is_empty()).then(|| (self.native_policy.storage == LibraryStorage::Extracted).into()),
328            ),
329        ];
330        let application = self.manifest.application_mut();
331        for (def, value) in defaults {
332            if let Some(value) = value
333                && application.android_attr(def).is_none()
334            {
335                application.set_attr(def, value);
336            }
337        }
338        Ok(())
339    }
340
341    /// Checks the manifest and its consistency with the builder's inputs.
342    fn check(&self) -> Result<App, Error> {
343        let manifest = &self.manifest;
344        // Mistyped values would read as defaults below, so they fail first.
345        manifest.document().check()?;
346        manifest.validate()?;
347        let package = PackageName::try_new(manifest.package().ok_or(ManifestError::MissingPackage)?)?;
348        let (min_sdk, target_sdk) = (manifest.min_sdk().api_level()?, manifest.effective_target_sdk()?);
349        if min_sdk > target_sdk {
350            return Err(BuildError::MinSdkAboveTarget { min: min_sdk, target: target_sdk }.into());
351        }
352        if let Some(max) = manifest.max_sdk().filter(|&max| max < min_sdk.get()) {
353            return Err(BuildError::MaxSdkBelowMin { min: min_sdk, max }.into());
354        }
355        match (manifest.has_code(), self.dex.is_empty()) {
356            (true, true) => return Err(BuildError::MissingCode.into()),
357            (false, false) => return Err(BuildError::CodeDeclaredAbsent.into()),
358            _ => {}
359        }
360        if target_sdk >= ApiLevel::S
361            && let Some(component) = manifest
362                .components()
363                .find(|c| c.element("intent-filter").is_some() && c.android_attr(attr::EXPORTED).is_none())
364        {
365            let name = component.android_attr(attr::NAME).map(ToString::to_string);
366            return Err(
367                BuildError::ExportedRequired { component: name.unwrap_or_else(|| component.name.clone()) }.into()
368            );
369        }
370        let storage = self.native_policy.storage;
371        if let Some(declared) = manifest.extract_native_libs().filter(|_| !self.native.is_empty())
372            && declared != (storage == LibraryStorage::Extracted)
373        {
374            return Err(BuildError::ExtractNativeLibsConflict { manifest: declared, storage }.into());
375        }
376        let entry_library = match manifest.native_activity_library() {
377            None => None,
378            Some(Ok(library)) => Some(LibraryName::try_new(library)?),
379            Some(Err(value)) => return Err(BuildError::NativeLibraryNotLiteral { value: value.clone() }.into()),
380        };
381        Ok(App { package, min_sdk, target_sdk, entry_library })
382    }
383
384    /// Checks the set of native libraries; [`read_native`](Self::read_native)
385    /// checks each one.
386    fn check_native(&self, app: &App) -> Result<(), Error> {
387        if app.entry_library.is_some() {
388            if app.min_sdk < ApiLevel::GINGERBREAD {
389                return Err(BuildError::NativeActivityRequiresApi9.into());
390            }
391            if self.native.is_empty() {
392                return Err(BuildError::NoNativeLibraries.into());
393            }
394        }
395        let abis: BTreeSet<Abi> = self.native.iter().map(|n| n.abi).collect();
396        if self.native_policy.page_size == PageSize::FourKiB
397            && app.target_sdk >= ApiLevel::VANILLA_ICE_CREAM
398            && abis.iter().any(|a| a.is_64_bit())
399        {
400            return Err(NativeError::FourKibPages { target_sdk: app.target_sdk }.into());
401        }
402        if let Some(entry) = &app.entry_library
403            && let Some(abi) =
404                abis.into_iter().find(|&abi| !self.native.iter().any(|n| n.abi == abi && &n.name == entry))
405        {
406            return Err(NativeError::MissingEntryLibrary { library: entry.clone(), abi }.into());
407        }
408        Ok(())
409    }
410
411    /// Reads native library `input` and checks it against the policy and the
412    /// other libraries of its ABI.
413    fn read_native<'a>(&self, input: &'a NativeInput, app: &App) -> Result<Cow<'a, [u8]>, Error> {
414        let policy = &self.native_policy;
415        let bytes = match &input.content {
416            Content::Bytes(bytes) => Cow::Borrowed(&bytes[..]),
417            Content::File(path) => Cow::Owned(std::fs::read(path).map_err(Error::file(FileOperation::Read, path))?),
418        };
419        let info = ElfInfo::parse(&bytes)?;
420        info.check(&input.name, input.abi, policy)?;
421        if app.entry_library.as_ref() == Some(&input.name) && !info.exports(NATIVE_ACTIVITY_ENTRY) {
422            return Err(
423                NativeError::MissingEntryPoint { library: input.name.clone(), symbol: NATIVE_ACTIVITY_ENTRY }.into()
424            );
425        }
426        if policy.check_dependencies {
427            let packaged =
428                |needed: &str| self.native.iter().any(|n| n.abi == input.abi && n.name.file_name() == needed);
429            for needed in info.needed.iter().filter(|needed| !packaged(needed)) {
430                policy.check_system_library(&input.name, input.abi, needed, app.min_sdk)?;
431            }
432        }
433        Ok(bytes)
434    }
435}
436
437/// The facts of a checked app that writing it depends on.
438struct App {
439    package: PackageName,
440    min_sdk: ApiLevel,
441    target_sdk: ApiLevel,
442    /// The library a `NativeActivity` loads, if the app has one.
443    entry_library: Option<LibraryName>,
444}
445
446/// Entries the builder writes itself, which `file` must not shadow: the
447/// manifest, resource table, DEX files, native libraries (which are
448/// validated), and JAR signature files.
449fn is_reserved(path: &ArchivePath) -> bool {
450    let name = path.as_str();
451    let dex = name.strip_prefix("classes").and_then(|rest| rest.strip_suffix(".dex"));
452    name == "AndroidManifest.xml"
453        || name == "resources.arsc"
454        || dex.is_some_and(|n| n.is_empty() || n.bytes().all(|b| b.is_ascii_digit()))
455        || path.is_in("lib")
456        || crate::archive::is_signature_file(name)
457}
458
459fn app_ref(ty: ResourceType, name: &str) -> crate::res::Value {
460    ResourceRef::app(ty, name).expect("built-in resource names are valid").into()
461}