1use 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#[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#[derive(Clone, Debug)]
45struct NativeInput {
46 abi: Abi,
47 name: LibraryName,
48 content: Content,
49}
50
51#[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 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 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 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 pub fn resources_mut(&mut self) -> &mut Resources {
125 &mut self.resources
126 }
127
128 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 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 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 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 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 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 pub fn dex(&mut self, content: impl Into<Content>) -> &mut Self {
209 self.dex.push(content.into());
210 self
211 }
212
213 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 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 pub fn native_policy(&mut self, policy: NativePolicy) -> &mut Self {
241 self.native_policy = policy;
242 self
243 }
244
245 pub fn sign_with(&mut self, signer: Signer) -> &mut Self {
247 self.signer = Some(signer);
248 self
249 }
250
251 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 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 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 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 fn check(&self) -> Result<App, Error> {
343 let manifest = &self.manifest;
344 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 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 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
437struct App {
439 package: PackageName,
440 min_sdk: ApiLevel,
441 target_sdk: ApiLevel,
442 entry_library: Option<LibraryName>,
444}
445
446fn 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}