1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
use super::bundletool;
use crate::error::*;
use std::path::{Path, PathBuf};

/// ## Generate a set of APKs from your app bundle
///
/// When `bundletool` generates APKs from your app bundle,it includes them in a container
/// called an APK set archive, which uses the `.apks` file extension. To generate an APK
/// set for all device configurations your app supports from your app bundle, use the
/// `bundletool build-apks` command, as shown below.
///
/// ```xml
/// bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks
/// ```
///
/// If you want to deploy the APKs to a device, you need to also include your app's
/// signing information, as shown in the command below. If you do not specify signing
/// information, `bundletool` attempts to sign your APKs with a debug key for you.
///
/// ```sh
/// `bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks`
/// `--ks=/MyApp/keystore.jks`
/// `--ks-pass=file:/MyApp/keystore.pwd`
/// `--ks-key-alias=MyKeyAlias`
/// `--key-pass=file:/MyApp/key.pwd`
/// ```
///
/// The table below describes the various flags and options you can set when using the
/// `bundletool build-apks` command in greater detail. Only `--bundle` and `--output` are
/// required—all other flags are optional
#[derive(Debug, Default)]
pub struct BuildApks {
    bundle: PathBuf,
    output: PathBuf,
    overwrite: bool,
    aapt2: Option<PathBuf>,
    ks: Option<PathBuf>,
    ks_pass_pass: Option<String>,
    ks_pass_file: Option<PathBuf>,
    ks_key_alias: Option<String>,
    key_pass_pass: Option<String>,
    key_pass_file: Option<PathBuf>,
    connected_device: bool,
    device_id: Option<String>,
    device_spec: Option<PathBuf>,
    mode_universal: bool,
    local_testing: bool,
}

#[derive(Debug)]
pub enum KsPass {
    KsPassPass,
    KsPassFile,
}

#[derive(Debug)]
pub enum KeyPass {
    KeyPassPass,
    KeyPassFile,
}

impl BuildApks {
    /// (`Required`) Specifies the path to the app bundle you built using Android Studio.
    /// To learn more, read [`Build your project`].
    ///
    /// (Required) Specifies the name of the output `.apks` file, which contains all the
    /// APK artifacts for your app. To test the artifacts in this file on a device, go to
    /// the section about how to
    /// [`deploy APKs to a connected device`](https://developer.android.com/studio/command-line/bundletool#deploy_with_bundletool)
    ///
    /// [Build your project]: (https://developer.android.com/studio/run#reference)
    pub fn new(bundle: &Path, output: &Path) -> Self {
        Self {
            bundle: bundle.to_owned(),
            output: output.to_owned(),
            ..Default::default()
        }
    }

    /// Include this flag if you want to overwrite any existing output file with the same
    /// path you specify using the `--output` option. If you don't include this flag and
    /// the output file already exists, you get a build error
    pub fn overwrite(&mut self, overwrite: bool) -> &mut Self {
        self.overwrite = overwrite;
        self
    }

    /// Specifies a custom path to AAPT2. By default, `bundletool` includes its own
    /// version of AAPT2
    pub fn aapt2(&mut self, aapt2: &Path) -> &mut Self {
        self.aapt2 = Some(aapt2.to_owned());
        self
    }

    /// Specifies the path to the deployment keystore used to sign the APKs. This flag is
    /// optional. If you don't include it, `bundletool` attempts to sign your APKs with a
    /// debug signing key
    pub fn ks(&mut self, ks: &Path) -> &mut Self {
        self.ks = Some(ks.to_owned());
        self
    }

    /// Specifies your keystore's password. If you're specifying a password in plain text,
    /// qualify it with pass:. If you're passing the path to a file that contains the
    /// password, qualify it with file:. If you specify a keystore using the `--ks` flag
    /// without specifying `--ks-pass`, `build_apks` prompts you for a password from the
    /// command line
    pub fn ks_pass_pass(&mut self, ks_pass_pass: String) -> &mut Self {
        self.ks_pass_pass = Some(ks_pass_pass);
        self
    }

    /// Specifies your keystore's password. If you're specifying a password in plain text,
    /// qualify it with pass:. If you're passing the path to a file that contains the
    /// password, qualify it with file:. If you specify a keystore using the `--ks` flag
    /// without specifying `--ks-pass`, `build_apks` prompts you for a password from the
    /// command line
    pub fn ks_pass_file(&mut self, ks_pass_file: &Path) -> &mut Self {
        self.ks_pass_file = Some(ks_pass_file.to_owned());
        self
    }

    /// Specifies the alias of the signing key you want to use
    pub fn ks_key_alias(&mut self, ks_key_alias: String) -> &mut Self {
        self.ks_key_alias = Some(ks_key_alias);
        self
    }

    /// Specifies the password for the signing key. If you're specifying a password in
    /// plain text, qualify it with pass:. If you're passing the path to a file that
    /// contains the password, qualify it with file:.
    ///
    /// If this password is identical to the one for the keystore itself, you can omit
    /// this flag
    pub fn key_pass_pass(&mut self, key_pass_pass: String) -> &mut Self {
        self.key_pass_pass = Some(key_pass_pass);
        self
    }

