1use std::{env, fs, io::{self, Error, ErrorKind, Read}, path::Path};
2use std::fs::File;
3use std::io::{Cursor, Write};
4use std::path::PathBuf;
5use clap::builder::{OsStr, Str};
6use flate2::read::GzDecoder;
7use itertools::Itertools;
8use tar::Archive;
9use regex::Regex;
10use globmatch;
11use fs_extra;
12use path_absolutize::*;
13
14pub fn new_project(with_kwui: Option<PathBuf>, static_crt: bool, output_dir: &PathBuf, prj_type: &str, crate_name: &str) -> anyhow::Result<()> {
15 println!("CREATE PROJECT IN [{}]", output_dir.display());
16 std::fs::create_dir_all(output_dir)?;
17
18 let mut enable_android = true;
19 if let Err(_) = android_sdk_home() {
20 println!("WARNING: $ANDROID_HOME NOT SET, ANDROID SUPPORT DISABLED!");
21 enable_android = false;
22 }
23 if let Err(_) = android_ndk_home() {
24 println!("WARNING: $ANDROID_NDK_HOME NOT SET, ANDROID SUPPORT DISABLED!");
25 enable_android = false;
26 }
27 if let Err(_) = java_home() {
28 println!("WARNING: $JAVA_HOME NOT SET, ANDROID SUPPORT DISABLED!");
29 enable_android = false;
30 }
31
32 apply_template(output_dir, prj_type, crate_name, with_kwui, enable_android)?;
33
34 if static_crt {
35 make_static_crt_config(output_dir)?;
36 }
37
38 if enable_android {
39 android_copy_sdk_licenses(output_dir)?;
40 android_copy_cxx_stl_library(output_dir)?;
41 }
42
43 println!("CREATE {} PROJECT [{}] DONE", prj_type, crate_name);
44 Ok(())
45}
46
47pub fn apply_template(output_dir: &PathBuf, prj_type: &str, crate_name: &str, with_kwui: Option<PathBuf>, enable_android: bool) -> anyhow::Result<()> {
48 let templates_url = kwui_templates_url();
49 println!("USING TEMPLATES URL [{}]", templates_url);
50 let template = download(&templates_url)?;
51
52 let (kwui_rs, kwui_sys, kwui_cli) = kwui_dirs(env!("TAG"), with_kwui);
53
54 let mut tar = GzDecoder::new(Cursor::new(template));
55 let mut tar = Archive::new(tar);
56
57 let prefix = String::from(prj_type) + "/";
58 for entry in tar.entries()? {
59 let mut entry = entry?;
60 let src_path = entry.path()?.to_path_buf();
61
62 if let Ok(src_path) = src_path.strip_prefix(&prefix) {
63 let need_patch = src_path
64 .extension()
65 .filter(|ext| ext.to_string_lossy() == "in")
66 .is_some();
67 if need_patch {
68 let mut content = String::new();
69 entry.read_to_string(&mut content)?;
70
71 let content = configure_file(&src_path, content, crate_name, "0.1.0",
72 &kwui_rs, &kwui_sys, &kwui_cli, enable_android);
73
74 let mut dst_path = output_dir.join(src_path);
76 dst_path.set_extension("");
77
78 fs::write(&dst_path, content)?;
79 } else {
80 let dst_path = output_dir.join(src_path);
81 entry.unpack(&dst_path);
82 }
83 }
84 }
85 Ok(())
86}
87
88fn kwui_dirs(tag: &str, with_kwui: Option<PathBuf>) -> (String, String, String) {
90 if let Some(Ok(kwui_dir)) = with_kwui.map(|p| p.absolutize().map(|e| e.to_path_buf())) {
91 let kwui_dir = kwui_dir.display().to_string().replace('\\', "/");
92 let kwui_rs = format!("{{ version = \"{}\", path = \"{}\" }}",
93 tag, &kwui_dir);
94 let kwui_sys = format!("{{ version = \"{}\", path = \"{}/kwui-sys\" }}",
95 tag, &kwui_dir);
96 let kwui_cli = format!("{{ version = \"{}\", path = \"{}/kwui-cli\" }}",
97 tag, &kwui_dir);
98 (kwui_rs, kwui_sys, kwui_cli)
99 } else {
100 let version = format!("\"{}\"", tag);
101 (version.clone(), version.clone(), version)
102 }
103}
104
105fn make_static_crt_config(output_dir: &PathBuf) -> anyhow::Result<()> {
106 let dot_cargo_dir = output_dir.join(".cargo");
107 fs::create_dir_all(&dot_cargo_dir)?;
108 let mut file = fs::File::create(dot_cargo_dir.join("config.toml"))?;
109 file.write_all(b"[target.x86_64-pc-windows-msvc]\n\
110rustflags = [\"-C\", \"target-feature=+crt-static\"]\n\
111\n\
112[target.i686-pc-windows-msvc]\n\
113rustflags = [\"-C\", \"target-feature=+crt-static\"]\n\
114\n")?;
115 Ok(())
116}
117
118fn configure_file(filename: &Path, content: String, crate_name: &str, crate_version: &str,
119 kwui_rs: &str, kwui_sys: &str, kwui_cli: &str, enable_android: bool) -> String {
120 let crate_id = String::from("proj.kwui.") + crate_name;
121 let crate_version_code = "1000";
122
123 let content = content.replace("@KWUI_RS_DEP@", kwui_rs);
124 let content = content.replace("@KWUI_SYS_DEP@", kwui_sys);
125 let content = content.replace("@KWUI_CLI_DEP@", kwui_cli);
126 let content = if enable_android {
127 let content = content.replace("@ANDROID_APPLICATION_NAME@", crate_name);
128 let content = content.replace("@ANDROID_APPLICATION_ID@", &crate_id);
129 let content = content.replace("@ANDROID_APPLICATION_VERSIONNAME@", crate_version);
130 let content = content.replace("@ANDROID_APPLICATION_VERSIONCODE@", crate_version_code);
131 let content = content.replace("@ANDROID_PACKAGE_NAME@", crate_name);
132 let content = content.replace("@ANDROID_PACKAGE_ID@", &crate_id);
133 let content = content.replace("@ANDROID_PACKAGE_VERSIONNAME@", crate_version);
134 let content = content.replace("@ANDROID_PACKAGE_VERSIONCODE@", crate_version_code);
135 let content = content.replace("@ANDROID_ADDITIONAL_PARAMS@", "");
136 let content = content.replace("@ANDROID_ASSETS_DIR@", "../assets");
137 let content = content.replace("@JAVA_HOME@", &java_home().unwrap());
138 let content = content.replace("@CMAKE_ANDROID_SDK@", &android_sdk_home().unwrap_or_default().display().to_string());
139 content
140 } else {
141 content
142 };
143
144 let re = Regex::new(r"(@[a-zA-Z_]+@)").unwrap();
145 for (_, [placeholder]) in re.captures_iter(&content).map(|c| c.extract()) {
146 eprintln!("error: {}: unhandled template placeholder {}", filename.display(), placeholder);
147 }
148
149 content
150}
151
152pub fn download(url: impl AsRef<str>) -> io::Result<Vec<u8>> {
154 let url = url.as_ref();
155
156 if let Some(file_path) = url.strip_prefix("file://") {
158 return fs::read(Path::new(file_path));
159 }
160
161 if url.starts_with("file:") {
163 eprintln!("Unsupported file: URL {}", url);
164 return Err(Error::from(ErrorKind::Unsupported));
165 }
166 let resp = std::process::Command::new("curl")
167 .arg("-L")
169 .arg("-f")
172 .arg("-s")
174 .arg(url)
175 .output();
176 match resp {
177 Ok(out) => {
178 let result = out.stdout;
180 if out.status.success() {
181 Ok(result)
182 } else {
183 Err(io::Error::new(
184 io::ErrorKind::Other,
185 format!(
186 "curl error code: {:?}\ncurl stderr: {:?}",
187 out.status.code(),
188 std::str::from_utf8(&out.stderr)
189 ),
190 ))
191 }
192 }
193 Err(e) => Err(io::Error::new(
194 io::ErrorKind::Other,
195 format!("curl command error : {e:#?}"),
196 )),
197 }
198}
199
200pub fn kwui_templates_url() -> String {
201 let url = std::env::var("KWUI_TEMPLATES_URL")
202 .unwrap_or("https://github.com/wanghoi/kwui-binaries/releases/download/{tag}/kwui-templates-{key}.tar.gz"
203 .into());
204 url.replace("{tag}", env!("TAG"))
205 .replace("{key}", env!("KEY"))
206}
207
208fn java_home() -> anyhow::Result<String> {
209 let s = std::env::var("JAVA_HOME")?;
210 Ok(s)
211}
212
213fn android_sdk_home() -> anyhow::Result<PathBuf> {
214 let s = std::env::var("ANDROID_HOME")?;
215 Ok(PathBuf::from(s))
216}
217
218fn android_copy_sdk_licenses(output_dir: &PathBuf) -> anyhow::Result<()> {
219 let copy_src_dir = android_sdk_home()?.join("licenses");
220
221 if copy_src_dir.is_dir() {
222 println!("COPY ANDROID LICENSES ");
223 let copy_dest_dir = output_dir.join("android");
224 std::fs::create_dir_all(©_dest_dir).expect("create android licenses directory failed");
225 let copy_opts = fs_extra::dir::CopyOptions::new().overwrite(true);
226 fs_extra::copy_items(&[copy_src_dir], copy_dest_dir, ©_opts)?;
227 } else {
228 println!("cargo::warning=WARN: copy android licenses dir [{}] not found.", copy_src_dir.display());
229 }
230
231 Ok(())
232}
233
234fn android_copy_cxx_stl_library(output_dir: &PathBuf) -> anyhow::Result<()> {
235 println!("COPY ANDROID cxx_shared LIBRARY");
236
237 let src_file = find_library("**/aarch64-linux-android/libc++_shared.so")?;
238 let copy_dest_dir = output_dir.join("android/app/src/main/jniLibs/arm64-v8a");
239 std::fs::create_dir_all(©_dest_dir).expect("create jniLibs directory failed");
240 std::fs::copy(&src_file, ©_dest_dir.join("libc++_shared.so"))
241 .map_err(|e| {
242 eprintln!("copy libc++_shared.so failed");
243 e
244 })?;
245
246 Ok(())
247}
248
249fn android_ndk_home() -> anyhow::Result<PathBuf> {
250 let s = std::env::var("ANDROID_NDK_HOME")?;
251 Ok(PathBuf::from(s))
252}
253
254fn manifest_dir() -> PathBuf {
255 std::env::var("CARGO_MANIFEST_DIR").unwrap().into()
256}
257
258fn find_library(filepath: &str) -> anyhow::Result<PathBuf> {
259 let b = globmatch::Builder::new(filepath)
260 .build(android_ndk_home()?)
261 .unwrap();
262 if let Some(Ok(p)) = b.into_iter().next() {
263 Ok(p)
264 } else {
265 anyhow::bail!("find_library {} failed", filepath)
266 }
267}