uniui_build 0.0.6

Builds uniui applicatins for different targets
Documentation
use std::collections::HashMap;

use ::uni_components::ui_page::UiPage;

use crate::SimpleUiPage;

mod manifest_parsing;
mod template_instantiation;

const ADB: &'static str = "$ADB$";
const DIR: &'static str = "$DIR$";

/// Builds list of ui-crates into APK
///
/// Each ui-crate should use [uniui_gui::u_main!] to identify main funciton.
///
/// [[package.metadata.uni_android]] section shuld be configured inside crate's
/// Cargo.toml. It may contains few sub-sections:
/// * main_function - name of the main function marked by [uniui_gui::u_main!]
/// * java_files - the files will be copied into APK tree for the package which name will
///   be the same as the crate name
/// * export_func_file - path to `export_func.json` file.
///
/// `export_func.json` file - array of [manifest_parsing::Func]. All functions
/// from the file will be reexported with related `Func::export` name.
pub struct AndroidBuilder {
	pages: Vec<(SimpleUiPage, bool)>,
	path_to_icon: String,
	app_id: String,
	sdk_location: String,
	ndk_locaiton: String,
	app_name: String,
	manifest_processor: crate::manifest_processor::ManifestProcessor,
}

impl AndroidBuilder {
	/// Creates new instance of `AndroidBuilder`
	pub fn new(
		path_to_icon: String,
		app_id: String,
		sdk_location: String,
		ndk_locaiton: String,
		app_name: String,
	) -> Self {
		let manifest_processor = crate::manifest_processor::ManifestProcessor::new();

		return Self {
			pages: Vec::new(),
			path_to_icon,
			app_id,
			sdk_location,
			ndk_locaiton,
			app_name,
			manifest_processor,
		};
	}

	/// Specify UiPage and module_name which implements the page
	///
	/// The crate_name should be the same as the crate name in
	/// [build-dependencies] secion of Cargo.toml
	///
	/// The crate should use [uniui_gui::u_main!] to identify main funciton.
	///
	/// [[package.metadata.uni_android.main_function]] section shuld be configured inside
	/// crate's Cargo.toml.
	pub fn add_page<T>(
		&mut self,
		page: &'static dyn UiPage<Data = T>,
		crate_name: &str,
	) {
		self.add_path(page.path(), crate_name, false);
	}

	/// Specify Launcher UiPage and module_name which implements the page
	///
	/// The crate_name should be the same as the crate name in
	/// [build-dependencies] secion of Cargo.toml
	///
	/// The crate should use [uniui_gui::u_main!] to identify main funciton.
	///
	/// [[package.metadata.uni_android.main_function]] section shuld be configured inside
	/// crate's Cargo.toml.
	///
	/// Provided UiPage will be used as an entry point into the app.
	/// Theoretically it's possible to have multiple entry points but it's not
	/// a common case.
	pub fn add_launcher<T>(
		&mut self,
		page: &'static dyn UiPage<Data = T>,
		crate_name: &str,
	) {
		self.add_path(page.path(), crate_name, true);
	}