    /// Specifies the password for the signing key. If you're specifying a password in
    /// plain text, qualify it with pass:. If you're passing the path to a file that
    /// contains the password, qualify it with file:.
    ///
    /// If this password is identical to the one for the keystore itself, you can omit
    /// this flag
    pub fn key_pass_file(&mut self, key_pass_file: &Path) -> &mut Self {
        self.key_pass_file = Some(key_pass_file.to_owned());
        self
    }

    /// Instructs `build_apks` to build APKs that target the configuration of a connected
    /// device. If you don't include this flag, `build_apks` generates APKs for all device
    /// configurations your app supports
    pub fn connected_device(&mut self, connected_device: bool) -> &mut Self {
        self.connected_device = connected_device;
        self
    }

    /// If you have more than one connected device, use this flag to specify the serial ID
    /// of the device to which you want to deploy your app
    pub fn device_id(&mut self, device_id: String) -> &mut Self {
        self.device_id = Some(device_id);
        self
    }

    /// Use this flag to provide a path to a `.json` file that specifies the device
    /// configuration you want to target. To learn more, go to the section about how to
    /// [`Create and use device specification JSON files`](https://developer.android.com/studio/command-line/bundletool#create_use_json)
    pub fn device_spec(&mut self, device_spec: &Path) -> &mut Self {
        self.device_spec = Some(device_spec.to_owned());
        self
    }

    /// Set the mode to universal if you want `build_apks` to build only a single APK that
    /// includes all of your app's code and resources such that the APK is compatible with
    /// all device configurations your app supports.
    ///
    /// ## Note
    /// `build_apks` includes only feature modules that specify `<dist:fusing
    /// dist:include="true"/>` in their manifest in a universal APK. To learn more, read
    /// about the [`feature module manifest`].
    ///
    /// Keep in mind, these APKs are larger than those optimized for a particular device
    /// configuration. However, they're easier to share with internal testers who, for
    /// example, want to test your app on multiple device configurations.
    ///
    /// [feature module manifest]: https://developer.android.com/guide/playcore/feature-delivery#dynamic_feature_manifest
    pub fn mode_universal(&mut self, mode_universal: bool) -> &mut Self {
        self.mode_universal = mode_universal;
        self
    }

    /// Use this flag to enable your app bundle for local testing. Local testing allows
    /// for quick, iterative testing cycles without the need to upload to Google Play
    /// servers.
    ///
    /// For an example of how to test module installation using the `--local-testing`
    /// flag, see
    /// [`Locally test module installs`](https://developer.android.com/guide/app-bundle/test/testing-fakesplitinstallmanager)
    pub fn local_testing(&mut self, local_testing: bool) -> &mut Self {
        self.local_testing = local_testing;
        self
    }

    /// Runs `build_apks` commands to build apks
    pub fn run(&self) -> Result<PathBuf> {
        let mut build_apks = bundletool()?;
        build_apks.arg("build-apks");
        build_apks.arg("--bundle").arg(&self.bundle);
        build_apks.arg("--output").arg(&self.output);
        if self.overwrite {
            build_apks.arg("--overwrite");
        }
        if let Some(aapt2) = &self.aapt2 {
            build_apks.arg("--aapt2").arg(aapt2);
        }
        if let Some(ks) = &self.ks {
            build_apks.arg("--ks").arg(ks);
        }
        if let Some(ks_pass_pass) = &self.ks_pass_pass {
            build_apks
                .arg("--ks-pass")
                .arg(format!("pass:{}", ks_pass_pass));
        }
        if let Some(ks_pass_file) = &self.ks_pass_file {
            build_apks.arg("--ks-pass").arg(format!(
                "file:{}",
                ks_pass_file.to_str().expect("Wrong ks_pass_file provided")
            ));
        }
        if let Some(ks_key_alias) = &self.ks_key_alias {
            build_apks.arg("--ks-key-alias").arg(ks_key_alias);
        }
        if let Some(key_pass_pass) = &self.key_pass_pass {
            build_apks
                .arg("--key-pass")
                .arg(format!("pass:{}", key_pass_pass));
        }
        if let Some(key_pass_file) = &self.key_pass_file {
            build_apks.arg("--key-pass").arg(format!(
                "file:{}",
                key_pass_file
                    .to_str()
                    .expect("Wrong key_pass_file provided")
            ));
        }
        if self.connected_device {
            build_apks.arg("--connected-device");
        }
        if let Some(device_id) = &self.device_id {
            build_apks.arg("--device-id").arg(device_id);
        }
        if let Some(device_spec) = &self.device_spec {
            build_apks.arg("--device-spec").arg(device_spec);
        }
        if self.mode_universal {
            build_apks.arg("--mode").arg("universal");
        }
        if self.local_testing {
            build_apks.arg("--local-testing");
        }
        build_apks.output_err(true)?;
        Ok(self.output.clone())
    }
}