use cargo::core::{Package, TargetKind};
use cargo::util::CargoResult;
use cargo::CliError;
use failure::format_err;
use itertools::Itertools;
use serde::Deserialize;
use std::collections::btree_map::BTreeMap;
use std::env;
use std::fs;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use toml;
#[derive(Clone)]
pub struct AndroidConfig {
pub cargo_package_name: String,
pub cargo_package_version: String,
pub manifest_path: PathBuf,
pub sdk_path: PathBuf,
pub ndk_path: PathBuf,
pub build_targets: Vec<AndroidBuildTarget>,
pub android_jar_path: PathBuf,
pub target_sdk_version: u32,
pub min_sdk_version: u32,
pub build_tools_version: String,
pub release: bool,
pub default_target_config: TomlAndroidTarget,
target_configs: BTreeMap<(TargetKind, String), TomlAndroidTarget>,
}
impl AndroidConfig {
pub fn resolve(&self, target: (TargetKind, String)) -> CargoResult<AndroidTargetConfig> {
let primary_config = self.target_configs.get(&target);
let target_name = target.1;
let is_default_target = target_name == self.cargo_package_name;
let example = target.0 == TargetKind::ExampleBin;
Ok(AndroidTargetConfig {
package_name: primary_config
.and_then(|a| a.package_name.clone())
.or_else(|| {
if is_default_target {
self.default_target_config.package_name.clone()
} else {
None
}
})
.unwrap_or_else(|| {
if example {
format!("rust.{}.example.{}", self.cargo_package_name, target_name)
} else {
format!("rust.{}", target_name)
}
}),
package_label: primary_config
.and_then(|a| a.label.clone())
.or_else(|| {
if is_default_target {
self.default_target_config.label.clone()
} else {
None
}
})
.unwrap_or_else(|| target_name.clone()),
version_code: primary_config
.and_then(|a| a.version_code)
.or_else(|| self.default_target_config.version_code)
.unwrap_or(1),
version_name: primary_config
.and_then(|a| a.version_name.clone())
.or_else(|| self.default_target_config.version_name.clone())
.unwrap_or_else(|| self.cargo_package_version.clone()),
package_icon: primary_config
.and_then(|a| a.icon.clone())
.or_else(|| self.default_target_config.icon.clone()),
assets_path: primary_config
.and_then(|a| a.assets.as_ref())
.or_else(|| self.default_target_config.assets.as_ref())
.map(|p| self.manifest_path.parent().unwrap().join(p)),
res_path: primary_config
.and_then(|a| a.res.as_ref())
.or_else(|| self.default_target_config.res.as_ref())
.map(|p| self.manifest_path.parent().unwrap().join(p)),
fullscreen: primary_config
.and_then(|a| a.fullscreen)
.or_else(|| self.default_target_config.fullscreen)
.unwrap_or(false),
application_attributes: primary_config
.and_then(|a| a.application_attributes.clone())
.or_else(|| self.default_target_config.application_attributes.clone())
.map(build_attribute_string),
activity_attributes: primary_config
.and_then(|a| a.activity_attributes.clone())
.or_else(|| self.default_target_config.activity_attributes.clone())
.map(build_attribute_string),
opengles_version_major: primary_config
.and_then(|a| a.opengles_version_major)
.or_else(|| self.default_target_config.opengles_version_major)
.unwrap_or(2),
opengles_version_minor: primary_config
.and_then(|a| a.opengles_version_minor)
.or_else(|| self.default_target_config.opengles_version_minor)
.unwrap_or(0),
features: primary_config
.and_then(|a| a.feature.clone())
.or_else(|| self.default_target_config.feature.clone())
.unwrap_or_else(Vec::new)
.into_iter()
.map(AndroidFeature::from)
.collect(),
permissions: primary_config
.and_then(|a| a.permission.clone())
.or_else(|| self.default_target_config.permission.clone())
.unwrap_or_else(Vec::new)
.into_iter()
.map(AndroidPermission::from)
.collect(),
})
}
}
#[derive(Debug, Copy, Clone, Deserialize)]
pub enum AndroidBuildTarget {
#[serde(rename(deserialize = "armv7-linux-androideabi"))]
ArmV7a,
#[serde(rename(deserialize = "aarch64-linux-android"))]
Arm64V8a,
#[serde(rename(deserialize = "i686-linux-android"))]
X86,
#[serde(rename(deserialize = "x86_64-linux-android"))]
X86_64,
}
#[derive(Clone)]
pub struct AndroidFeature {
pub name: String,
pub required: bool,
pub version: Option<String>,
}
impl From<TomlFeature> for AndroidFeature {
fn from(f: TomlFeature) -> Self {
AndroidFeature {
name: f.name,
required: f.required.unwrap_or(true),
version: f.version,
}
}
}
#[derive(Clone)]
pub struct AndroidPermission {
pub name: String,
pub max_sdk_version: Option<u32>,
}
impl From<TomlPermission> for AndroidPermission {
fn from(p: TomlPermission) -> Self {
AndroidPermission {
name: p.name,
max_sdk_version: p.max_sdk_version,
}
}
}
pub struct AndroidTargetConfig {
pub package_name: String,
pub package_label: String,
pub version_code: i32,
pub version_name: String,
pub package_icon: Option<String>,
pub assets_path: Option<PathBuf>,
pub res_path: Option<PathBuf>,
pub fullscreen: bool,
pub application_attributes: Option<String>,
pub activity_attributes: Option<String>,
pub opengles_version_major: u8,
pub opengles_version_minor: u8,
pub features: Vec<AndroidFeature>,
pub permissions: Vec<AndroidPermission>,
}
pub fn load(package: &Package) -> Result<AndroidConfig, CliError> {
let manifest_content = {
let content = {
let mut file = File::open(package.manifest_path()).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
content
};
let config: TomlConfig = toml::from_str(&content).map_err(failure::Error::from)?;
config.package.metadata.and_then(|m| m.android)
};
let ndk_path = env::var("NDK_HOME").map_err(|_| {
format_err!(
"Please set the path to the Android NDK with the \
$NDK_HOME environment variable."
)
})?;
let sdk_path = {
let mut sdk_path = env::var("ANDROID_SDK_HOME").ok();
if sdk_path.is_none() {
sdk_path = env::var("ANDROID_HOME").ok();
}
sdk_path.ok_or_else(|| {
format_err!(
"Please set the path to the Android SDK with either the $ANDROID_SDK_HOME or \
the $ANDROID_HOME environment variable."
)
})?
};
let build_tools_version = {
let dir = fs::read_dir(Path::new(&sdk_path).join("build-tools"))
.map_err(|_| format_err!("Android SDK has no build-tools directory"))?;
let mut versions = Vec::new();
for next in dir {
let next = next.unwrap();
let meta = next.metadata().unwrap();
if !meta.is_dir() {
if !meta.is_file() {
let meta = next.path().metadata().unwrap();
if !meta.is_dir() {
continue;
}
} else {
continue;
}
}
let file_name = next.file_name().into_string().unwrap();
if !file_name.chars().next().unwrap().is_digit(10) {
continue;
}
versions.push(file_name);
}
versions.sort_by(|a, b| b.cmp(&a));
versions
.into_iter()
.next()
.ok_or_else(|| format_err!("Unable to determine build tools version"))?
};
let android_version = manifest_content
.as_ref()
.and_then(|a| a.android_version)
.unwrap_or(29);
let android_jar_path = Path::new(&sdk_path)
.join("platforms")
.join(format!("android-{}", android_version))
.join("android.jar");
if !android_jar_path.exists() {
Err(format_err!(
"'{}' does not exist",
android_jar_path.to_string_lossy()
))?;
}
let target_sdk_version = manifest_content
.as_ref()
.and_then(|a| a.target_sdk_version)
.unwrap_or(android_version);
let min_sdk_version = manifest_content
.as_ref()
.and_then(|a| a.min_sdk_version)
.unwrap_or(18);
let default_target_config = manifest_content
.as_ref()
.map(|a| a.default_target_config.clone())
.unwrap_or_else(Default::default);
let mut target_configs = BTreeMap::new();
manifest_content
.as_ref()
.and_then(|a| a.bin.as_ref())
.unwrap_or(&Vec::new())
.iter()
.for_each(|t| {
target_configs.insert((TargetKind::Bin, t.name.clone()), t.config.clone());
});
manifest_content
.as_ref()
.and_then(|a| a.example.as_ref())
.unwrap_or(&Vec::new())
.iter()
.for_each(|t| {
target_configs.insert((TargetKind::ExampleBin, t.name.clone()), t.config.clone());
});
Ok(AndroidConfig {
cargo_package_name: package.name().to_string(),
cargo_package_version: package.version().to_string(),
manifest_path: package.manifest_path().to_owned(),
sdk_path: Path::new(&sdk_path).to_owned(),
ndk_path: Path::new(&ndk_path).to_owned(),
android_jar_path,
target_sdk_version,
min_sdk_version,
build_tools_version,
release: false,
build_targets: manifest_content
.as_ref()
.and_then(|a| a.build_targets.clone())
.unwrap_or_else(|| {
vec![
AndroidBuildTarget::ArmV7a,
AndroidBuildTarget::Arm64V8a,
AndroidBuildTarget::X86,
]
}),
default_target_config,
target_configs,
})
}
fn build_attribute_string(input_map: BTreeMap<String, String>) -> String {
input_map
.iter()
.map(|(key, val)| format!("\n{}=\"{}\"", key, val))
.join("")
}
#[derive(Debug, Clone, Deserialize)]
struct TomlConfig {
package: TomlPackage,
}
#[derive(Debug, Clone, Deserialize)]
struct TomlPackage {
name: String,
metadata: Option<TomlMetadata>,
}
#[derive(Debug, Clone, Deserialize)]
struct TomlMetadata {
android: Option<TomlAndroid>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct TomlAndroid {
android_version: Option<u32>,
target_sdk_version: Option<u32>,
min_sdk_version: Option<u32>,
build_targets: Option<Vec<AndroidBuildTarget>>,
#[serde(flatten)]
default_target_config: TomlAndroidTarget,
bin: Option<Vec<TomlAndroidSpecificTarget>>,
example: Option<Vec<TomlAndroidSpecificTarget>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TomlFeature {
name: String,
required: Option<bool>,
version: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TomlPermission {
name: String,
max_sdk_version: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct TomlAndroidSpecificTarget {
name: String,
#[serde(flatten)]
config: TomlAndroidTarget,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct TomlAndroidTarget {
pub package_name: Option<String>,
pub label: Option<String>,
pub version_code: Option<i32>,
pub version_name: Option<String>,
pub icon: Option<String>,
pub assets: Option<String>,
pub res: Option<String>,
pub fullscreen: Option<bool>,
pub application_attributes: Option<BTreeMap<String, String>>,
pub activity_attributes: Option<BTreeMap<String, String>>,
pub opengles_version_major: Option<u8>,
pub opengles_version_minor: Option<u8>,
pub feature: Option<Vec<TomlFeature>>,
pub permission: Option<Vec<TomlPermission>>,
}