	pub fn execute(self) {
		let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR have to be setted up");

		// contains all packages (including transitive dependency) which has
		// configured [[package.metadata.uni_android]] section.
		let map: HashMap<_, _> = {
			let mut map = HashMap::new();

			let resolved = {
				let config = cargo::util::config::Config::default()
					.expect("Config creation failed");

				let mut pr = cargo::core::registry::PackageRegistry::new(&config)
					.expect("Package registry failed");
				pr.lock_patches();

				let path_to_cargotoml =
					format!("{}/Cargo.toml", self.manifest_processor.manifest_path());
				let path = std::path::Path::new(&path_to_cargotoml);

				let source_id = cargo::core::SourceId::for_path(path.clone())
					.expect("Source id creation failed");


				let (manifest, _) =
					cargo::util::toml::read_manifest(&path, source_id, &config).unwrap();
				let m = match manifest {
					cargo::core::manifest::EitherManifest::Real(m) => m,
					cargo::core::manifest::EitherManifest::Virtual(v) => {
						panic!("virtual manifest:{:?}", v);
					},
				};
				let pkg = cargo::core::package::Package::new(m, path);
				let summary = pkg.summary().clone();
				let resolve_ops = cargo::core::resolver::ResolveOpts::everything();

				let try_to_use = std::collections::HashSet::new();

				let _lock = config
					.acquire_package_cache_lock()
					.expect("package cache lock failed");
				cargo::core::resolver::resolve(
					&[(summary, resolve_ops)], // summaries: &[(Summary, ResolveOpts)],
					&[],                       /* replacements: &[(PackageIdSpec,
					                            * Dependency)], */
					&mut pr,     // registry: &mut dyn Registry,
					&try_to_use, // try_to_use: &HashSet<PackageId>,
					None,        // config: Option<&Config>,
					false,       // check_public_visible_dependencies: bool
				)
				.expect("Resolve failed")
			};

			for (page, _) in self.pages.iter() {
				manifest_parsing::process_manifest(
					page.module_name().to_owned(),
					page.module_path().to_owned(),
					&mut map,
				);

				for pkg in resolved.iter() {
					let sumary = resolved.summary(pkg);
					let name: String = sumary.name().to_string();
					let path = self
						.manifest_processor
						.get_manifest_path(&name)
						.expect("Crate path not found. Sounds unsoundness.");
					manifest_parsing::process_manifest(name, path, &mut map);
				}
			}

			map.iter().filter_map(|(x, c)| c.clone().map(|c| (x.clone(), c))).collect()
		};

		// construct list with dependencies
		//
		// we should add all packages with configured [[package.metadata.uni_android]]
		// to our dependencies list because we will reexport it's functions
		let deps = map
			.iter()
			.map(|(n, c)| format!("{} = {{ path=\"{}\" }}", n, c.manifest_path))
			.collect::<Vec<_>>()
			.join("\n");
		let deps = format!(
			"{}\n{}\n",
			deps,
			"uni_tmp_jni = { version = \"0.17.0\", path = \
			 \"/zprojects/maze/src/tmp/jni-rs/\"}"
		);

		// generate reexports for all functions from
		// [[package.metadata.uni_android.export_func_file]]
		let lib_content = map
			.iter()
			.flat_map(|(name, config)| {
				config.funcs.iter().flat_map(move |funcs| {
					funcs.iter().map(move |f| {
						let params = f
							.params
							.iter()
							.enumerate()
							.map(|(id, t)| format!("_a{}: {}", id, t))
							.collect::<Vec<_>>()
							.join(",");
						let params_2 = f
							.params
							.iter()
							.enumerate()
							.map(|(id, _)| format!("_a{}", id))
							.collect::<Vec<_>>()
							.join(",");
						format!(
							"
						#[no_mangle]
						#[allow(non_snake_case)]
						pub extern \"C\" fn {}(
							{}
						) -> {} {{
							return {}::{}({});
						}}
					",
							f.export, params, f.result, name, f.name, params_2
						)
					})
				})
			})
			.collect::<Vec<_>>()
			.join("\n");

		// collect all activities
		let activities = {
			let mut activities = Vec::new();
			for (page, is_launcher) in self.pages.iter() {
				activities.push(template_instantiation::ActivityInfo {
					name: activity_name_for(&page),
					is_launcher: *is_launcher,
				});
			}

			activities
		};

		// the name for new package which will contain generic java classes
		let package = format!("com.uniuirust.buildfor.{}", self.app_id);

