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
279 pub fn windows_aumid(&self) -> Txt {
292 match self.get("windows_aumid") {
293 Some(id) => id,
294 None => self.app_id.clone(),
295 }
296 }
297}
298impl About {
299 fn fallback_name() -> Self {
300 Self {
301 pkg_name: Txt::from_static(""),
302 pkg_authors: Box::new([]),
303 version: Version::new(0, 0, 0),
304 app: fallback_name(),
305 org: Txt::from_static(""),
306 description: Txt::from_static(""),
307 homepage: Txt::from_static(""),
308 license: Txt::from_static(""),
309 has_about: false,
310 app_id: fallback_id(),
311 meta: vec![],
312 is_test: false,
313 }
314 }
315
316 #[cfg(feature = "parse")]
318 pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
319 #[derive(serde::Deserialize)]
320 struct Manifest {
321 package: Package,
322 }
323 #[derive(serde::Deserialize)]
324 struct Package {
325 name: Txt,
326 version: Version,
327 description: Option<Txt>,
328 homepage: Option<Txt>,
329 license: Option<Txt>,
330 authors: Option<Box<[Txt]>>,
331 metadata: Option<Metadata>,
332 }
333 #[derive(serde::Deserialize)]
334 struct Metadata {
335 zng: Option<Zng>,
336 }
337 #[derive(serde::Deserialize)]
338 struct Zng {
339 about: toml::Table,
340 }
341
342 let m: Manifest = toml::from_str(cargo_toml)?;
343 let mut about = About {
344 pkg_name: m.package.name,
345 pkg_authors: m.package.authors.unwrap_or_default(),
346 version: m.package.version,
347 description: m.package.description.unwrap_or_default(),
348 homepage: m.package.homepage.unwrap_or_default(),
349 license: m.package.license.unwrap_or_default(),
350 app: Txt::from_static(""),
351 org: Txt::from_static(""),
352 app_id: Txt::from_static(""),
353 has_about: false,
354 meta: vec![],
355 is_test: false,
356 };
357 if let Some(zng) = m.package.metadata.and_then(|m| m.zng)
358 && !zng.about.is_empty()
359 {
360 let s = |key: &str| match zng.about.get(key) {
361 Some(toml::Value::String(s)) => Txt::from_str(s.as_str()),
362 _ => Txt::from_static(""),
363 };
364 about.has_about = true;
365 about.app = s("app");
366 about.org = s("org");
367 about.app_id = clean_id(&s("app_id"));
368 for (k, v) in zng.about {
369 if let toml::Value::String(v) = v
370 && !["app", "org", "app_id"].contains(&k.as_str())
371 {
372 about.meta.push((k.into(), v.into()));
373 }
374 }
375 }
376 if about.app.is_empty() {
377 about.app = about.pkg_name.clone();
378 }
379 if about.org.is_empty() {
380 about.org = about.pkg_authors.first().cloned().unwrap_or_default();
381 }
382 if about.app_id.is_empty() {
383 about.app_id = clean_id(&format!(
384 "{}.{}.{}",
385 about.get("qualifier").unwrap_or_default(),
386 about.org,
387 about.app
388 ));
389 }
390 Ok(about)
391 }
392
393 #[doc(hidden)]
394 #[expect(clippy::too_many_arguments)]
395 pub fn macro_new(
396 pkg_name: &'static str,
397 pkg_authors: &[&'static str],
398 (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
399 app_id: &'static str,
400 app: &'static str,
401 org: &'static str,
402 description: &'static str,
403 homepage: &'static str,
404 license: &'static str,
405 has_about: bool,
406 meta: &[(&'static str, &'static str)],
407 is_test: bool,
408 ) -> Self {
409 Self {
410 pkg_name: Txt::from_static(pkg_name),
411 pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
412 version: {
413 let mut v = Version::new(major, minor, patch);
414 v.pre = semver::Prerelease::from_str(pre).unwrap();
415 v.build = semver::BuildMetadata::from_str(build).unwrap();
416 v
417 },
418 app_id: Txt::from_static(app_id),
419 app: Txt::from_static(app),
420 org: Txt::from_static(org),
421 meta: meta.iter().map(|(k, v)| (Txt::from_static(k), Txt::from_static(v))).collect(),
422 description: Txt::from_static(description),
423 homepage: Txt::from_static(homepage),
424 license: Txt::from_static(license),
425 has_about,
426 is_test,
427 }
428 }
429}
430
431pub fn about() -> &'static About {
441 &ABOUT
442}
443
444fn fallback_name() -> Txt {
445 let exe = current_exe();
446 let exe_name = exe.file_name().unwrap().to_string_lossy();
447 let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
448 Txt::from_str(name)
449}
450
451fn fallback_id() -> Txt {
452 let exe = current_exe();
453 let exe_name = exe.file_name().unwrap().to_string_lossy();
454 clean_id(&exe_name)
455}
456
457fn clean_id(raw: &str) -> Txt {
462 let mut r = String::new();
463 let mut sep = "";
464 for i in raw.split('.') {
465 let i = i.trim();
466 if i.is_empty() {
467 continue;
468 }
469 r.push_str(sep);
470 for (i, c) in i.trim().char_indices() {
471 if i == 0 {
472 if !c.is_ascii_alphabetic() {
473 r.push('i');
474 } else {
475 r.push(c.to_ascii_lowercase());
476 }
477 } else if c.is_ascii_alphanumeric() || c == '_' {
478 r.push(c.to_ascii_lowercase());
479 } else {
480 r.push('_');
481 }
482 }
483 sep = ".";
484 }
485 r.into()
486}
487
488pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
497 BIN.join(relative_path)
498}
499lazy_static! {
500 static ref BIN: PathBuf = find_bin();
501}
502
503fn find_bin() -> PathBuf {
504 if cfg!(target_arch = "wasm32") {
505 PathBuf::from("./")
506 } else {
507 current_exe().parent().expect("current_exe path parent is required").to_owned()
508 }
509}
510
511pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
539 res_impl(relative_path.as_ref())
540}
541#[cfg(all(
542 any(debug_assertions, feature = "built_res"),
543 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
544))]
545fn res_impl(relative_path: &Path) -> PathBuf {
546 let built = BUILT_RES.join(relative_path);
547 if built.exists() {
548 return built;
549 }
550
551 RES.join(relative_path)
552}
553#[cfg(not(all(
554 any(debug_assertions, feature = "built_res"),
555 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
556)))]
557fn res_impl(relative_path: &Path) -> PathBuf {
558 RES.join(relative_path)
559}
560
561pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
589 #[cfg(target_os = "android")]
590 {
591 let version = res(format!(".zng-env.res.{}", about().version));
592 if !version.exists() {
593 if let Some(res) = open_res() {
594 if let Err(e) = install_res(version, res) {
595 tracing::error!("res install failed, {e}");
596 }
597 }
598 }
599 }
600 #[cfg(not(target_os = "android"))]
602 let _ = open_res;
603}
604#[cfg(target_os = "android")]
605fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
606 let res_path = version.parent().unwrap();
607 let _ = fs::remove_dir_all(res_path);
608 fs::create_dir(res_path)?;
609
610 let mut res = tar::Archive::new(res);
611 res.unpack(res_path)?;
612
613 let mut needs_pop = false;
615 for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
616 needs_pop = i == 0 && entry?.file_name() == "res";
617 }
618 if needs_pop {
619 let tmp = res_path.parent().unwrap().join("res-tmp");
620 fs::rename(res_path.join("res"), &tmp)?;
621 fs::rename(tmp, res_path)?;
622 }
623
624 fs::File::create(&version)?;
625
626 Ok(())
627}
628
629pub fn init_res(path: impl Into<PathBuf>) {
635 if lazy_static_init(&RES, path.into()).is_err() {
636 panic!("cannot `init_res`, `res` has already inited")
637 }
638}
639
640#[cfg(any(debug_assertions, feature = "built_res"))]
646pub fn init_built_res(path: impl Into<PathBuf>) {
647 if lazy_static_init(&BUILT_RES, path.into()).is_err() {
648 panic!("cannot `init_built_res`, `res` has already inited")
649 }
650}
651
652lazy_static! {
653 static ref RES: PathBuf = find_res();
654
655 #[cfg(any(debug_assertions, feature = "built_res"))]
656 static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
657}
658#[cfg(target_os = "android")]
659fn find_res() -> PathBuf {
660 android_internal("res")
661}
662#[cfg(not(target_os = "android"))]
663fn find_res() -> PathBuf {
664 #[cfg(not(target_arch = "wasm32"))]
665 if let Ok(mut p) = std::env::current_exe() {
666 p.set_extension("res-dir");
667 if let Ok(dir) = read_line(&p) {
668 return bin(dir);
669 }
670 }
671 if cfg!(debug_assertions) {
672 PathBuf::from("res")
673 } else if cfg!(target_arch = "wasm32") {
674 PathBuf::from("./res")
675 } else if cfg!(windows) {
676 bin("../res")
677 } else if cfg!(target_os = "macos") {
678 bin("../Resources")
679 } else if cfg!(target_family = "unix") {
680 let c = current_exe();
681 bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
682 } else {
683 panic!(
684 "resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
685 std::env::consts::OS
686 )
687 }
688}
689
690pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
705 CONFIG.join(relative_path)
706}
707
708pub fn init_config(path: impl Into<PathBuf>) {
714 if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
715 panic!("cannot `init_config`, `original_config` has already inited")
716 }
717}
718
719pub fn original_config() -> PathBuf {
723 ORIGINAL_CONFIG.clone()
724}
725lazy_static! {
726 static ref ORIGINAL_CONFIG: PathBuf = find_config();
727}
728
729pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
736 migrate_config_impl(new_path.as_ref())
737}
738fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
739 let prev_path = CONFIG.as_path();
740
741 if prev_path == new_path {
742 return Ok(());
743 }
744
745 let original_path = ORIGINAL_CONFIG.as_path();
746 let is_return = new_path == original_path;
747
748 if !is_return && dir_exists_not_empty(new_path) {
749 return Err(io::Error::new(
750 io::ErrorKind::AlreadyExists,
751 "can only migrate to new dir or empty dir",
752 ));
753 }
754 let created = !new_path.exists();
755 if created {
756 fs::create_dir_all(new_path)?;
757 }
758
759 let migrate = |from: &Path, to: &Path| {
760 copy_dir_all(from, to)?;
761 if fs::remove_dir_all(from).is_ok() {
762 fs::create_dir(from)?;
763 }
764
765 let redirect = ORIGINAL_CONFIG.join("config-dir");
766 if is_return {
767 fs::remove_file(redirect)
768 } else {
769 fs::write(redirect, to.display().to_string().as_bytes())
770 }
771 };
772
773 if let Err(e) = migrate(prev_path, new_path) {
774 if fs::remove_dir_all(new_path).is_ok() && !created {
775 let _ = fs::create_dir(new_path);
776 }
777 return Err(e);
778 }
779
780 tracing::info!("changed config dir to `{}`", new_path.display());
781
782 Ok(())
783}
784
785fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
786 for entry in fs::read_dir(from)? {
787 let from = entry?.path();
788 if from.is_dir() {
789 let to = to.join(from.file_name().unwrap());
790 fs::create_dir(&to)?;
791 copy_dir_all(&from, &to)?;
792 } else if from.is_file() {
793 let to = to.join(from.file_name().unwrap());
794 fs::copy(&from, &to)?;
795 } else {
796 continue;
797 }
798 }
799 Ok(())
800}
801
802lazy_static! {
803 static ref CONFIG: PathBuf = redirect_config(original_config());
804}
805
806#[cfg(target_os = "android")]
807fn find_config() -> PathBuf {
808 android_internal("config")
809}
810#[cfg(not(target_os = "android"))]
811fn find_config() -> PathBuf {
812 let cfg_dir = res("config-dir");
813 if let Ok(dir) = read_line(&cfg_dir) {
814 return res(dir);
815 }
816
817 if cfg!(debug_assertions) {
818 return PathBuf::from("target/tmp/dev_config/");
819 }
820
821 let a = about();
822 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
823 dirs.config_dir().to_owned()
824 } else {
825 panic!(
826 "config dir not specified for platform {}, use a '{}' file to specify an alternative",
827 std::env::consts::OS,
828 cfg_dir.display(),
829 )
830 }
831}
832fn redirect_config(cfg: PathBuf) -> PathBuf {
833 if cfg!(target_arch = "wasm32") {
834 return cfg;
835 }
836
837 if let Ok(dir) = read_line(&cfg.join("config-dir")) {
838 let mut dir = PathBuf::from(dir);
839 if dir.is_relative() {
840 dir = cfg.join(dir);
841 }
842 if dir.exists() {
843 let test_path = dir.join(".zng-config-test");
844 if let Err(e) = fs::create_dir_all(&dir)
845 .and_then(|_| fs::write(&test_path, "# check write access"))
846 .and_then(|_| fs::remove_file(&test_path))
847 {
848 eprintln!("error writing to migrated `{}`, {e}", dir.display());
849 tracing::error!("error writing to migrated `{}`, {e}", dir.display());
850 return cfg;
851 }
852 } else if let Err(e) = fs::create_dir_all(&dir) {
853 eprintln!("error creating migrated `{}`, {e}", dir.display());
854 tracing::error!("error creating migrated `{}`, {e}", dir.display());
855 return cfg;
856 }
857 dir
858 } else {
859 create_dir_opt(cfg)
860 }
861}
862
863fn create_dir_opt(dir: PathBuf) -> PathBuf {
864 if let Err(e) = std::fs::create_dir_all(&dir) {
865 eprintln!("error creating `{}`, {e}", dir.display());
866 tracing::error!("error creating `{}`, {e}", dir.display());
867 }
868 dir
869}
870
871pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
884 CACHE.join(relative_path)
885}
886
887pub fn init_cache(path: impl Into<PathBuf>) {
893 match lazy_static_init(&CACHE, path.into()) {
894 Ok(p) => {
895 create_dir_opt(p.to_owned());
896 }
897 Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
898 }
899}
900
901pub fn clear_cache() -> io::Result<()> {
905 best_effort_clear(CACHE.as_path())
906}
907fn best_effort_clear(path: &Path) -> io::Result<()> {
908 let mut error = None;
909
910 match fs::read_dir(path) {
911 Ok(cache) => {
912 for entry in cache {
913 match entry {
914 Ok(e) => {
915 let path = e.path();
916 if path.is_dir() {
917 if fs::remove_dir_all(&path).is_err() {
918 match best_effort_clear(&path) {
919 Ok(()) => {
920 if let Err(e) = fs::remove_dir(&path) {
921 error = Some(e)
922 }
923 }
924 Err(e) => {
925 error = Some(e);
926 }
927 }
928 }
929 } else if path.is_file()
930 && let Err(e) = fs::remove_file(&path)
931 {
932 error = Some(e);
933 }
934 }
935 Err(e) => {
936 error = Some(e);
937 }
938 }
939 }
940 }
941 Err(e) => {
942 error = Some(e);
943 }
944 }
945
946 match error {
947 Some(e) => Err(e),
948 None => Ok(()),
949 }
950}
951
952pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
961 migrate_cache_impl(new_path.as_ref())
962}
963fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
964 if dir_exists_not_empty(new_path) {
965 return Err(io::Error::new(
966 io::ErrorKind::AlreadyExists,
967 "can only migrate to new dir or empty dir",
968 ));
969 }
970 fs::create_dir_all(new_path)?;
971 let write_test = new_path.join(".zng-cache");
972 fs::write(&write_test, "# zng cache dir".as_bytes())?;
973 fs::remove_file(&write_test)?;
974
975 fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
976
977 tracing::info!("changed cache dir to `{}`", new_path.display());
978
979 let prev_path = CACHE.as_path();
980 if prev_path == new_path {
981 return Ok(());
982 }
983 if let Err(e) = best_effort_move(prev_path, new_path) {
984 eprintln!("failed to migrate all cache files, {e}");
985 tracing::error!("failed to migrate all cache files, {e}");
986 }
987
988 Ok(())
989}
990
991fn dir_exists_not_empty(dir: &Path) -> bool {
992 match fs::read_dir(dir) {
993 Ok(dir) => {
994 for entry in dir {
995 match entry {
996 Ok(_) => return true,
997 Err(e) => {
998 if e.kind() != io::ErrorKind::NotFound {
999 return true;
1000 }
1001 }
1002 }
1003 }
1004 false
1005 }
1006 Err(e) => e.kind() != io::ErrorKind::NotFound,
1007 }
1008}
1009
1010fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
1011 let mut error = None;
1012
1013 match fs::read_dir(from) {
1014 Ok(cache) => {
1015 for entry in cache {
1016 match entry {
1017 Ok(e) => {
1018 let from = e.path();
1019 if from.is_dir() {
1020 let to = to.join(from.file_name().unwrap());
1021 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1022 fs::create_dir(&to)?;
1023 best_effort_move(&from, &to)?;
1024 fs::remove_dir(&from)
1025 }) {
1026 error = Some(e)
1027 }
1028 } else if from.is_file() {
1029 let to = to.join(from.file_name().unwrap());
1030 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1031 fs::copy(&from, &to)?;
1032 fs::remove_file(&from)
1033 }) {
1034 error = Some(e);
1035 }
1036 }
1037 }
1038 Err(e) => {
1039 error = Some(e);
1040 }
1041 }
1042 }
1043 }
1044 Err(e) => {
1045 error = Some(e);
1046 }
1047 }
1048
1049 match error {
1050 Some(e) => Err(e),
1051 None => Ok(()),
1052 }
1053}
1054
1055lazy_static! {
1056 static ref CACHE: PathBuf = create_dir_opt(find_cache());
1057}
1058#[cfg(target_os = "android")]
1059fn find_cache() -> PathBuf {
1060 android_internal("cache")
1061}
1062#[cfg(not(target_os = "android"))]
1063fn find_cache() -> PathBuf {
1064 let cache_dir = config("cache-dir");
1065 if let Ok(dir) = read_line(&cache_dir) {
1066 return config(dir);
1067 }
1068
1069 if cfg!(debug_assertions) {
1070 return PathBuf::from("target/tmp/dev_cache/");
1071 }
1072
1073 let a = about();
1074 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
1075 dirs.cache_dir().to_owned()
1076 } else {
1077 panic!(
1078 "cache dir not specified for platform {}, use a '{}' file to specify an alternative",
1079 std::env::consts::OS,
1080 cache_dir.display(),
1081 )
1082 }
1083}
1084
1085fn current_exe() -> PathBuf {
1086 std::env::current_exe().expect("current_exe path is required")
1087}
1088
1089fn read_line(path: &Path) -> io::Result<String> {
1090 let file = fs::File::open(path)?;
1091 for line in io::BufReader::new(file).lines() {
1092 let line = line?;
1093 let line = line.trim();
1094 if line.starts_with('#') {
1095 continue;
1096 }
1097 return Ok(line.into());
1098 }
1099 Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
1100}
1101
1102#[cfg(target_os = "android")]
1103mod android {
1104 use super::*;
1105
1106 lazy_static! {
1107 static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
1108 }
1109
1110 pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
1114 if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
1115 panic!("cannot `init_android_paths`, already inited")
1116 }
1117 }
1118
1119 pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
1123 ANDROID_PATHS[0].join(relative_path)
1124 }
1125
1126 pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
1130 ANDROID_PATHS[1].join(relative_path)
1131 }
1132}
1133#[cfg(target_os = "android")]
1134pub use android::*;
1135
1136#[cfg(test)]
1137mod tests {
1138 use crate::*;
1139
1140 #[test]
1141 fn parse_manifest() {
1142 init!();
1143 let a = about();
1144 assert_eq!(a.pkg_name, "zng-env");
1145 assert_eq!(a.app, "zng-env");
1146 assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
1147 assert_eq!(a.org, "The Zng Project Developers");
1148 }
1149}