android_build/dexer.rs
1//! Builder for compiling Java source code into Android DEX bytecode.
2
3use std::path::{Path, PathBuf};
4use std::ffi::{OsStr, OsString};
5use std::process::{Command, ExitStatus};
6use crate::env_paths::{self, PathExt};
7use crate::JavaRun;
8
9/// A builder for generating Android DEX bytecode by invoking `d8` commands.
10///
11/// Currently incremental building options are not provided here.
12///
13/// If you need to customize the `d8` command beyond what is provided here,
14/// you can use the [`Dexer::command()`] method to get a [`Command`]
15/// that can be further customized with additional arguments.
16///
17/// Documentation on `d8` options are based on
18/// <https://developer.android.com/tools/d8/>.
19///
20/// Note: Newer JDK versions (including JDK 21 and above) may not work with
21/// Android D8 from older build tools versions (below 35.0.0) if there are
22/// anonymous classes in the Java code, which produce files like `Cls$1.class`.
23#[derive(Clone, Debug, Default)]
24pub struct Dexer {
25 /// Override the default `JAVA_HOME` path.
26 /// Otherwise, the default path is found using the `JAVA_HOME` env var.
27 java_home: Option<PathBuf>,
28
29 /// Override the default `d8.jar` path.
30 /// Otherwise, the default path is found using [crate::android_d8_jar].
31 android_d8_jar_path: Option<PathBuf>,
32
33 /// Compile DEX bytecode without debug information. However, `d8` includes some information
34 /// that's used when generating stacktraces and logging exceptions.
35 release: bool,
36
37 /// Specify the minimum Android API level you want the output DEX files to support.
38 android_min_api: Option<u32>,
39
40 /// Disable Java 8 language features. Use this flag only if you don't intend to compile
41 /// Java bytecode that uses language features introduced in Java 8.
42 no_desugaring: bool,
43
44 /// Specify the path to the `android.jar` of your Android SDK.
45 /// Otherwise, the default path is found using [crate::android_jar].
46 android_jar_path: Option<PathBuf>,
47
48 /// Specify classpath resources that `d8` may require to compile your project's DEX files.
49 class_paths: Vec<OsString>,
50
51 /// Specify the desired path for the DEX output. By default, `d8` outputs the DEX file(s)
52 /// in the current working directory.
53 out_dir: Option<OsString>,
54
55 /// Specifies paths to compiled Java bytecodes that you want to convert into DEX bytecode.
56 /// The input bytecode can be in any combination of `*.class` files or containers, such as
57 /// JAR, APK, or ZIP files.
58 files: Vec<OsString>,
59}
60
61impl Dexer {
62 /// Creates a new `Dexer` instance with default values,
63 /// which can be further customized using the builder methods.
64 pub fn new() -> Self {
65 Default::default()
66 }
67
68 /// Executes the `java` command based on this `Dexer` instance.
69 pub fn run(&self) -> std::io::Result<ExitStatus> {
70 self.command()?.status()
71 }
72
73 /// Returns a [`Command`] based on this `Dexer` instance
74 /// that can be inspected or customized before being executed.
75 pub fn command(&self) -> std::io::Result<Command> {
76 let mut d8_run = JavaRun::new();
77
78 if let Some(java_home) = &self.java_home {
79 d8_run.java_home(java_home);
80 }
81
82 let d8_jar_path = self.android_d8_jar_path
83 .clone()
84 .and_then(PathExt::path_if_exists)
85 .or_else(|| env_paths::android_d8_jar(None))
86 .ok_or_else(|| std::io::Error::other(
87 "d8.jar not provided, and could not be auto-discovered."
88 ))?;
89
90 d8_run.class_path(d8_jar_path)
91 .main_class("com.android.tools.r8.D8");
92
93 if self.release {
94 d8_run.arg("--release");
95 }
96
97 if let Some(min_api) = self.android_min_api {
98 d8_run.arg("--min-api").arg(min_api.to_string());
99 }
100
101 if self.no_desugaring {
102 // `--lib` and `--classpath` options are probably redundant under this mode.
103 d8_run.arg("--no-desugaring");
104 }
105
106 let android_jar_path = self.android_jar_path
107 .clone()
108 .and_then(PathExt::path_if_exists)
109 .or_else(|| env_paths::android_jar(None))
110 .ok_or_else(|| std::io::Error::other(
111 "android.jar not provided, and could not be auto-discovered."
112 ))?;
113 d8_run.arg("--lib").arg(android_jar_path);
114
115 for class_path in &self.class_paths {
116 d8_run.arg("--classpath").arg(class_path);
117 }
118
119 if let Some(out_dir) = &self.out_dir {
120 d8_run.arg("--output").arg(out_dir);
121 }
122
123 for file in &self.files {
124 d8_run.arg(file);
125 }
126
127 d8_run.command()
128 }
129
130 ///////////////////////////////////////////////////////////////////////////
131 //////////////////////// Builder methods below ////////////////////////////
132 ///////////////////////////////////////////////////////////////////////////
133
134 /// Override the default `JAVA_HOME` path.
135 ///
136 /// If not set, the default path is found using the `JAVA_HOME` env var.
137 pub fn java_home<P: AsRef<OsStr>>(&mut self, java_home: P) -> &mut Self {
138 self.java_home = Some(java_home.as_ref().into());
139 self
140 }
141
142 /// Override the default `d8.jar` path.
143 ///
144 /// Otherwise, the default path is found using [crate::android_d8_jar].
145 pub fn android_d8_jar<P: AsRef<OsStr>>(&mut self, android_d8_jar_path: P) -> &mut Self {
146 self.android_d8_jar_path.replace(android_d8_jar_path.as_ref().into());
147 self
148 }
149
150 /// Compile DEX bytecode without debug information (including those enabled with
151 /// [crate::DebugInfo] when running [crate::JavaBuild]). However, `d8` includes some
152 /// information that's used when generating stacktraces and logging exceptions.
153 pub fn release(&mut self, release: bool) -> &mut Self {
154 self.release = release;
155 self
156 }
157
158 /// Specify the minimum Android API level you want the output DEX files to support.
159 ///
160 /// Set it to `20` to disable the multidex feature, so it may be loaded by `DexClassLoader`
161 /// available on Android 7.1 and older versions without using the legacy multidex library.
162 /// This is also useful if you want to make sure of having only one `classes.dex` output
163 /// file; still, it keeps compatible with newest Android versions.
164 pub fn android_min_api(&mut self, api_level: u32) -> &mut Self {
165 self.android_min_api.replace(api_level);
166 self
167 }
168
169 /// Disable Java 8 language features. Use this flag only if you don't intend to compile
170 /// Java bytecode that uses language features introduced in Java 8.
171 pub fn no_desugaring(&mut self, no_desugaring: bool) -> &mut Self {
172 self.no_desugaring = no_desugaring;
173 self
174 }
175
176 /// Specify the path to the `android.jar` of your Android SDK. This is required when
177 /// [compiling bytecode that uses Java 8 language features](https://developer.android.google.cn/tools/d8#j8).
178 ///
179 /// If not set, the default path is found using [crate::android_jar].
180 pub fn android_jar<P: AsRef<OsStr>>(&mut self, android_jar_path: P) -> &mut Self {
181 self.android_jar_path.replace(android_jar_path.as_ref().into());
182 self
183 }
184
185 /// Specify classpath resources that `d8` may require to compile your project's DEX files.
186 ///
187 /// In particular, `d8` requires that you specify certain resources when [compiling bytecode
188 /// that uses Java 8 language features](https://developer.android.google.cn/tools/d8#j8).
189 /// This is usually the the path to all of your project's Java bytecode, even if you don't
190 /// intend to compile all of the bytecode into DEX bytecode.
191 pub fn class_path<S: AsRef<OsStr>>(&mut self, class_path: S) -> &mut Self {
192 self.class_paths.push(class_path.as_ref().into());
193 self
194 }
195
196 /// Specify the desired path for the DEX output. By default, `d8` outputs the DEX file(s)
197 /// in the current working directory.
198 pub fn out_dir<P: AsRef<OsStr>>(&mut self, out_dir: P) -> &mut Self {
199 self.out_dir = Some(out_dir.as_ref().into());
200 self
201 }
202
203 /// Adds a compiled Java bytecode file that you want to convert into DEX bytecode.
204 /// The input bytecode can be in any combination of `*.class` files or containers, such as
205 /// JAR, APK, or ZIP files.
206 pub fn file<P: AsRef<OsStr>>(&mut self, file: P) -> &mut Self {
207 self.files.push(file.as_ref().into());
208 self
209 }
210
211 /// Adds multiple compiled Java bytecode files that you want to convert into DEX bytecode.
212 ///
213 /// This is the same as calling [`Dexer::file()`] multiple times.
214 pub fn files<P>(&mut self, files: P) -> &mut Self
215 where
216 P: IntoIterator,
217 P::Item: AsRef<OsStr>,
218 {
219 self.files.extend(files.into_iter().map(|f| f.as_ref().into()));
220 self
221 }
222
223 /// Searches and adds `.class` files under `class_path` directory recursively.
224 ///
225 /// This is the same as calling [`Dexer::files()`] for these files, usually more convenient.
226 pub fn collect_classes<P: AsRef<OsStr>>(&mut self, class_path: P) -> std::io::Result<&mut Self> {
227 let class_path = PathBuf::from(class_path.as_ref());
228 if !class_path.is_dir() {
229 return Err(std::io::Error::new(
230 std::io::ErrorKind::InvalidInput,
231 "`class_path` is not a directory"
232 ));
233 }
234 let extension = Some(std::ffi::OsStr::new("class"));
235 visit_dirs(class_path, &mut |entry| {
236 if entry.path().extension() == extension {
237 self.file(entry.path());
238 }
239 })?;
240 Ok(self)
241 }
242}
243
244/// Walking a directory only visiting files. Copied from `std::fs::read_dir` examples.
245fn visit_dirs(
246 dir: impl AsRef<Path>,
247 cb: &mut impl FnMut(&std::fs::DirEntry),
248) -> std::io::Result<()> {
249 if dir.as_ref().is_dir() {
250 for entry in std::fs::read_dir(dir)? {
251 let entry = entry?;
252 let path = entry.path();
253 if path.is_dir() {
254 visit_dirs(&path, cb)?;
255 } else {
256 cb(&entry);
257 }
258 }
259 }
260 Ok(())
261}