1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/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::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#[macro_export]
100macro_rules! init {
101 () => {
102 let _on_main_exit = $crate::init_parse!($crate);
103 };
104}
105#[doc(hidden)]
106pub use zng_env_proc_macros::init_parse;
107
108#[doc(hidden)]
109pub fn init(about: About) -> impl Drop {
110 if lazy_static_init(&ABOUT, about).is_err() {
111 panic!("env already inited, env::init must be the first call in the process")
112 }
113 process_init()
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
120pub struct About {
121 pub pkg_name: Txt,
123 pub pkg_authors: Box<[Txt]>,
125 pub crate_name: Txt,
127 pub version: Version,
129 pub app: Txt,
131 pub org: Txt,
133 pub qualifier: Txt,
137 pub description: Txt,
139 pub homepage: Txt,
141
142 pub license: Txt,
144
145 pub has_about: bool,
150}
151impl About {
152 fn fallback_name() -> Self {
153 Self {
154 pkg_name: Txt::from_static(""),
155 pkg_authors: Box::new([]),
156 version: Version::new(0, 0, 0),
157 app: fallback_name(),
158 crate_name: Txt::from_static(""),
159 org: Txt::from_static(""),
160 qualifier: Txt::from_static(""),
161 description: Txt::from_static(""),
162 homepage: Txt::from_static(""),
163 license: Txt::from_static(""),
164 has_about: false,
165 }
166 }
167
168 pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
170 let m: Manifest = toml::from_str(cargo_toml)?;
171 let mut about = About {
172 crate_name: m.package.name.replace('-', "_").into(),
173 pkg_name: m.package.name,
174 pkg_authors: m.package.authors.unwrap_or_default(),
175 version: m.package.version,
176 description: m.package.description.unwrap_or_default(),
177 homepage: m.package.homepage.unwrap_or_default(),
178 license: m.package.license.unwrap_or_default(),
179 app: Txt::from_static(""),
180 org: Txt::from_static(""),
181 qualifier: Txt::from_static(""),
182 has_about: false,
183 };
184 if let Some(m) = m.package.metadata.and_then(|m| m.zng).and_then(|z| z.about) {
185 about.has_about = true;
186 about.app = m.app.unwrap_or_default();
187 about.org = m.org.unwrap_or_default();
188 about.qualifier = m.qualifier.unwrap_or_default();
189 }
190 if about.app.is_empty() {
191 about.app = about.pkg_name.clone();
192 }
193 if about.org.is_empty() {
194 about.org = about.pkg_authors.first().cloned().unwrap_or_default();
195 }
196 Ok(about)
197 }
198
199 #[doc(hidden)]
200 #[expect(clippy::too_many_arguments)]
201 pub fn macro_new(
202 pkg_name: &'static str,
203 pkg_authors: &[&'static str],
204 crate_name: &'static str,
205 (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
206 app: &'static str,
207 org: &'static str,
208 qualifier: &'static str,
209 description: &'static str,
210 homepage: &'static str,
211 license: &'static str,
212 has_about: bool,
213 ) -> Self {
214 Self {
215 pkg_name: Txt::from_static(pkg_name),
216 pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
217 crate_name: Txt::from_static(crate_name),
218 version: {
219 let mut v = Version::new(major, minor, patch);
220 v.pre = semver::Prerelease::from_str(pre).unwrap();
221 v.build = semver::BuildMetadata::from_str(build).unwrap();
222 v
223 },
224 app: Txt::from_static(app),
225 org: Txt::from_static(org),
226 qualifier: Txt::from_static(qualifier),
227 description: Txt::from_static(description),
228 homepage: Txt::from_static(homepage),
229 license: Txt::from_static(license),
230 has_about,
231 }
232 }
233}
234#[derive(serde::Deserialize)]
235struct Manifest {
236 package: Package,
237}
238#[derive(serde::Deserialize)]
239struct Package {
240 name: Txt,
241 version: Version,
242 description: Option<Txt>,
243 homepage: Option<Txt>,
244 license: Option<Txt>,
245 authors: Option<Box<[Txt]>>,
246 metadata: Option<Metadata>,
247}
248#[derive(serde::Deserialize)]
249struct Metadata {
250 zng: Option<Zng>,
251}
252#[derive(serde::Deserialize)]
253struct Zng {
254 about: Option<MetadataAbout>,
255}
256#[derive(serde::Deserialize)]
257struct MetadataAbout {
258 app: Option<Txt>,
259 org: Option<Txt>,
260 qualifier: Option<Txt>,
261}
262
263pub fn about() -> &'static About {
273 &ABOUT
274}
275
276fn fallback_name() -> Txt {
277 let exe = current_exe();
278 let exe_name = exe.file_name().unwrap().to_string_lossy();
279 let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
280 Txt::from_str(name)
281}
282
283pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
292 BIN.join(relative_path)
293}
294lazy_static! {
295 static ref BIN: PathBuf = find_bin();
296}
297
298fn find_bin() -> PathBuf {
299 if cfg!(target_arch = "wasm32") {
300 PathBuf::from("./")
301 } else {
302 current_exe().parent().expect("current_exe path parent is required").to_owned()
303 }
304}
305
306pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
334 res_impl(relative_path.as_ref())
335}
336#[cfg(all(
337 any(debug_assertions, feature = "built_res"),
338 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
339))]
340fn res_impl(relative_path: &Path) -> PathBuf {
341 let built = BUILT_RES.join(relative_path);
342 if built.exists() {
343 return built;
344 }
345
346 RES.join(relative_path)
347}
348#[cfg(not(all(
349 any(debug_assertions, feature = "built_res"),
350 not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
351)))]
352fn res_impl(relative_path: &Path) -> PathBuf {
353 RES.join(relative_path)
354}
355
356pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
384 #[cfg(target_os = "android")]
385 {
386 let version = res(format!(".zng-env.res.{}", about().version));
387 if !version.exists() {
388 if let Some(res) = open_res() {
389 if let Err(e) = install_res(version, res) {
390 tracing::error!("res install failed, {e}");
391 }
392 }
393 }
394 }
395 #[cfg(not(target_os = "android"))]
397 let _ = open_res;
398}
399#[cfg(target_os = "android")]
400fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
401 let res_path = version.parent().unwrap();
402 let _ = fs::remove_dir_all(res_path);
403 fs::create_dir(res_path)?;
404
405 let mut res = tar::Archive::new(res);
406 res.unpack(res_path)?;
407
408 let mut needs_pop = false;
410 for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
411 needs_pop = i == 0 && entry?.file_name() == "res";
412 }
413 if needs_pop {
414 let tmp = res_path.parent().unwrap().join("res-tmp");
415 fs::rename(res_path.join("res"), &tmp)?;
416 fs::rename(tmp, res_path)?;
417 }
418
419 fs::File::create(&version)?;
420
421 Ok(())
422}
423
424pub fn init_res(path: impl Into<PathBuf>) {
430 if lazy_static_init(&RES, path.into()).is_err() {
431 panic!("cannot `init_res`, `res` has already inited")
432 }
433}
434
435#[cfg(any(debug_assertions, feature = "built_res"))]
441pub fn init_built_res(path: impl Into<PathBuf>) {
442 if lazy_static_init(&BUILT_RES, path.into()).is_err() {
443 panic!("cannot `init_built_res`, `res` has already inited")
444 }
445}
446
447lazy_static! {
448 static ref RES: PathBuf = find_res();
449
450 #[cfg(any(debug_assertions, feature="built_res"))]
451 static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
452}
453#[cfg(target_os = "android")]
454fn find_res() -> PathBuf {
455 android_internal("res")
456}
457#[cfg(not(target_os = "android"))]
458fn find_res() -> PathBuf {
459 #[cfg(not(target_arch = "wasm32"))]
460 if let Ok(mut p) = std::env::current_exe() {
461 p.set_extension("res-dir");
462 if let Ok(dir) = read_line(&p) {
463 return bin(dir);
464 }
465 }
466 if cfg!(debug_assertions) {
467 PathBuf::from("res")
468 } else if cfg!(target_arch = "wasm32") {
469 PathBuf::from("./res")
470 } else if cfg!(windows) {
471 bin("../res")
472 } else if cfg!(target_os = "macos") {
473 bin("../Resources")
474 } else if cfg!(target_family = "unix") {
475 let c = current_exe();
476 bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
477 } else {
478 panic!(
479 "resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
480 std::env::consts::OS
481 )
482 }
483}
484
485pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
500 CONFIG.join(relative_path)
501}
502
503pub fn init_config(path: impl Into<PathBuf>) {
509 if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
510 panic!("cannot `init_config`, `original_config` has already inited")
511 }
512}
513
514pub fn original_config() -> PathBuf {
518 ORIGINAL_CONFIG.clone()
519}
520lazy_static! {
521 static ref ORIGINAL_CONFIG: PathBuf = find_config();
522}
523
524pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
531 migrate_config_impl(new_path.as_ref())
532}
533fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
534 let prev_path = CONFIG.as_path();
535
536 if prev_path == new_path {
537 return Ok(());
538 }
539
540 let original_path = ORIGINAL_CONFIG.as_path();
541 let is_return = new_path == original_path;
542
543 if !is_return && dir_exists_not_empty(new_path) {
544 return Err(io::Error::new(
545 io::ErrorKind::AlreadyExists,
546 "can only migrate to new dir or empty dir",
547 ));
548 }
549 let created = !new_path.exists();
550 if created {
551 fs::create_dir_all(new_path)?;
552 }
553
554 let migrate = |from: &Path, to: &Path| {
555 copy_dir_all(from, to)?;
556 if fs::remove_dir_all(from).is_ok() {
557 fs::create_dir(from)?;
558 }
559
560 let redirect = ORIGINAL_CONFIG.join("config-dir");
561 if is_return {
562 fs::remove_file(redirect)
563 } else {
564 fs::write(redirect, to.display().to_string().as_bytes())
565 }
566 };
567
568 if let Err(e) = migrate(prev_path, new_path) {
569 eprintln!("migration failed, {e}");
570 if fs::remove_dir_all(new_path).is_ok() && !created {
571 let _ = fs::create_dir(new_path);
572 }
573 }
574
575 tracing::info!("changed config dir to `{}`", new_path.display());
576
577 Ok(())
578}
579
580fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
581 for entry in fs::read_dir(from)? {
582 let from = entry?.path();
583 if from.is_dir() {
584 let to = to.join(from.file_name().unwrap());
585 fs::create_dir(&to)?;
586 copy_dir_all(&from, &to)?;
587 } else if from.is_file() {
588 let to = to.join(from.file_name().unwrap());
589 fs::copy(&from, &to)?;
590 } else {
591 continue;
592 }
593 }
594 Ok(())
595}
596
597lazy_static! {
598 static ref CONFIG: PathBuf = redirect_config(original_config());
599}
600
601#[cfg(target_os = "android")]
602fn find_config() -> PathBuf {
603 android_internal("config")
604}
605#[cfg(not(target_os = "android"))]
606fn find_config() -> PathBuf {
607 let cfg_dir = res("config-dir");
608 if let Ok(dir) = read_line(&cfg_dir) {
609 return res(dir);
610 }
611
612 if cfg!(debug_assertions) {
613 return PathBuf::from("target/tmp/dev_config/");
614 }
615
616 let a = about();
617 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier, &a.org, &a.app) {
618 dirs.config_dir().to_owned()
619 } else {
620 panic!(
621 "config dir not specified for platform {}, use a '{}' file to specify an alternative",
622 std::env::consts::OS,
623 cfg_dir.display(),
624 )
625 }
626}
627fn redirect_config(cfg: PathBuf) -> PathBuf {
628 if cfg!(target_arch = "wasm32") {
629 return cfg;
630 }
631
632 if let Ok(dir) = read_line(&cfg.join("config-dir")) {
633 let mut dir = PathBuf::from(dir);
634 if dir.is_relative() {
635 dir = cfg.join(dir);
636 }
637 if dir.exists() {
638 let test_path = dir.join(".zng-config-test");
639 if let Err(e) = fs::create_dir_all(&dir)
640 .and_then(|_| fs::write(&test_path, "# check write access"))
641 .and_then(|_| fs::remove_file(&test_path))
642 {
643 eprintln!("error writing to migrated `{}`, {e}", dir.display());
644 tracing::error!("error writing to migrated `{}`, {e}", dir.display());
645 return cfg;
646 }
647 } else if let Err(e) = fs::create_dir_all(&dir) {
648 eprintln!("error creating migrated `{}`, {e}", dir.display());
649 tracing::error!("error creating migrated `{}`, {e}", dir.display());
650 return cfg;
651 }
652 dir
653 } else {
654 create_dir_opt(cfg)
655 }
656}
657
658fn create_dir_opt(dir: PathBuf) -> PathBuf {
659 if let Err(e) = std::fs::create_dir_all(&dir) {
660 eprintln!("error creating `{}`, {e}", dir.display());
661 tracing::error!("error creating `{}`, {e}", dir.display());
662 }
663 dir
664}
665
666pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
679 CACHE.join(relative_path)
680}
681
682pub fn init_cache(path: impl Into<PathBuf>) {
688 match lazy_static_init(&CONFIG, path.into()) {
689 Ok(p) => {
690 create_dir_opt(p.to_owned());
691 }
692 Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
693 }
694}
695
696pub fn clear_cache() -> io::Result<()> {
700 best_effort_clear(CACHE.as_path())
701}
702fn best_effort_clear(path: &Path) -> io::Result<()> {
703 let mut error = None;
704
705 match fs::read_dir(path) {
706 Ok(cache) => {
707 for entry in cache {
708 match entry {
709 Ok(e) => {
710 let path = e.path();
711 if path.is_dir() {
712 if fs::remove_dir_all(&path).is_err() {
713 match best_effort_clear(&path) {
714 Ok(()) => {
715 if let Err(e) = fs::remove_dir(&path) {
716 error = Some(e)
717 }
718 }
719 Err(e) => {
720 error = Some(e);
721 }
722 }
723 }
724 } else if path.is_file() {
725 if let Err(e) = fs::remove_file(&path) {
726 error = Some(e);
727 }
728 }
729 }
730 Err(e) => {
731 error = Some(e);
732 }
733 }
734 }
735 }
736 Err(e) => {
737 error = Some(e);
738 }
739 }
740
741 match error {
742 Some(e) => Err(e),
743 None => Ok(()),
744 }
745}
746
747pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
756 migrate_cache_impl(new_path.as_ref())
757}
758fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
759 if dir_exists_not_empty(new_path) {
760 return Err(io::Error::new(
761 io::ErrorKind::AlreadyExists,
762 "can only migrate to new dir or empty dir",
763 ));
764 }
765 fs::create_dir_all(new_path)?;
766 let write_test = new_path.join(".zng-cache");
767 fs::write(&write_test, "# zng cache dir".as_bytes())?;
768 fs::remove_file(&write_test)?;
769
770 fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
771
772 tracing::info!("changed cache dir to `{}`", new_path.display());
773
774 let prev_path = CACHE.as_path();
775 if prev_path == new_path {
776 return Ok(());
777 }
778 if let Err(e) = best_effort_move(prev_path, new_path) {
779 eprintln!("failed to migrate all cache files, {e}");
780 tracing::error!("failed to migrate all cache files, {e}");
781 }
782
783 Ok(())
784}
785
786fn dir_exists_not_empty(dir: &Path) -> bool {
787 match fs::read_dir(dir) {
788 Ok(dir) => {
789 for entry in dir {
790 match entry {
791 Ok(_) => return true,
792 Err(e) => {
793 if e.kind() != io::ErrorKind::NotFound {
794 return true;
795 }
796 }
797 }
798 }
799 false
800 }
801 Err(e) => e.kind() != io::ErrorKind::NotFound,
802 }
803}
804
805fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
806 let mut error = None;
807
808 match fs::read_dir(from) {
809 Ok(cache) => {
810 for entry in cache {
811 match entry {
812 Ok(e) => {
813 let from = e.path();
814 if from.is_dir() {
815 let to = to.join(from.file_name().unwrap());
816 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
817 fs::create_dir(&to)?;
818 best_effort_move(&from, &to)?;
819 fs::remove_dir(&from)
820 }) {
821 error = Some(e)
822 }
823 } else if from.is_file() {
824 let to = to.join(from.file_name().unwrap());
825 if let Err(e) = fs::rename(&from, &to).or_else(|_| {
826 fs::copy(&from, &to)?;
827 fs::remove_file(&from)
828 }) {
829 error = Some(e);
830 }
831 }
832 }
833 Err(e) => {
834 error = Some(e);
835 }
836 }
837 }
838 }
839 Err(e) => {
840 error = Some(e);
841 }
842 }
843
844 match error {
845 Some(e) => Err(e),
846 None => Ok(()),
847 }
848}
849
850lazy_static! {
851 static ref CACHE: PathBuf = create_dir_opt(find_cache());
852}
853#[cfg(target_os = "android")]
854fn find_cache() -> PathBuf {
855 android_internal("cache")
856}
857#[cfg(not(target_os = "android"))]
858fn find_cache() -> PathBuf {
859 let cache_dir = config("cache-dir");
860 if let Ok(dir) = read_line(&cache_dir) {
861 return config(dir);
862 }
863
864 if cfg!(debug_assertions) {
865 return PathBuf::from("target/tmp/dev_cache/");
866 }
867
868 let a = about();
869 if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier, &a.org, &a.app) {
870 dirs.cache_dir().to_owned()
871 } else {
872 panic!(
873 "cache dir not specified for platform {}, use a '{}' file to specify an alternative",
874 std::env::consts::OS,
875 cache_dir.display(),
876 )
877 }
878}
879
880fn current_exe() -> PathBuf {
881 std::env::current_exe().expect("current_exe path is required")
882}
883
884fn read_line(path: &Path) -> io::Result<String> {
885 let file = fs::File::open(path)?;
886 for line in io::BufReader::new(file).lines() {
887 let line = line?;
888 let line = line.trim();
889 if line.starts_with('#') {
890 continue;
891 }
892 return Ok(line.into());
893 }
894 Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
895}
896
897#[cfg(target_os = "android")]
898mod android {
899 use super::*;
900
901 lazy_static! {
902 static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
903 }
904
905 pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
909 if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
910 panic!("cannot `init_android_paths`, already inited")
911 }
912 }
913
914 pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
918 ANDROID_PATHS[0].join(relative_path)
919 }
920
921 pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
925 ANDROID_PATHS[1].join(relative_path)
926 }
927}
928#[cfg(target_os = "android")]
929pub use android::*;
930
931#[cfg(test)]
932mod tests {
933 use crate::*;
934
935 #[test]
936 fn parse_manifest() {
937 init!();
938 let a = about();
939 assert_eq!(a.pkg_name, "zng-env");
940 assert_eq!(a.app, "zng-env");
941 assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
942 assert_eq!(a.org, "The Zng Project Developers");
943 }
944}