1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::{
13 fs,
14 io::{self, BufRead},
15 path::{Path, PathBuf},
16 str::FromStr,
17};
18
19use semver::Version;
20use zng_txt::{ToTxt, Txt};
21use zng_unique_id::{lazy_static, lazy_static_init};
22mod process;
23pub use process::*;
24
25pub mod windows_subsystem;
26
27lazy_static! {
28 static ref ABOUT: About = About::fallback_name();
29}
30
31#[allow(clippy::test_attr_in_doctest)]
126#[macro_export]
127macro_rules! init {
128 () => {
129 let _on_main_exit = $crate::init_parse!($crate);
130 };
131}
132#[doc(hidden)]
133pub use zng_env_proc_macros::init_parse;
134
135#[doc(hidden)]
136pub fn init(about: About) -> Box<dyn std::any::Any> {
137 if !about.is_test {
138 if lazy_static_init(&ABOUT, about).is_err() {
139 panic!("env::init! already called\nnote: In `cfg(test)` builds init! can be called multiple times")
140 }
141 Box::new(process_init())
142 } else {
143 if lazy_static_init(&ABOUT, about).is_ok() {
145 Box::leak(Box::new(process_init()));
146 }
147 Box::new(())
148 }
149}
150
151#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
155#[non_exhaustive]
156pub struct About {
157 pub pkg_name: Txt,
161 pub pkg_authors: Box<[Txt]>,
165
166 pub version: Version,
170
171 pub app_id: Txt,
179 pub app: Txt,
185 pub org: Txt,
191
192 pub description: Txt,
196 pub homepage: Txt,
200
201 pub license: Txt,
205
206 pub has_about: bool,
211
212 pub meta: Vec<(Txt, Txt)>,
216
217 pub is_test: bool,
219}
220impl About {
221 pub fn crate_name(&self) -> Txt {
223 self.pkg_name.replace('-', "_").into()
224 }
225
226 pub fn qualifier(&self) -> Txt {
230 if let Some((_, v)) = self.meta.iter().find(|(k, _)| k == "qualifier") {
231 return v.clone();
232 }
233 self.try_qualifier().map(Txt::from_str).unwrap_or_default()
234 }
235 fn try_qualifier(&self) -> Option<&str> {
236 let last_dot = self.app_id.rfind('.')?;
237 let len = self.app_id[..last_dot].rfind('.')?;
238 Some(&self.app_id[..len])
239 }
240
241 pub fn get(&self, key: &str) -> Option<Txt> {
247 match key {
248 "pkg_name" => Some(self.pkg_name.clone()),
249 "pkg_authors" => {
250 let mut r = String::new();
251 let mut sep = "";
252 for a in &self.pkg_authors {
253 r.push_str(sep);
254 r.push_str(a);
255 sep = ", ";
256 }
257 Some(r.into())
258 }
259 "version" => Some(self.version.to_txt()),
260 "app_id" => Some(self.app_id.clone()),
261 "app" => Some(self.app.clone()),
262 "org" => Some(self.org.clone()),
263 "description" => Some(self.description.clone()),
264 "homepage" => Some(self.homepage.clone()),
265 "license" => Some(self.license.clone()),
266 "crate_name" => Some(self.crate_name()),
267 "qualifier" => Some(self.qualifier()),
268 _ => {
269 for (k, v) in self.meta.iter() {
270 if k == key {
271 return Some(v.clone());
272 }
273 }
274 None
275 }
276 }
277 }
278}
279impl About {
280 fn fallback_name() -> Self {
281 Self {
282 pkg_name: Txt::from_static(""),
283 pkg_authors: Box::new([]),
284 version: Version::new(0, 0, 0),
285 app: fallback_name(),
286 org: Txt::from_static(""),
287 description: Txt::from_static(""),
288 homepage: Txt::from_static(""),
289 license: Txt::from_static(""),
290 has_about: false,
291 app_id: fallback_id(),
292 meta: vec![],
293 is_test: false,
294 }
295 }
296
297 #[cfg(feature = "parse")]
299 pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
300 #[derive(serde::Deserialize)]
301 struct Manifest {
302 package: Package,
303 }
304 #[derive(serde::Deserialize)]
305 struct Package {
306 name: Txt,
307 version: Version,
308 description: Option<Txt>,
309 homepage: Option<Txt>,
310 license: Option<Txt>,
311 authors: Option<Box<[Txt]>>,
312 metadata: Option<Metadata>,
313 }
314 #[derive(serde::Deserialize)]
315 struct Metadata {
316 zng: Option<Zng>,
317 }
318 #[derive(serde::Deserialize)]
319 struct Zng {
320 about: toml::Table,
321 }
322
323 let m: Manifest = toml::from_str(cargo_toml)?;
324 let mut about = About {
325 pkg_name: m.package.name,
326 pkg_authors: m.package.authors.unwrap_or_default(),
327 version: m.package.version,
328 description: m.package.description.unwrap_or_default(),
329 homepage: m.package.homepage.unwrap_or_default(),
330 license: m.package.license.unwrap_or_default(),
331 app: Txt::from_static(""),
332 org: Txt::from_static(""),
333 app_id: Txt::from_static(""),
334 has_about: false,
335 meta: vec![],
336 is_test: false,
337 };
338 if let Some(zng) = m.package.metadata.and_then(|m| m.zng)
339 && !zng.about.is_empty()
340 {
341 let s = |key: &str| match zng.about.get(key) {
342 Some(toml::Value::String(s)) => Txt::from_str(s.as_str()),
343 _ => Txt::from_static(""),
344 };
345 about.has_about = true;
346 about.app = s("app");
347 about.org = s("org");
348 about.app_id = clean_id(&s("app_id"));
349 for (k, v) in zng.about {
350 if let toml::Value::String(v) = v
351 && !["app", "org", "app_id"].contains(&k.as_str())
352 {
353 about.meta.push((k.into(), v.into()));
354 }
355 }
356 }
357 if about.app.is_empty() {
358 about.app = about.pkg_name.clone();
359 }
360 if about.org.is_empty() {
361 about.org = about.pkg_authors.first().cloned().unwrap_or_default();
362 }
363 if about.app_id.is_empty() {
364 about.app_id = clean_id(&format!(
365 "{}.{}.{}",
366 about.get("qualifier").unwrap_or_default(),
367 about.org,
368 about.app
369 ));
370 }
371 Ok(about)
372 }
373
374 #[doc(hidden)]
375 #[expect(clippy::too_many_arguments)]
376 pub fn macro_new(
377 pkg_name: &'static str,
378 pkg_authors: &[&'static str],
379 (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
380 app_id: &'static str,
381 app: &'static str,
382 org: &'static str,
383 description: &'static str,
384 homepage: &'static str,
385 license: &'static str,
386 has_about: bool,
387 meta: &[(&'static str, &'static str)],
388 is_test: bool,
389 ) -> Self {
390 Self {
391 pkg_name: Txt::from_static(pkg_name),
392 pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
393 version: {
394 let mut v = Version::new(major, minor, patch);
395 v.pre = semver::Prerelease::from_str(pre).unwrap();
396 v.build = semver::BuildMetadata::from_str(build).unwrap();
397 v
398 },
399 app_id: Txt::from_static(app_id),
400 app: Txt::from_static(app),
401 org: Txt::from_static(org),
402 meta: meta.iter().map(|(k, v)| (Txt::from_static(k), Txt::from_static(v))).collect(),
403 description: Txt::from_static(description),
404 homepage: Txt::from_static(homepage),
405 license: Txt::from_static(license),
406 has_about,
407 is_test,
408 }
409 }
410}
411
412pub fn about() -> &'static About {
422 &ABOUT
423}
424
425fn fallback_name() -> Txt {
426 let exe = current_exe();
427 let exe_name = exe.file_name().unwrap().to_string_lossy();
428 let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
429 Txt::from_str(name)
430}
431
432fn fallback_id() -> Txt {
433 let exe = current_exe();
434 let exe_name = exe.file_name().unwrap().to_string_lossy();
435 clean_id(&exe_name)
436}
437
438fn clean_id(raw: &str) -> Txt {
443 let mut r = String::new();
444 let mut sep = "";
445 for i in raw.split('.') {
446 let i = i.trim();
447 if i.is_empty() {
448 continue;
449 }
450 r.push_str(sep);
451 for (i, c) in i.trim().char_indices() {
452 if i == 0 {
453 if !c.is_ascii_alphabetic() {
454 r.push('i');
455 } else {
456 r.push(c.to_ascii_lowercase());
457 }
458 } else if c.is_ascii_alphanumeric() || c == '_' {
459 r.push(c.to_ascii_lowercase());
460 } else {
461 r.push('_');
462 }
463 }
464 sep = ".";
465 }
466 r.into()
467}
468
469pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
478 BIN.join(relative_path)
479}
480lazy_static! {
481 static ref BIN: PathBuf = find_bin();
482}
483
484fn find_bin() -> PathBuf {
485 if cfg!(target_arch = "wasm32") {
486 PathBuf::from("./")
487 } else {
488 current_exe().parent().expect("current_exe path parent is required").to_owned()
489 }
490}
491
492pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
520 res_impl(relative_path.as_ref())
521}
522#[cfg(all(
523 any(debug_assertions, feature = "built_res"),
524 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
525))]
526fn res_impl(relative_path: &Path) -> PathBuf {
527 let built = BUILT_RES.join(relative_path);
528 if built.exists() {
529 return built;
530 }
531
532 RES.join(relative_path)
533}
534#[cfg(not(all(
535 any(debug_assertions, feature = "built_res"),
536 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
537)))]
538fn res_impl(relative_path: &Path) -> PathBuf {
539 RES.join(relative_path)
540}
541
542pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
570 #[cfg(target_os = "android")]
571 {
572 let version = res(format!(".zng-env.res.{}", about().version));
573 if !version.exists() {
574 if let Some(res) = open_res() {
575 if let Err(e) = install_res(version, res) {
576 tracing::error!("res install failed, {e}");
577 }
578 }
579 }
580 }
581 #[cfg(not(target_os = "android"))]
583 let _ = open_res;
584}
585#[cfg(target_os = "android")]
586fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
587 let res_path = version.parent().unwrap();
588 let _ = fs::remove_dir_all(res_path);
589 fs::create_dir(res_path)?;
590
591 let mut res = tar::Archive::new(res);
592 res.unpack(res_path)?;
593
594 let mut needs_pop = false;
596 for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
597 needs_pop = i == 0 && entry?.file_name() == "res";
598 }
599 if needs_pop {
600 let tmp = res_path.parent().unwrap().join("res-tmp");
601 fs::rename(res_path.join("res"), &tmp)?;
602 fs::rename(tmp, res_path)?;
603 }
604
605 fs::File::create(&version)?;
606
607 Ok(())
608}
609
610pub fn init_res(path: impl Into<PathBuf>) {
616 if lazy_static_init(&RES, path.into()).is_err() {
617 panic!("cannot `init_res`, `res` has already inited")
618 }
619}
620
621#[cfg(any(debug_assertions, feature = "built_res"))]
627pub fn init_built_res(path: impl Into<PathBuf>) {
628 if lazy_static_init(&BUILT_RES, path.into()).is_err() {
629 panic!("cannot `init_built_res`, `res` has already inited")
630 }
631}
632
633lazy_static! {
634 static ref RES: PathBuf = find_res();
635
636 #[cfg(any(debug_assertions, feature = "built_res"))]
637 static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
638}
639#[cfg(target_os = "android")]
640fn find_res() -> PathBuf {
641 android_internal("res")
642}
643#[cfg(not(target_os = "android"))]
644fn find_res() -> PathBuf {
645 #[cfg(not(target_arch = "wasm32"))]
646 if let Ok(mut p) = std::env::current_exe() {
647 p.set_extension("res-dir");
648 if let Ok(dir) = read_line(&p) {
649 return bin(dir);
650 }
651 }
652 if cfg!(debug_assertions) {
653 PathBuf::from("res")
654 } else if cfg!(target_arch = "wasm32") {
655 PathBuf::from("./res")
656 } else if cfg!(windows) {
657 bin("../res")
658 } else if cfg!(target_os = "macos") {
659 bin("../Resources")
660 } else if cfg!(target_family = "unix") {
661 let c = current_exe();
662 bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
663 } else {
664 panic!(
665 "resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
666 std::env::consts::OS
667 )
668 }
669}
670
671pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
686 CONFIG.join(relative_path)
687}
688
689pub fn init_config(path: impl Into<PathBuf>) {
695 if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
696 panic!("cannot `init_config`, `original_config` has already inited")
697 }
698}
699
700pub fn original_config() -> PathBuf {
704 ORIGINAL_CONFIG.clone()
705}
706lazy_static! {
707 static ref ORIGINAL_CONFIG: PathBuf = find_config();
708}
709
710pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
717 migrate_config_impl(new_path.as_ref())
718}
719fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
720 let prev_path = CONFIG.as_path();
721
722 if prev_path == new_path {
723 return Ok(());
724 }
725
726 let original_path = ORIGINAL_CONFIG.as_path();
727 let is_return = new_path == original_path;
728
729 if !is_return && dir_exists_not_empty(new_path) {
730 return Err(io::Error::new(
731 io::ErrorKind::AlreadyExists,
732 "can only migrate to new dir or empty dir",
733 ));
734 }
735 let created = !new_path.exists();
736 if created {
737 fs::create_dir_all(new_path)?;
738 }
739
740 let migrate = |from: &Path, to: &Path| {
741 copy_dir_all(from, to)?;
742 if fs::remove_dir_all(from).is_ok() {
743 fs::create_dir(from)?;
744 }
745
746 let redirect = ORIGINAL_CONFIG.join("config-dir");
747 if is_return {
748 fs::remove_file(redirect)
749 } else {
750 fs::write(redirect, to.display().to_string().as_bytes())
751 }
752 };
753
754 if let Err(e) = migrate(prev_path, new_path) {
755 if fs::remove_dir_all(new_path).is_ok() && !created {
756 let _ = fs::create_dir(new_path);
757 }
758 return Err(e);
759 }
760
761 tracing::info!("changed config dir to `{}`", new_path.display());
762
763 Ok(())
764}
765
766fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
767 for entry in fs::read_dir(from)? {
768 let from = entry?.path();
769 if from.is_dir() {
770 let to = to.join(from.file_name().unwrap());
771 fs::create_dir(&to)?;
772 copy_dir_all(&from, &to)?;
773 } else if from.is_file() {
774 let to = to.join(from.file_name().unwrap());
775 fs::copy(&from, &to)?;
776 } else {
777 continue;
778 }
779 }
780 Ok(())
781}
782
783lazy_static! {
784 static ref CONFIG: PathBuf = redirect_config(original_config());
785}
786
787#[cfg(target_os = "android")]
788fn find_config() -> PathBuf {
789 android_internal("config")
790}
791#[cfg(not(target_os = "android"))]
792fn find_config() -> PathBuf {
793 let cfg_dir = res("config-dir");
794 if let Ok(dir) = read_line(&cfg_dir) {
795 return res(dir);
796 }
797
798 if cfg!(debug_assertions) {
799 return PathBuf::from("target/tmp/dev_config/");
800 }
801
802 let a = about();
803 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
804 dirs.config_dir().to_owned()
805 } else {
806 panic!(
807 "config dir not specified for platform {}, use a '{}' file to specify an alternative",
808 std::env::consts::OS,
809 cfg_dir.display(),
810 )
811 }
812}
813fn redirect_config(cfg: PathBuf) -> PathBuf {
814 if cfg!(target_arch = "wasm32") {
815 return cfg;
816 }
817
818 if let Ok(dir) = read_line(&cfg.join("config-dir")) {
819 let mut dir = PathBuf::from(dir);
820 if dir.is_relative() {
821 dir = cfg.join(dir);
822 }
823 if dir.exists() {
824 let test_path = dir.join(".zng-config-test");
825 if let Err(e) = fs::create_dir_all(&dir)
826 .and_then(|_| fs::write(&test_path, "# check write access"))
827 .and_then(|_| fs::remove_file(&test_path))
828 {
829 eprintln!("error writing to migrated `{}`, {e}", dir.display());
830 tracing::error!("error writing to migrated `{}`, {e}", dir.display());
831 return cfg;
832 }
833 } else if let Err(e) = fs::create_dir_all(&dir) {
834 eprintln!("error creating migrated `{}`, {e}", dir.display());
835 tracing::error!("error creating migrated `{}`, {e}", dir.display());
836 return cfg;
837 }
838 dir
839 } else {
840 create_dir_opt(cfg)
841 }
842}
843
844fn create_dir_opt(dir: PathBuf) -> PathBuf {
845 if let Err(e) = std::fs::create_dir_all(&dir) {
846 eprintln!("error creating `{}`, {e}", dir.display());
847 tracing::error!("error creating `{}`, {e}", dir.display());
848 }
849 dir
850}
851
852pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
865 CACHE.join(relative_path)
866}
867
868pub fn init_cache(path: impl Into<PathBuf>) {
874 match lazy_static_init(&CACHE, path.into()) {
875 Ok(p) => {
876 create_dir_opt(p.to_owned());
877 }
878 Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
879 }
880}
881
882pub fn clear_cache() -> io::Result<()> {
886 best_effort_clear(CACHE.as_path())
887}
888fn best_effort_clear(path: &Path) -> io::Result<()> {
889 let mut error = None;
890
891 match fs::read_dir(path) {
892 Ok(cache) => {
893 for entry in cache {
894 match entry {
895 Ok(e) => {
896 let path = e.path();
897 if path.is_dir() {
898 if fs::remove_dir_all(&path).is_err() {
899 match best_effort_clear(&path) {
900 Ok(()) => {
901 if let Err(e) = fs::remove_dir(&path) {
902 error = Some(e)
903 }
904 }
905 Err(e) => {
906 error = Some(e);
907 }
908 }
909 }
910 } else if path.is_file()
911 && let Err(e) = fs::remove_file(&path)
912 {
913 error = Some(e);
914 }
915 }
916 Err(e) => {
917 error = Some(e);
918 }
919 }
920 }
921 }
922 Err(e) => {
923 error = Some(e);
924 }
925 }
926
927 match error {
928 Some(e) => Err(e),
929 None => Ok(()),
930 }
931}
932
933pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
942 migrate_cache_impl(new_path.as_ref())
943}
944fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
945 if dir_exists_not_empty(new_path) {
946 return Err(io::Error::new(
947 io::ErrorKind::AlreadyExists,
948 "can only migrate to new dir or empty dir",
949 ));
950 }
951 fs::create_dir_all(new_path)?;
952 let write_test = new_path.join(".zng-cache");
953 fs::write(&write_test, "# zng cache dir".as_bytes())?;
954 fs::remove_file(&write_test)?;
955
956 fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
957
958 tracing::info!("changed cache dir to `{}`", new_path.display());
959
960 let prev_path = CACHE.as_path();
961 if prev_path == new_path {
962 return Ok(());
963 }
964 if let Err(e) = best_effort_move(prev_path, new_path) {
965 eprintln!("failed to migrate all cache files, {e}");
966 tracing::error!("failed to migrate all cache files, {e}");
967 }
968
969 Ok(())
970}
971
972fn dir_exists_not_empty(dir: &Path) -> bool {
973 match fs::read_dir(dir) {
974 Ok(dir) => {
975 for entry in dir {
976 match entry {
977 Ok(_) => return true,
978 Err(e) => {
979 if e.kind() != io::ErrorKind::NotFound {
980 return true;
981 }
982 }
983 }
984 }
985 false
986 }
987 Err(e) => e.kind() != io::ErrorKind::NotFound,
988 }
989}
990
991fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
992 let mut error = None;
993
994 match fs::read_dir(from) {
995 Ok(cache) => {
996 for entry in cache {
997 match entry {
998 Ok(e) => {
999 let from = e.path();
1000 if from.is_dir() {
1001 let to = to.join(from.file_name().unwrap());
1002 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1003 fs::create_dir(&to)?;
1004 best_effort_move(&from, &to)?;
1005 fs::remove_dir(&from)
1006 }) {
1007 error = Some(e)
1008 }
1009 } else if from.is_file() {
1010 let to = to.join(from.file_name().unwrap());
1011 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1012 fs::copy(&from, &to)?;
1013 fs::remove_file(&from)
1014 }) {
1015 error = Some(e);
1016 }
1017 }
1018 }
1019 Err(e) => {
1020 error = Some(e);
1021 }
1022 }
1023 }
1024 }
1025 Err(e) => {
1026 error = Some(e);
1027 }
1028 }
1029
1030 match error {
1031 Some(e) => Err(e),
1032 None => Ok(()),
1033 }
1034}
1035
1036lazy_static! {
1037 static ref CACHE: PathBuf = create_dir_opt(find_cache());
1038}
1039#[cfg(target_os = "android")]
1040fn find_cache() -> PathBuf {
1041 android_internal("cache")
1042}
1043#[cfg(not(target_os = "android"))]
1044fn find_cache() -> PathBuf {
1045 let cache_dir = config("cache-dir");
1046 if let Ok(dir) = read_line(&cache_dir) {
1047 return config(dir);
1048 }
1049
1050 if cfg!(debug_assertions) {
1051 return PathBuf::from("target/tmp/dev_cache/");
1052 }
1053
1054 let a = about();
1055 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
1056 dirs.cache_dir().to_owned()
1057 } else {
1058 panic!(
1059 "cache dir not specified for platform {}, use a '{}' file to specify an alternative",
1060 std::env::consts::OS,
1061 cache_dir.display(),
1062 )
1063 }
1064}
1065
1066fn current_exe() -> PathBuf {
1067 std::env::current_exe().expect("current_exe path is required")
1068}
1069
1070fn read_line(path: &Path) -> io::Result<String> {
1071 let file = fs::File::open(path)?;
1072 for line in io::BufReader::new(file).lines() {
1073 let line = line?;
1074 let line = line.trim();
1075 if line.starts_with('#') {
1076 continue;
1077 }
1078 return Ok(line.into());
1079 }
1080 Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
1081}
1082
1083#[cfg(target_os = "android")]
1084mod android {
1085 use super::*;
1086
1087 lazy_static! {
1088 static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
1089 }
1090
1091 pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
1095 if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
1096 panic!("cannot `init_android_paths`, already inited")
1097 }
1098 }
1099
1100 pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
1104 ANDROID_PATHS[0].join(relative_path)
1105 }
1106
1107 pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
1111 ANDROID_PATHS[1].join(relative_path)
1112 }
1113}
1114#[cfg(target_os = "android")]
1115pub use android::*;
1116
1117#[cfg(test)]
1118mod tests {
1119 use crate::*;
1120
1121 #[test]
1122 fn parse_manifest() {
1123 init!();
1124 let a = about();
1125 assert_eq!(a.pkg_name, "zng-env");
1126 assert_eq!(a.app, "zng-env");
1127 assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
1128 assert_eq!(a.org, "The Zng Project Developers");
1129 }
1130}