use std::os;
use std::collections::{HashSet, HashMap};
use std::env;
use std::fs;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::process::{Command, Stdio};
use termcmd::TermCmd;
use config::Config;
pub struct BuildResult {
pub apk_path: PathBuf,
}
pub fn build(manifest_path: &Path, config: &Config) -> BuildResult {
match Command::new(&config.ant_command).arg("-version").stdout(Stdio::null()).status() {
Ok(s) if s.success() => (),
_ => {
println!("Could not execute `ant`. Did you install it?");
exit(1);
}
}
pub trait AddRustcReleaseFlag {
fn add_rustc_release_flag(&mut self, config: &Config) -> &mut Self;
}
pub trait AddGccReleaseFlag {
fn add_gcc_release_flag(&mut self, config: &Config) -> &mut Self;
}
pub trait AddCargoReleaseFlag {
fn add_cargo_release_flag(&mut self, config: &Config) -> &mut Self;
}
pub trait AddAntReleaseFlag {
fn add_ant_release_flag(&mut self, config: &Config) -> &mut Self;
}
pub trait AddCargoTargetFlag {
fn add_cargo_target_flag(&mut self, config: &Config) -> &mut Self;
}
impl AddGccReleaseFlag for TermCmd {
fn add_gcc_release_flag(&mut self, config: &Config) -> &mut TermCmd {
if config.release {
self.arg("-O3")
} else {
self
}
}
}
impl AddRustcReleaseFlag for TermCmd {
fn add_rustc_release_flag(&mut self, config: &Config) -> &mut TermCmd {
if config.release {
self.arg("-C")
.arg("opt-level=3")
} else {
self
}
}
}
impl AddCargoReleaseFlag for TermCmd {
fn add_cargo_release_flag(&mut self, config: &Config) -> &mut TermCmd {
if config.release {
self.arg("--release")
} else {
self
}
}
}
impl AddAntReleaseFlag for TermCmd {
fn add_ant_release_flag(&mut self, config: &Config) -> &mut TermCmd {
if config.release {
self.arg("release")
} else {
self.arg("debug")
}
}
}
impl AddCargoTargetFlag for TermCmd {
fn add_cargo_target_flag(&mut self, config: &Config) -> &mut TermCmd {
if let Some(ref target) = config.target {
self.arg("--bin").arg(target)
} else {
self
}
}
}
let android_artifacts_dir = {
let target_dir = manifest_path.parent().unwrap().join("target");
target_dir.join("android-artifacts")
};
build_android_artifacts_dir(&android_artifacts_dir, &config);
let mut abi_libs: HashMap<&str, Vec<String>> = HashMap::new();
for build_target in config.build_targets.iter() {
let build_target_dir = android_artifacts_dir.join(build_target);
let (gcc_path, ar_path) = {
let host_os = if cfg!(target_os = "windows") { "windows" }
else if cfg!(target_os = "linux") { "linux" }
else if cfg!(target_os = "macos") { "darwin" }
else { panic!("Unknown or incompatible host OS") };
let target_arch = if build_target.starts_with("arm") { "arm-linux-androideabi" }
else if build_target.starts_with("aarch64") { "aarch64-linux-android" }
else if build_target.starts_with("i") { "x86" }
else if build_target.starts_with("x86_64") { "x86_64" }
else if build_target.starts_with("mipsel") { "mipsel-linux-android" }
else { panic!("Unknown or incompatible build target: {}", build_target) };
let tool_prefix = if build_target.starts_with("arm") { "arm-linux-androideabi" }
else if build_target.starts_with("aarch64") { "aarch64-linux-android" }
else if build_target.starts_with("i") { "i686-linux-android" }
else if build_target.starts_with("x86_64") { "x86_64-linux-android" }
else if build_target.starts_with("mipsel") { "mipsel-linux-android" }
else { panic!("Unknown or incompatible build target: {}", build_target) };
let base = config.ndk_path.join(format!("toolchains/{}-4.9/prebuilt/{}-x86_64", target_arch, host_os));
(base.join(format!("bin/{}-gcc", tool_prefix)), base.join(format!("bin/{}-ar", tool_prefix)))
};
let gcc_sysroot = {
let arch = if build_target.starts_with("arm") { "arm" }
else if build_target.starts_with("aarch64") { "arm64" }
else if build_target.starts_with("i") { "x86" }
else if build_target.starts_with("x86_64") { "x86_64" }
else if build_target.starts_with("mips") { "mips" }
else { panic!("Unknown or incompatible build target: {}", build_target) };
config.ndk_path.join(format!("platforms/android-{v}/arch-{a}",
v = config.android_version, a = arch))
};
let abi = if build_target.starts_with("arm") { "armeabi" }
else if build_target.starts_with("aarch64") { "arm64-v8a" }
else if build_target.starts_with("i") { "x86" }
else if build_target.starts_with("x86_64") { "x86_64" }
else if build_target.starts_with("mips") { "mips" }
else { panic!("Unknown or incompatible build target: {}", build_target) };
TermCmd::new("Compiling android_native_app_glue.c", &gcc_path)
.arg(config.ndk_path.join("sources/android/native_app_glue/android_native_app_glue.c"))
.arg("-c")
.add_gcc_release_flag(config)
.arg("-o").arg(build_target_dir.join("android_native_app_glue.o"))
.arg("--sysroot").arg(&gcc_sysroot)
.execute();
let injected_glue_lib = {
let mut cmd = TermCmd::new("Compiling injected-glue", "rustc");
cmd.arg(android_artifacts_dir.join("injected-glue/lib.rs"))
.arg("--crate-type").arg("rlib")
.add_rustc_release_flag(config)
.arg("--crate-name").arg("cargo_apk_injected_glue")
.arg("--target").arg(build_target)
.arg("--out-dir").arg(&build_target_dir);
cmd.execute();
let stdout = cmd.arg("--print").arg("file-names")
.exec_stdout();
let stdout = String::from_utf8(stdout).unwrap();
build_target_dir.join(stdout.lines().next().unwrap())
};
{
let mut file = File::create(build_target_dir.join("glue_obj.rs")).unwrap();
file.write_all(&include_bytes!("../glue_obj.rs")[..]).unwrap();
}
TermCmd::new("Compiling glue_obj", "rustc")
.arg(build_target_dir.join("glue_obj.rs"))
.arg("--crate-type").arg("staticlib")
.add_rustc_release_flag(config)
.arg("--target").arg(build_target)
.arg("--extern").arg(format!("cargo_apk_injected_glue={}", injected_glue_lib.to_string_lossy()))
.arg("--emit").arg("obj")
.arg("-o").arg(build_target_dir.join("glue_obj.o"))
.execute();
let native_libraries_dir = android_artifacts_dir.join(format!("build/libs/{}", abi));
if fs::metadata(&native_libraries_dir).is_err() {
fs::DirBuilder::new().recursive(true).create(&native_libraries_dir).unwrap();
}
TermCmd::new("Compiling crate", "cargo").arg("rustc")
.arg("--target").arg(build_target)
.add_cargo_target_flag(config)
.add_cargo_release_flag(config)
.arg("--")
.arg("-C").arg(format!("linker={}", android_artifacts_dir.join(if cfg!(target_os = "windows") { "linker_exe.exe" } else { "linker_exe" })
.to_string_lossy()))
.arg("--extern").arg(format!("cargo_apk_injected_glue={}", injected_glue_lib.to_string_lossy()))
.inherit_stdouterr()
.env("CARGO_APK_GCC", gcc_path.as_os_str())
.env("CARGO_APK_GCC_SYSROOT", gcc_sysroot.as_os_str())
.env("CARGO_APK_NATIVE_APP_GLUE", build_target_dir.join("android_native_app_glue.o"))
.env("CARGO_APK_GLUE_OBJ", build_target_dir.join("glue_obj.o"))
.env("CARGO_APK_GLUE_LIB", injected_glue_lib)
.env("CARGO_APK_LINKER_OUTPUT", native_libraries_dir.join("libmain.so"))
.env("CARGO_APK_LIB_PATHS_OUTPUT", build_target_dir.join("lib_paths"))
.env("CARGO_APK_LIBS_OUTPUT", build_target_dir.join("libs"))
.env("TARGET_CC", gcc_path.as_os_str()) .env("TARGET_AR", ar_path.as_os_str()) .env("TARGET_CFLAGS", &format!("--sysroot {}", gcc_sysroot.to_string_lossy())) .execute();
let shared_objects_to_load = {
let lib_paths: Vec<String> = {
if let Ok(f) = File::open(build_target_dir.join("lib_paths")) {
let l = BufReader::new(f);
l.lines().map(|l| l.unwrap()).collect()
} else {
vec![]
}
};
let libs_list: HashSet<String> = {
if let Ok(f) = File::open(build_target_dir.join("libs")) {
let l = BufReader::new(f);
l.lines().map(|l| l.unwrap()).collect()
} else {
HashSet::new()
}
};
let mut shared_objects_to_load = Vec::new();
for dir in lib_paths.iter() {
fs::read_dir(&dir).and_then(|paths| {
for path in paths {
let path = path.unwrap().path();
match (path.file_name(), path.extension()) {
(Some(filename), Some(ext)) => {
let filename = filename.to_str().unwrap();
if filename.starts_with("lib") && ext == "so" &&
libs_list.contains(filename)
{
shared_objects_to_load.push(filename.to_owned());
fs::copy(&path, native_libraries_dir.join(filename)).unwrap();
}
}
_ => {}
}
}
Ok(())
}).ok();
}
shared_objects_to_load
};
abi_libs.insert(abi, shared_objects_to_load);
}
build_java_src(&android_artifacts_dir, &config, &abi_libs);
TermCmd::new("Invoking ant", &config.ant_command)
.add_ant_release_flag(config)
.current_dir(android_artifacts_dir.join("build"))
.execute();
BuildResult {
apk_path: android_artifacts_dir.join(format!("build/bin/{}-debug.apk", config.project_name)),
}
}
fn build_android_artifacts_dir(path: &Path, config: &Config) {
if fs::metadata(path.join("build")).is_err() {
fs::DirBuilder::new().recursive(true).create(path.join("build")).unwrap();
}
{
fs::create_dir_all(path.join("injected-glue")).unwrap();
let mut lib = File::create(path.join("injected-glue/lib.rs")).unwrap();
lib.write_all(&include_bytes!("../injected-glue/lib.rs")[..]).unwrap();
let mut ffi = File::create(path.join("injected-glue/ffi.rs")).unwrap();
ffi.write_all(&include_bytes!("../injected-glue/ffi.rs")[..]).unwrap();
}
build_linker(path);
build_manifest(path, config);
build_build_xml(path, config);
build_local_properties(path, config);
build_project_properties(path, config);
build_assets(path, config);
build_res(path, config);
for target in config.build_targets.iter() {
if fs::metadata(path.join(target)).is_err() {
fs::DirBuilder::new().recursive(true).create(path.join(target)).unwrap();
}
}
}
fn build_linker(path: &Path) {
let exe_file = path.join(if cfg!(target_os = "windows") { "linker_exe.exe" } else { "linker_exe" });
let src_file = path.join("linker_src");
{
let mut src_write = fs::File::create(&src_file).unwrap();
src_write.write_all(&include_bytes!("../linker.rs")[..]).unwrap();
}
let status = Command::new("rustc").arg(src_file).arg("-o").arg(&exe_file).status().unwrap();
assert!(status.success());
assert!(fs::metadata(&exe_file).is_ok());
}
fn build_java_src(path: &Path, config: &Config, abi_libs: &HashMap<&str, Vec<String>>)
{
let file = path.join("build/src/rust").join(config.project_name.replace("-", "_"))
.join("MainActivity.java");
fs::create_dir_all(file.parent().unwrap()).unwrap();
let mut file = File::create(&file).unwrap();
let mut libs_string = String::new();
for (abi, libs) in abi_libs {
libs_string.push_str(format!(" if (abi.equals(\"{}\")) {{\n",abi).as_str());
libs_string.push_str(format!(" matched_an_abi = true;\n").as_str());
for name in libs {
let line = format!(" System.loadLibrary(\"{}\");\n",
name.trim_left_matches("lib").trim_right_matches(".so"));
libs_string.push_str(&*line);
}
libs_string.push_str(format!(" break;\n").as_str());
libs_string.push_str(format!(" }}\n").as_str());
}
write!(file, r#"package rust.{package_name};
import java.lang.UnsupportedOperationException;
import android.os.Build;
import android.util.Log;
public class MainActivity extends android.app.NativeActivity {{
static {{
String[] supported_abis;
try {{
supported_abis = (String[]) Build.class.getField("SUPPORTED_ABIS").get(null);
}} catch (Exception e) {{
// Assume that this is an older phone; use backwards-compatible targets.
supported_abis = new String[]{{Build.CPU_ABI, Build.CPU_ABI2}};
}}
boolean matched_an_abi = false;
for (String abi : supported_abis) {{
{libs}
}}
if (!matched_an_abi) {{
throw new UnsupportedOperationException("Could not find a native abi target to load");
}}
}}
}}"#, libs = libs_string, package_name = config.project_name.replace("-", "_")).unwrap();
}
fn build_manifest(path: &Path, config: &Config) {
let file = path.join("build/AndroidManifest.xml");
let mut file = File::create(&file).unwrap();
let application_attrs = format!(r#"
android:label="{0}"{1}{2}{3}"#,
config.package_label,
config.package_icon.as_ref().map_or(String::new(), |a| format!(r#"
android:icon="{}""#, a)),
if config.fullscreen { r#"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen""#
} else { "" },
config.application_attributes.as_ref().map_or(String::new(), |a| a.replace("\n","\n "))
);
let activity_attrs = format!(r#"
android:name="rust.{1}.MainActivity"
android:label="{0}"
android:configChanges="orientation|keyboardHidden|screenSize" {2}"#,
config.package_label,
config.project_name.replace("-", "_"),
config.activity_attributes.as_ref().map_or(String::new(), |a| a.replace("\n","\n "))
);
write!(
file, r#"<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{0}"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="9" />
<uses-feature android:glEsVersion="{1}" android:required="true"></uses-feature>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application {2} >
<activity {3} >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<!-- END_INCLUDE(manifest) -->
"#,
config.package_name.replace("-", "_"),
format!("0x{:04}{:04}", 2, 0), application_attrs,
activity_attrs
).unwrap();
}
fn build_assets(path: &Path, config: &Config) {
let src_path = match config.assets_path {
None => return,
Some(ref p) => p,
};
let dst_path = path.join("build/assets");
if !dst_path.exists() {
create_dir_symlink(&src_path, &dst_path).expect("Can not create symlink to assets");
}
}
fn build_res(path: &Path, config: &Config) {
let src_path = match config.res_path {
None => return,
Some(ref p) => p,
};
let dst_path = path.join("build/res");
if !dst_path.exists() {
create_dir_symlink(&src_path, &dst_path).expect("Can not create symlink to res");
}
}
#[cfg(target_os = "windows")]
fn create_dir_symlink(src_path: &Path, dst_path: &Path) -> io::Result<()> {
os::windows::fs::symlink_dir(&src_path, &dst_path)
}
#[cfg(not(target_os = "windows"))]
fn create_dir_symlink(src_path: &Path, dst_path: &Path) -> io::Result<()> {
os::unix::fs::symlink(&src_path, &dst_path)
}
fn build_build_xml(path: &Path, config: &Config) {
let file = path.join("build/build.xml");
let mut file = File::create(&file).unwrap();
write!(file, r#"<?xml version="1.0" encoding="UTF-8"?>
<project name="{project_name}" default="help">
<property file="local.properties" />
<loadproperties srcFile="project.properties" />
<import file="custom_rules.xml" optional="true" />
<import file="${{sdk.dir}}/tools/ant/build.xml" />
</project>
"#, project_name = config.project_name).unwrap()
}
fn build_local_properties(path: &Path, config: &Config) {
let file = path.join("build/local.properties");
let mut file = File::create(&file).unwrap();
let abs_dir = if config.sdk_path.is_absolute() {
config.sdk_path.clone()
} else {
env::current_dir().unwrap().join(&config.sdk_path)
};
if cfg!(target_os = "windows") {
write!(file, r"sdk.dir={}", abs_dir.to_str().unwrap().replace("\\", "\\\\")).unwrap();
} else {
write!(file, r"sdk.dir={}", abs_dir.to_str().unwrap()).unwrap();
}
}
fn build_project_properties(path: &Path, config: &Config) {
let file = path.join("build/project.properties");
let mut file = File::create(&file).unwrap();
write!(file, r"target=android-{}", config.android_version).unwrap();
}