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