Skip to main content

kwui_cli/
new.rs

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                // strip ".in" postfix
75                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
88// returns (kwui_rs_dir, kwui_sys_dir, kwui_cli_dir)
89fn 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
152/// Download a file from the given URL and return the data.
153pub fn download(url: impl AsRef<str>) -> io::Result<Vec<u8>> {
154    let url = url.as_ref();
155
156    // `file` URL, empty hostname, absolute path
157    if let Some(file_path) = url.strip_prefix("file://") {
158        return fs::read(Path::new(file_path));
159    }
160
161    // `file` URLs with non-empty hostname or relative paths are unsupported.
162    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        // follow redirects
168        .arg("-L")
169        // fail fast with no "error pages" output. more of a hint though, so we might still get error on stdout.
170        // so make sure to check the actual status returned.
171        .arg("-f")
172        // silent. no progress or error messages. only pure "response data"
173        .arg("-s")
174        .arg(url)
175        .output();
176    match resp {
177        Ok(out) => {
178            // ideally, we would redirect response to a file directly, but lets take it one step at a time.
179            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(&copy_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, &copy_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(&copy_dest_dir).expect("create jniLibs directory failed");
240    std::fs::copy(&src_file, &copy_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}