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}