		// Generate reexports for uniui_gui::AndroidApplication for each activity
		let lib_content_2 = self
			.pages
			.iter()
			.map(|(s, _)| {
				let m_path = s.module_path().to_owned();
				let config = manifest_parsing::get_config(m_path.clone());
				let main = config
					.expect(&format!("UI crates without metadata.uni_android:{}", m_path))
					.main_function
					.expect(&format!(
						"uni_android.main_function not specified in manifest:{}",
						m_path
					));
				let on_create_name = format!(
					"Java_{}_{}_onCreateNative",
					package.clone().replace("_", "_1").replace(".", "_"),
					activity_name_for(&s).replace("_", "_1"),
				);

				let on_tick_name = format!(
					"Java_{}_{}_onTickNative",
					package.clone().replace("_", "_1").replace(".", "_"),
					activity_name_for(&s).replace("_", "_1"),
				);

				let on_cleanup_name = format!(
					"Java_{}_{}_onCleanupNative",
					package.clone().replace("_", "_1").replace(".", "_"),
					activity_name_for(&s).replace("_", "_1"),
				);

				format!(
					"
					#[no_mangle]
					#[allow(non_snake_case)]
					pub extern \"C\" fn {}(
						env: jni::JNIEnv,
						activity: jni::objects::JObject
					) {{
						{}::{}::{}(env, activity);
					}}

					#[no_mangle]
					#[allow(non_snake_case)]
					pub extern \"C\" fn {}(
						env: jni::JNIEnv,
						activity: jni::objects::JObject,
						addr: jni::sys::jlong,
					) -> jni::sys::jlong {{
						return {}::{}::{}(env, activity, addr);
					}}

					#[no_mangle]
					#[allow(non_snake_case)]
					pub extern \"C\" fn {}(
						env: jni::JNIEnv,
						activity: jni::objects::JObject,
						addr: jni::sys::jlong,
					) {{
						{}::{}::{}(env, activity, addr);
					}}
				",
					on_create_name,
					s.module_name(),
					crate::android_module_name(main.clone()),
					crate::RUN_FUNCTION_NAME,
					on_tick_name,
					s.module_name(),
					crate::android_module_name(main.clone()),
					crate::TICK_FUNCTION_NAME,
					on_cleanup_name,
					s.module_name(),
					crate::android_module_name(main),
					crate::CLEANUP_FUNCTION_NAME,
				)
			})
			.collect::<Vec<_>>()
			.join("\n");


		let lib_content = format!(
			"extern crate uni_tmp_jni as jni;\n{}",
			vec![lib_content, lib_content_2].join("\n")
		);


		let dir = format!("{}/android", std::env::var("OUT_DIR").unwrap());
		let arch_list = "\"arm\", \"x86\", \"arm64\", \"x86_64\"";

		template_instantiation::instantiate_for(
			&dir,
			&deps,
			arch_list,
			&self.app_id,
			&self.sdk_location,
			&self.ndk_locaiton,
			&package,
			&self.app_name,
			&lib_content,
			activities,
		)
		.expect("Template instantiation failed");

		// copy all [[package.metadata.uni_android.java_files]]
		{
			for (name, config) in map.iter() {
				if let Some(files_list) = config.java_files.as_ref() {
					for file in files_list {
						let dest_dir = format!("{}/app/src/main/java/{}", dir, name);
						{
							// Otherwise canonicalize will fail
							std::fs::create_dir_all(&dest_dir)
								.expect(&format!("Can't create dir:{}", dest_dir,));

							let dest_dir = std::path::Path::new(&dest_dir);
							let dest_dir =
								dest_dir.canonicalize().expect("Canonicalization failed");
							match dest_dir.starts_with(&out_dir) {
								true => {
									// we believe it's safe because we checked that the
									// directory is a part of "OUT_DIR".
									// Hope nobody will create a bug where the script
									// will delete User's content.
									// I have no idea how to fix the problem with old
									// files in the java directory in the other way.

									// Finally it looks like it's only affect native
									// developers. Commented for now to prevent potential
									// data lost
									//
									// std::fs::remove_dir_all(&dest_dir).expect(&format!
									// { 	"Can't remove directory {}",
									// 	dest_dir.display(),
									// });
								},
								false => {
									panic!(
										"Java direcotry for crate:{} is not a part of \
										 OUT directory subtree. Dir:{}",
										name,
										dest_dir.display(),
									);
								},
							}
						}

						std::fs::create_dir_all(&dest_dir)
							.expect(&format!("Can't create dir:{}", dest_dir,));

						let file_last_name = std::path::Path::new(file)
							.file_name()
							.expect(&format!("Can't extract filename from:{}", file))
							.to_str()
							.expect(&format!(
								"Can't extract filename as string from:{}",
								file
							));

						let source = format!("{}/{}", config.manifest_path, file);
						let destination = format!("{}/{}", dest_dir, file_last_name);

						std::fs::copy(&source, &destination).expect(&format!(
							"Copy failed source:{} destination:{}",
							source, destination,
						));
					}
				}
			}
		}

		let gradle_command = match std::env::var("PROFILE") == Ok("release".to_owned()) {
			true => "assembleRelease",
			false => "assembleDebug",
		};

		// run gradle:
		// * compile all crates for `android` into .so library
		// * compile all java/kotlin files
		let gradle_status = std::process::Command::new("./gradlew")
			.arg("--no-daemon")
			.arg(gradle_command)
			.current_dir(&dir)
			.status()
			.expect("Gradle build failed. Please see error above");

		if !gradle_status.success() {
			panic!("Gradle build failed. Please see error above");
		}

		// generate templates for application start simplification
		let file_name = format!("{}/uni_build_generated.rs", out_dir);
		let rust = std::include_str!("./build_android/templates/rust.rs")
			.replace(ADB, &format!("{}/platform-tools/adb", self.sdk_location))
			.replace(DIR, &dir);
		std::fs::write(&file_name, &rust).expect("Rust helpers write failed");

		// to suppress `unused` warning
		let _ = self.path_to_icon.clone();
	}

	fn add_path(
		&mut self,
		path: &str,
		crate_name: &str,
		is_launcher: bool,
	) {
		let manifest_path =
			self.manifest_processor.get_manifest_path(crate_name).expect(&format!(
				"Crate:{} not found in manifest:{}. Please add the crate into \
				 [build-dependencies] list",
				crate_name,
				self.manifest_processor.manifest_path(),
			));

		self.pages.push((
			SimpleUiPage::new(path.to_owned(), crate_name.to_owned(), manifest_path),
			is_launcher,
		));
	}
}

fn activity_name_for(s: &SimpleUiPage) -> String {
	return s.module_name().to_owned();
}