use super::IntegrateConfig;
use crate::integration::utils::{overlay_dir, replace_file_content};
use crate::misc::{IntegrationBackend, Template};
use anyhow::Result;
use include_dir::{include_dir, Dir};
use itertools::Itertools;
use log::warn;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
fn execute_overlay_dir(
current_reference_dir: &Dir,
replacements: &HashMap<&'static str, &str>,
dart_root: &Path,
config: &IntegrateConfig,
comment_out_files: Option<&[String]>,
include_ohos: bool,
) -> Result<()> {
overlay_dir(
current_reference_dir,
replacements,
dart_root,
&|target_path, reference_content, existing_content| {
modify_file(
target_path.into(),
reference_content,
existing_content,
replacements,
config.enable_local_dependency,
comment_out_files,
)
},
&|path| {
filter_file(
path,
config.enable_write_lib,
config.enable_integration_test,
include_ohos,
)
},
)
}
pub(super) fn execute_overlay_templates(
replacements: &HashMap<&'static str, &str>,
dart_root: &Path,
config: &IntegrateConfig,
include_ohos: bool,
dart_package_name: &str,
) -> Result<()> {
execute_overlay_dir(
&TemplateDirs::SHARED_SHARED,
replacements,
dart_root,
config,
None,
include_ohos,
)?;
let (shared_template_dir, comment_out_files) = match &config.template {
Template::App => (&TemplateDirs::SHARED_APP, vec!["main.dart".to_string()]),
Template::Plugin => (
&TemplateDirs::SHARED_PLUGIN,
vec![format!("{dart_package_name}.dart")],
),
};
execute_overlay_dir(
shared_template_dir,
replacements,
dart_root,
config,
Some(&comment_out_files),
include_ohos,
)?;
if let Some(dir) = backend_shared_template_dir(config.integration_backend) {
execute_overlay_dir(dir, replacements, dart_root, config, None, include_ohos)?;
}
if let Some(dir) = backend_template_dir(config.integration_backend, config.template) {
execute_overlay_dir(dir, replacements, dart_root, config, None, include_ohos)?;
}
Ok(())
}
fn backend_shared_template_dir(
integration_backend: IntegrationBackend,
) -> Option<&'static Dir<'static>> {
match integration_backend {
IntegrationBackend::Cargokit => None,
IntegrationBackend::NativeAssets => Some(&TemplateDirs::NATIVE_ASSETS_SHARED),
}
}
fn backend_template_dir(
integration_backend: IntegrationBackend,
template: Template,
) -> Option<&'static Dir<'static>> {
match (integration_backend, template) {
(IntegrationBackend::Cargokit, Template::App) => Some(&TemplateDirs::CARGOKIT_APP),
(IntegrationBackend::Cargokit, Template::Plugin) => Some(&TemplateDirs::CARGOKIT_PLUGIN),
(IntegrationBackend::NativeAssets, Template::App) => None,
(IntegrationBackend::NativeAssets, Template::Plugin) => {
Some(&TemplateDirs::NATIVE_ASSETS_PLUGIN)
}
}
}
pub(super) fn compute_replacements<'a>(
config: &'a IntegrateConfig,
dart_package_name: &'a str,
rust_crate_name: &'a str,
include_ohos: bool,
) -> HashMap<&'static str, &'a str> {
let mut replacements = HashMap::new();
replacements.insert("REPLACE_ME_DART_PACKAGE_NAME", dart_package_name);
replacements.insert("REPLACE_ME_RUST_CRATE_NAME", rust_crate_name);
replacements.insert("REPLACE_ME_RUST_CRATE_DIR", config.rust_crate_dir.as_str());
replacements.insert("REPLACE_ME_FRB_VERSION", env!("CARGO_PKG_VERSION"));
let rust_frb_dependency = if config.enable_local_dependency {
r#"{ path = "../../../frb_rust" }"#
} else {
concat!(r#""="#, env!("CARGO_PKG_VERSION"), r#"""#)
};
replacements.insert("REPLACE_ME_RUST_FRB_DEPENDENCY", rust_frb_dependency);
replacements.insert("Cargo.toml.template", "Cargo.toml");
replacements.insert("Cargo.lock.template", "Cargo.lock");
replacements.insert(
"REPLACE_ME_OHOS_PLUGIN_PLATFORM_TEXT",
if include_ohos {
"\n ohos:\n ffiPlugin: true"
} else {
""
},
);
replacements
}
fn modify_file(
target_path: PathBuf,
reference_content: &[u8],
existing_content: Option<Vec<u8>>,
replacements: &HashMap<&str, &str>,
enable_local_dependency: bool,
comment_out_files: Option<&[String]>,
) -> Option<(PathBuf, Vec<u8>)> {
let src = replace_file_content(reference_content, replacements);
if let Some(existing_content) = existing_content {
if let (Some(file_name), Some(files)) = (
target_path.file_name().and_then(|e| e.to_str()),
comment_out_files,
) {
if files.contains(&file_name.to_owned()) {
return comment_out_existing_file_and_write_template(
existing_content,
target_path,
&src,
);
}
}
warn!(
"Skip writing to {target_path:?} because file already exists. \
It is suggested to remove that file before running this command to apply the full template."
);
return None;
}
if target_path.iter().contains(&OsStr::new("cargokit")) {
if let Some(comments) = compute_cargokit_comments(&target_path) {
return Some((target_path, [comments.as_bytes(), &src].concat()));
}
}
if target_path
.iter()
.contains(&OsStr::new("flutter_rust_bridge.yaml"))
{
let mut ans = String::from_utf8(src).unwrap();
if enable_local_dependency {
ans += "\nlocal: true\n";
}
return Some((target_path, ans.as_bytes().to_owned()));
}
Some((target_path, src))
}
fn comment_out_existing_file_and_write_template(
existing_content: Vec<u8>,
path: PathBuf,
src: &[u8],
) -> Option<(PathBuf, Vec<u8>)> {
let existing_content = String::from_utf8(existing_content);
let commented_existing_content = existing_content
.map(|x| {
format!(
"// The original content is temporarily commented out to allow generating a self-contained demo - feel free to uncomment later.\n\n{}\n\n",
x.split('\n').map(|line| format!("// {line}")).join("\n")
)
})
.unwrap_or_default();
Some((path, [commented_existing_content.as_bytes(), src].concat()))
}
fn filter_file(
path: &Path,
enable_write_lib: bool,
enable_integration_test: bool,
include_ohos: bool,
) -> bool {
if path.iter().contains(&OsStr::new("ohos")) && !include_ohos {
return false;
}
if path.iter().contains(&OsStr::new("cargokit")) {
return ![".git", ".github", "docs", "test"].contains(&file_name(path));
}
if !enable_write_lib {
if path.iter().contains(&OsStr::new("rust_builder")) {
return true;
}
if path.iter().contains(&OsStr::new("android"))
|| path.iter().contains(&OsStr::new("ios"))
|| path.iter().contains(&OsStr::new("windows"))
|| path.iter().contains(&OsStr::new("macos"))
|| path.iter().contains(&OsStr::new("linux"))
|| path.iter().contains(&OsStr::new("ohos"))
|| path.iter().contains(&OsStr::new("lib"))
|| path
.iter()
.contains(&OsStr::new("REPLACE_ME_RUST_CRATE_DIR"))
|| path
.iter()
.contains(&OsStr::new("flutter_rust_bridge.yaml"))
{
return false;
}
}
if !enable_integration_test
&& (path.iter().contains(&OsStr::new("integration_test"))
|| path.iter().contains(&OsStr::new("test_driver")))
{
return false;
}
true
}
fn compute_cargokit_comments(path: &Path) -> Option<String> {
if [".gitignore"].contains(&file_name(path)) {
return None;
}
let comment_leading = match file_extension(path) {
"dart" | "md" | "gradle" | "" => "///",
"yaml" | "toml" => "#",
"lock" | "cmake" | "sh" | "ps1" | "cmd" => return None,
ext => unreachable!("unexpected file extension for path={:?} ext={}", path, ext),
};
Some(
(CARGOKIT_PRELUDE.iter())
.map(|line| format!("{comment_leading} {line}"))
.join("\n")
+ "\n\n",
)
}
fn file_name(p: &Path) -> &str {
p.file_name().unwrap().to_str().unwrap()
}
fn file_extension(p: &Path) -> &str {
p.extension().unwrap_or_default().to_str().unwrap()
}
const CARGOKIT_PRELUDE: &[&str] = &[
"This is copied from Cargokit (which is the official way to use it currently)", "Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin",
];
struct TemplateDirs;
impl TemplateDirs {
const SHARED_SHARED: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/shared/shared");
const SHARED_APP: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/shared/app");
const SHARED_PLUGIN: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/shared/plugin");
const CARGOKIT_APP: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/cargokit/app");
const CARGOKIT_PLUGIN: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/cargokit/plugin");
const NATIVE_ASSETS_SHARED: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/native_assets/shared");
const NATIVE_ASSETS_PLUGIN: Dir<'static> =
include_dir!("$CARGO_MANIFEST_DIR/assets/integration_template/native_assets/plugin");
}
#[cfg(test)]
mod tests {
use super::filter_file;
use std::path::Path;
#[test]
fn test_filter_file_excludes_ohos_when_not_enabled() {
assert!(!filter_file(
Path::new("rust_builder/ohos/src/main/module.json5"),
true,
true,
false,
));
assert!(!filter_file(
Path::new("ohos/src/main/module.json5"),
true,
true,
false,
));
}
#[test]
fn test_filter_file_includes_ohos_when_enabled() {
assert!(filter_file(
Path::new("rust_builder/ohos/src/main/module.json5"),
true,
true,
true,
));
assert!(filter_file(
Path::new("ohos/src/main/module.json5"),
true,
true,
true,
));
}
#[test]
fn test_filter_file_no_write_lib_excludes_enabled_ohos_platform_shell() {
assert!(!filter_file(
Path::new("ohos/src/main/module.json5"),
false,
true,
true,
));
}
#[test]
fn test_filter_file_excludes_cargokit_metadata() {
assert!(!filter_file(
Path::new("rust_builder/cargokit/.git"),
true,
true,
true,
));
assert!(!filter_file(
Path::new("cargokit/.github"),
true,
true,
true,
));
assert!(!filter_file(Path::new("cargokit/docs"), true, true, true,));
assert!(filter_file(
Path::new("cargokit/build_tool/pubspec.yaml"),
true,
true,
true,
));
}
}