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