lib_cargo_apk/
config.rs

1use cargo::core::{Package, TargetKind};
2use cargo::util::CargoResult;
3use cargo::CliError;
4use failure::format_err;
5use itertools::Itertools;
6use serde::Deserialize;
7use std::collections::btree_map::BTreeMap;
8use std::env;
9use std::fs;
10use std::fs::File;
11use std::io::Read;
12use std::path::Path;
13use std::path::PathBuf;
14use toml;
15
16#[derive(Clone)]
17pub struct AndroidConfig {
18    /// Name of the cargo package
19    pub cargo_package_name: String,
20
21    /// Version of the cargo package
22    pub cargo_package_version: String,
23
24    /// Path to the manifest
25    pub manifest_path: PathBuf,
26    /// Path to the root of the Android SDK.
27    pub sdk_path: PathBuf,
28    /// Path to the root of the Android NDK.
29    pub ndk_path: PathBuf,
30
31    /// List of targets to build the app for. Eg. `armv7-linux-androideabi`.
32    pub build_targets: Vec<AndroidBuildTarget>,
33
34    /// Path to the android.jar for the selected android platform
35    pub android_jar_path: PathBuf,
36
37    /// Version of android:targetSdkVersion (optional). Default Value = android_version
38    pub target_sdk_version: u32,
39    /// Version of android:minSdkVersion (optional). Default Value = android_version
40    pub min_sdk_version: u32,
41
42    /// Version of the build tools to use
43    pub build_tools_version: String,
44
45    /// Should we build in release mode?
46    pub release: bool,
47
48    /// Target configuration settings that are associated with a specific target
49    pub default_target_config: TomlAndroidTarget,
50
51    /// Target specific configuration settings
52    target_configs: BTreeMap<(TargetKind, String), TomlAndroidTarget>,
53}
54
55impl AndroidConfig {
56    /// Builds the android target config based on the default target config and the specific target configs defined in the manifest
57    pub fn resolve(&self, target: (TargetKind, String)) -> CargoResult<AndroidTargetConfig> {
58        let primary_config = self.target_configs.get(&target);
59        let target_name = target.1;
60        let is_default_target = target_name == self.cargo_package_name;
61        let example = target.0 == TargetKind::ExampleBin;
62
63        Ok(AndroidTargetConfig {
64            package_name: primary_config
65                .and_then(|a| a.package_name.clone())
66                .or_else(|| {
67                    if is_default_target {
68                        self.default_target_config.package_name.clone()
69                    } else {
70                        None
71                    }
72                })
73                .unwrap_or_else(|| {
74                    if example {
75                        format!("rust.{}.example.{}", self.cargo_package_name, target_name)
76                    } else {
77                        format!("rust.{}", target_name)
78                    }
79                }),
80            package_label: primary_config
81                .and_then(|a| a.label.clone())
82                .or_else(|| {
83                    if is_default_target {
84                        self.default_target_config.label.clone()
85                    } else {
86                        None
87                    }
88                })
89                .unwrap_or_else(|| target_name.clone()),
90            version_code: primary_config
91                .and_then(|a| a.version_code)
92                .or_else(|| self.default_target_config.version_code)
93                .unwrap_or(1),
94            version_name: primary_config
95                .and_then(|a| a.version_name.clone())
96                .or_else(|| self.default_target_config.version_name.clone())
97                .unwrap_or_else(|| self.cargo_package_version.clone()),
98            package_icon: primary_config
99                .and_then(|a| a.icon.clone())
100                .or_else(|| self.default_target_config.icon.clone()),
101            assets_path: primary_config
102                .and_then(|a| a.assets.as_ref())
103                .or_else(|| self.default_target_config.assets.as_ref())
104                .map(|p| self.manifest_path.parent().unwrap().join(p)),
105            res_path: primary_config
106                .and_then(|a| a.res.as_ref())
107                .or_else(|| self.default_target_config.res.as_ref())
108                .map(|p| self.manifest_path.parent().unwrap().join(p)),
109            fullscreen: primary_config
110                .and_then(|a| a.fullscreen)
111                .or_else(|| self.default_target_config.fullscreen)
112                .unwrap_or(false),
113            application_attributes: primary_config
114                .and_then(|a| a.application_attributes.clone())
115                .or_else(|| self.default_target_config.application_attributes.clone())
116                .map(build_attribute_string),
117            activity_attributes: primary_config
118                .and_then(|a| a.activity_attributes.clone())
119                .or_else(|| self.default_target_config.activity_attributes.clone())
120                .map(build_attribute_string),
121            opengles_version_major: primary_config
122                .and_then(|a| a.opengles_version_major)
123                .or_else(|| self.default_target_config.opengles_version_major)
124                .unwrap_or(2),
125            opengles_version_minor: primary_config
126                .and_then(|a| a.opengles_version_minor)
127                .or_else(|| self.default_target_config.opengles_version_minor)
128                .unwrap_or(0),
129            features: primary_config
130                .and_then(|a| a.feature.clone())
131                .or_else(|| self.default_target_config.feature.clone())
132                .unwrap_or_else(Vec::new)
133                .into_iter()
134                .map(AndroidFeature::from)
135                .collect(),
136            permissions: primary_config
137                .and_then(|a| a.permission.clone())
138                .or_else(|| self.default_target_config.permission.clone())
139                .unwrap_or_else(Vec::new)
140                .into_iter()
141                .map(AndroidPermission::from)
142                .collect(),
143        })
144    }
145}
146
147/// Build targets supported by NDK
148#[derive(Debug, Copy, Clone, Deserialize)]
149pub enum AndroidBuildTarget {
150    #[serde(rename(deserialize = "armv7-linux-androideabi"))]
151    ArmV7a,
152    #[serde(rename(deserialize = "aarch64-linux-android"))]
153    Arm64V8a,
154    #[serde(rename(deserialize = "i686-linux-android"))]
155    X86,
156    #[serde(rename(deserialize = "x86_64-linux-android"))]
157    X86_64,
158}
159
160#[derive(Clone)]
161pub struct AndroidFeature {
162    pub name: String,
163    pub required: bool,
164    pub version: Option<String>,
165}
166
167impl From<TomlFeature> for AndroidFeature {
168    fn from(f: TomlFeature) -> Self {
169        AndroidFeature {
170            name: f.name,
171            required: f.required.unwrap_or(true),
172            version: f.version,
173        }
174    }
175}
176
177#[derive(Clone)]
178pub struct AndroidPermission {
179    pub name: String,
180    pub max_sdk_version: Option<u32>,
181}
182
183impl From<TomlPermission> for AndroidPermission {
184    fn from(p: TomlPermission) -> Self {
185        AndroidPermission {
186            name: p.name,
187            max_sdk_version: p.max_sdk_version,
188        }
189    }
190}
191
192/// Android build settings for a specific target
193pub struct AndroidTargetConfig {
194    /// Name that the package will have on the Android machine.
195    /// This is the key that Android uses to identify your package, so it should be unique for
196    /// for each application and should contain the vendor's name.
197    pub package_name: String,
198
199    /// Label for the package.
200    pub package_label: String,
201
202    /// Internal version number for manifest.
203    pub version_code: i32,
204
205    /// Version number which is shown to users.
206    pub version_name: String,
207
208    /// Name of the launcher icon.
209    /// Versions of this icon with different resolutions have to reside in the res folder
210    pub package_icon: Option<String>,
211
212    /// If `Some`, a path that contains the list of assets to ship as part of the package.
213    ///
214    /// The assets can later be loaded with the runtime library.
215    pub assets_path: Option<PathBuf>,
216
217    /// If `Some`, a path that contains the list of resources to ship as part of the package.
218    ///
219    /// The resources can later be loaded with the runtime library.
220    /// This folder contains for example the launcher icon, the styles and resolution dependent images.
221    pub res_path: Option<PathBuf>,
222
223    /// Should this app be in fullscreen mode (hides the title bar)?
224    pub fullscreen: bool,
225
226    /// Appends this string to the application attributes in the AndroidManifest.xml
227    pub application_attributes: Option<String>,
228
229    /// Appends this string to the activity attributes in the AndroidManifest.xml
230    pub activity_attributes: Option<String>,
231
232    /// The OpenGL ES major version in the AndroidManifest.xml
233    pub opengles_version_major: u8,
234
235    /// The OpenGL ES minor version in the AndroidManifest.xml
236    pub opengles_version_minor: u8,
237
238    /// uses-feature in AndroidManifest.xml
239    pub features: Vec<AndroidFeature>,
240
241    /// uses-permission in AndroidManifest.xml
242    pub permissions: Vec<AndroidPermission>,
243}
244
245pub fn load(package: &Package) -> Result<AndroidConfig, CliError> {
246    // Determine the name of the package and the Android-specific metadata from the Cargo.toml
247    let manifest_content = {
248        // Load Cargo.toml & parse
249        let content = {
250            let mut file = File::open(package.manifest_path()).unwrap();
251            let mut content = String::new();
252            file.read_to_string(&mut content).unwrap();
253            content
254        };
255        let config: TomlConfig = toml::from_str(&content).map_err(failure::Error::from)?;
256        config.package.metadata.and_then(|m| m.android)
257    };
258
259    // Determine the NDK path
260    let ndk_path = env::var("NDK_HOME").map_err(|_| {
261        format_err!(
262            "Please set the path to the Android NDK with the \
263             $NDK_HOME environment variable."
264        )
265    })?;
266
267    let sdk_path = {
268        let mut sdk_path = env::var("ANDROID_SDK_HOME").ok();
269
270        if sdk_path.is_none() {
271            sdk_path = env::var("ANDROID_HOME").ok();
272        }
273
274        sdk_path.ok_or_else(|| {
275            format_err!(
276                "Please set the path to the Android SDK with either the $ANDROID_SDK_HOME or \
277                 the $ANDROID_HOME environment variable."
278            )
279        })?
280    };
281
282    // Find the highest build tools.
283    let build_tools_version = {
284        let dir = fs::read_dir(Path::new(&sdk_path).join("build-tools"))
285            .map_err(|_| format_err!("Android SDK has no build-tools directory"))?;
286
287        let mut versions = Vec::new();
288        for next in dir {
289            let next = next.unwrap();
290
291            let meta = next.metadata().unwrap();
292            if !meta.is_dir() {
293                if !meta.is_file() {
294                    // It seems, symlink is here, so we should follow it
295                    let meta = next.path().metadata().unwrap();
296
297                    if !meta.is_dir() {
298                        continue;
299                    }
300                } else {
301                    continue;
302                }
303            }
304
305            let file_name = next.file_name().into_string().unwrap();
306            if !file_name.chars().next().unwrap().is_digit(10) {
307                continue;
308            }
309
310            versions.push(file_name);
311        }
312
313        versions.sort_by(|a, b| b.cmp(&a));
314        versions
315            .into_iter()
316            .next()
317            .ok_or_else(|| format_err!("Unable to determine build tools version"))?
318    };
319
320    // Determine the Sdk versions (compile, target, min)
321    let android_version = manifest_content
322        .as_ref()
323        .and_then(|a| a.android_version)
324        .unwrap_or(29);
325
326    // Check that the tool for the android platform is installed
327    let android_jar_path = Path::new(&sdk_path)
328        .join("platforms")
329        .join(format!("android-{}", android_version))
330        .join("android.jar");
331    if !android_jar_path.exists() {
332        Err(format_err!(
333            "'{}' does not exist",
334            android_jar_path.to_string_lossy()
335        ))?;
336    }
337
338    let target_sdk_version = manifest_content
339        .as_ref()
340        .and_then(|a| a.target_sdk_version)
341        .unwrap_or(android_version);
342    let min_sdk_version = manifest_content
343        .as_ref()
344        .and_then(|a| a.min_sdk_version)
345        .unwrap_or(18);
346
347    let default_target_config = manifest_content
348        .as_ref()
349        .map(|a| a.default_target_config.clone())
350        .unwrap_or_else(Default::default);
351
352    let mut target_configs = BTreeMap::new();
353    manifest_content
354        .as_ref()
355        .and_then(|a| a.bin.as_ref())
356        .unwrap_or(&Vec::new())
357        .iter()
358        .for_each(|t| {
359            target_configs.insert((TargetKind::Bin, t.name.clone()), t.config.clone());
360        });
361    manifest_content
362        .as_ref()
363        .and_then(|a| a.example.as_ref())
364        .unwrap_or(&Vec::new())
365        .iter()
366        .for_each(|t| {
367            target_configs.insert((TargetKind::ExampleBin, t.name.clone()), t.config.clone());
368        });
369
370    // For the moment some fields of the config are dummies.
371    Ok(AndroidConfig {
372        cargo_package_name: package.name().to_string(),
373        cargo_package_version: package.version().to_string(),
374        manifest_path: package.manifest_path().to_owned(),
375        sdk_path: Path::new(&sdk_path).to_owned(),
376        ndk_path: Path::new(&ndk_path).to_owned(),
377        android_jar_path,
378        target_sdk_version,
379        min_sdk_version,
380        build_tools_version,
381        release: false,
382        build_targets: manifest_content
383            .as_ref()
384            .and_then(|a| a.build_targets.clone())
385            .unwrap_or_else(|| {
386                vec![
387                    AndroidBuildTarget::ArmV7a,
388                    AndroidBuildTarget::Arm64V8a,
389                    AndroidBuildTarget::X86,
390                ]
391            }),
392        default_target_config,
393        target_configs,
394    })
395}
396
397fn build_attribute_string(input_map: BTreeMap<String, String>) -> String {
398    input_map
399        .iter()
400        .map(|(key, val)| format!("\n{}=\"{}\"", key, val))
401        .join("")
402}
403
404#[derive(Debug, Clone, Deserialize)]
405struct TomlConfig {
406    package: TomlPackage,
407}
408
409#[derive(Debug, Clone, Deserialize)]
410struct TomlPackage {
411    name: String,
412    metadata: Option<TomlMetadata>,
413}
414
415#[derive(Debug, Clone, Deserialize)]
416struct TomlMetadata {
417    android: Option<TomlAndroid>,
418}
419
420#[derive(Debug, Clone, Deserialize)]
421#[serde(deny_unknown_fields)]
422struct TomlAndroid {
423    android_version: Option<u32>,
424    target_sdk_version: Option<u32>,
425    min_sdk_version: Option<u32>,
426    build_targets: Option<Vec<AndroidBuildTarget>>,
427
428    #[serde(flatten)]
429    default_target_config: TomlAndroidTarget,
430
431    bin: Option<Vec<TomlAndroidSpecificTarget>>,
432    example: Option<Vec<TomlAndroidSpecificTarget>>,
433}
434
435#[derive(Debug, Clone, Deserialize)]
436#[serde(deny_unknown_fields)]
437pub struct TomlFeature {
438    name: String,
439    required: Option<bool>,
440    version: Option<String>,
441}
442
443#[derive(Debug, Clone, Deserialize)]
444#[serde(deny_unknown_fields)]
445pub struct TomlPermission {
446    name: String,
447    max_sdk_version: Option<u32>,
448}
449
450/// Configuration specific to a single cargo target
451#[derive(Debug, Clone, Deserialize)]
452#[serde(deny_unknown_fields)]
453struct TomlAndroidSpecificTarget {
454    name: String,
455
456    #[serde(flatten)]
457    config: TomlAndroidTarget,
458}
459
460#[derive(Debug, Default, Clone, Deserialize)]
461pub struct TomlAndroidTarget {
462    pub package_name: Option<String>,
463    pub label: Option<String>,
464    pub version_code: Option<i32>,
465    pub version_name: Option<String>,
466    pub icon: Option<String>,
467    pub assets: Option<String>,
468    pub res: Option<String>,
469    pub fullscreen: Option<bool>,
470    pub application_attributes: Option<BTreeMap<String, String>>,
471    pub activity_attributes: Option<BTreeMap<String, String>>,
472    pub opengles_version_major: Option<u8>,
473    pub opengles_version_minor: Option<u8>,
474    pub feature: Option<Vec<TomlFeature>>,
475    pub permission: Option<Vec<TomlPermission>>,
476}