Skip to main content

zng_env/
lib.rs

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//!
4//! Process environment directories and unique name.
5//!
6//! # Crate
7//!
8#![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/// Inits process metadata, calls process start handlers and defines the process lifetime in `main`.
32///
33/// This **must** be called in main.
34///
35/// Init [`about`] an [`About`] for the process metadata. See [`on_process_start!`] for process start handlers.
36/// See [`on_process_exit`] for exit handlers called at the end of the `main` function.
37///
38/// # Process Start
39///
40/// A single Zng executable can be built with multiple components that spawn different instances
41/// of the executable that must run as different processes. If the current instance is requested
42/// by component `init!` runs it and exits the process, never returning flow to the normal main function.
43///
44/// ```
45/// # mod zng { pub mod env { pub use zng_env::*; } }
46/// fn main() {
47///     println!("print in all processes");
48///     zng::env::init!();
49///     println!("print only in the app-process");
50///
51///     // directories are available after `init!`.
52///     let _res = zng::env::res("");
53///
54///     // APP.defaults().run(...);
55///
56///     // on_exit handlers are called here
57/// }
58/// ```
59///
60/// # Web Start
61///
62/// WebAssembly builds (`target_arch = "wasm32"`) must share the app wasm module reference by setting the custom attribute
63/// `__zng_env_init_module` on the Javascript `window` object.
64///
65/// The `init!` call **will panic** if the attribute is not found.
66///
67/// ```html
68/// <script type="module">
69/// import init, * as my_app_wasm from './my_app.js';
70/// window.__zng_env_init_module = my_app_wasm;
71/// async function main() {
72///   await init();
73/// }
74/// main();
75/// </script>
76/// ```
77///
78/// The example above imports and runs an app built using [`wasm-pack`] with `--target web` options.
79///
80/// # Android Start
81///
82/// Android builds (`target_os = "android"`) receive an `AndroidApp` instance from the `android_main`. This type
83/// is tightly coupled with the view-process implementation and so it is defined by the `zng-view` crate. In builds
84/// feature `"view"` you must call `zng::view_process::default::android::init_android_app` just after `init!`.
85///
86/// ```
87/// # macro_rules! demo { () => {
88/// #[unsafe(no_mangle)]
89/// fn android_main(app: zng::view_process::default::android::AndroidApp) {
90///     zng::env::init!();
91///     zng::view_process::default::android::init_android_app(app);
92///     // zng::view_process::default::run_same_process(..);
93/// }
94/// # }}
95/// ```
96///
97/// See the [multi example] for more details on how to support Android and other platforms.
98///
99/// # Test Start
100///
101/// In test builds this macro may be called multiple times in the same process at the start of `#[test]` functions.
102///
103/// ```
104/// # macro_rules! demo { () => {
105/// #[test]
106/// fn foo() {
107///     zng::env::init!();
108///     let mut app = APP.defaults().run_headless(false);
109///     // ...
110/// }
111///
112/// #[test]
113/// fn bar() {
114///     zng::env::init!();
115///     // ...
116/// }
117/// # }}
118/// ```
119///
120/// Note that the process start handlers will still only run once on the first test, the process exit handlers
121/// **will not run**.
122///
123/// [`wasm-pack`]: https://crates.io/crates/wasm-pack
124/// [multi example]: https://github.com/zng-ui/zng/tree/main/examples#multi
125#[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        // in test
144        if lazy_static_init(&ABOUT, about).is_ok() {
145            Box::leak(Box::new(process_init()));
146        }
147        Box::new(())
148    }
149}
150
151/// Metadata about the app and main crate.
152///
153/// See [`about`] for more details.
154#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
155#[non_exhaustive]
156pub struct About {
157    /// package.name
158    ///
159    /// Cargo crate name for the entry crate.
160    pub pkg_name: Txt,
161    /// package.authors
162    ///
163    /// Cargo crate authors for the entry crate.
164    pub pkg_authors: Box<[Txt]>,
165
166    /// package.version
167    ///
168    /// Cargo crate version for the entry crate.
169    pub version: Version,
170
171    /// package.metadata.zng.about.app_id
172    ///
173    /// Fully qualified unique name for the application. Is a list of one or more dot-separated identifiers,
174    /// each identifier starts with letter, identifiers contains only letters, digits and underscore.
175    /// A reverse DNS name is recommended.
176    ///
177    /// If the metadata is not set an id is derived from `"qualifier"`, `org` and `app` values.
178    pub app_id: Txt,
179    /// package.metadata.zng.about.app
180    ///
181    /// App display name.
182    ///
183    /// If the metadata is not set the `pkg_name` is used.
184    pub app: Txt,
185    /// package.metadata.zng.about.org
186    ///
187    /// Organization display name.
188    ///
189    /// If the metadata is not set the first `pkg_authors` is used.
190    pub org: Txt,
191
192    /// package.description
193    ///
194    /// Short description of the app.
195    pub description: Txt,
196    /// package.homepage
197    ///
198    /// Valid website about the app.
199    pub homepage: Txt,
200
201    /// package.license
202    ///
203    /// License title of the app.
204    pub license: Txt,
205
206    /// If package.metadata.zng.about is set on the Cargo.toml manifest.
207    ///
208    /// The presence of this section is used by `cargo zng res` to find the main
209    /// crate if the workspace has multiple bin crates.
210    pub has_about: bool,
211
212    /// package.metadata.zng.about.*
213    ///
214    /// Any other unknown string metadata.
215    pub meta: Vec<(Txt, Txt)>,
216
217    /// If app was started in a `cfg(test)` build binary.
218    pub is_test: bool,
219}
220impl About {
221    /// The `pkg_name` in snake_case.
222    pub fn crate_name(&self) -> Txt {
223        self.pkg_name.replace('-', "_").into()
224    }
225
226    /// The first components of `app_id` except the last two.
227    ///
228    /// For app id `"br.com.company.product"` the qualifier is `"br.com"`. For id `"company.product"` the qualifier is empty.
229    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    /// Get the value.
242    ///
243    /// The `key` can be an entry of [`meta`], the name of a field or the name of a value method.
244    ///
245    /// [`meta`]: Self::meta
246    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    /// Get the Windows AppUserModelID.
280    ///
281    /// This ID identifies the app for shell integration, like notifications. A shortcut
282    /// on the Start Menu must define a `System.AppUserModel.ID` that matches this ID.
283    ///
284    /// By default this is the [`app_id`], its strongly recommended that the app only use one ID, but if required
285    /// the AUMID can be explicitly set on the metadata using the `"windows_aumid"`.
286    ///
287    /// On startup the default view-process calls `SetCurrentProcessExplicitAppUserModelID` to ensure the GUI
288    /// is associated with the Start Menu entry even when not running from the start menu.
289    ///
290    /// [`app_id`]: About::app_id
291    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    /// Parse a Cargo.toml string.
317    #[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
431/// Gets metadata about the application.
432///
433/// The app must call [`init!`] at the beginning of the process, otherwise the metadata will fallback
434/// to just a name extracted from the current executable file path.
435///
436/// See the [`directories::ProjectDirs::from`] documentation for more details on how this metadata is
437/// used to create/find the app data directories.
438///
439/// [`directories::ProjectDirs::from`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.from
440pub 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
457/// * At least one identifier, dot-separated.
458/// * Each identifier must contain ASCII letters, ASCII digits and underscore only.
459/// * Each identifier must start with a letter.
460/// * All lowercase.
461fn 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
488/// Gets a path relative to the package binaries.
489///
490/// * In Wasm returns `./`, as in the relative URL.
491/// * In all other platforms returns `std::env::current_exe().parent()`.
492///
493/// # Panics
494///
495/// Panics if [`std::env::current_exe`] returns an error or has no parent directory.
496pub 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
511/// Gets a path relative to the package resources.
512///
513/// * The res dir can be set by [`init_res`] before any env dir is used.
514/// * In Android returns `android_internal("res")`, assumes the package assets are extracted to this directory.
515/// * In Linux, macOS and Windows if a file `bin/current_exe_name.res-dir` is found the first non-empty and non-comment (#) line
516///   defines the res path.
517/// * In `cfg(debug_assertions)` builds returns `res`.
518/// * In Wasm returns `./res`, as in the relative URL.
519/// * In macOS returns `bin("../Resources")`, assumes the package is deployed using a desktop `.app` folder.
520/// * In all other Unix systems returns `bin("../share/current_exe_name")`, assumes the package is deployed
521///   using a Debian package.
522/// * In Windows returns `bin("../res")`. Note that there is no Windows standard, make sure to install
523///   the project using this structure.
524///
525/// # Built Resources
526///
527/// In `cfg(any(debug_assertions, feature="built_res"))` builds if the `target/res/{relative_path}` path exists it
528/// is returned instead. This is useful during development when the app depends on res that are generated locally and not
529/// included in version control.
530///
531/// Note that the built resources must be packaged with the other res at the same relative location, so that release builds can find them.
532///
533/// # Android
534///
535/// Unfortunately Android does not provide file system access to the bundled resources, you must use the `ndk::asset::AssetManager` to
536/// request files that are decompressed on demand from the APK file. We recommend extracting all cross-platform assets once on startup
537/// to avoid having to implement special Android handling for each resource usage. See [`android_install_res`] for more details.
538pub 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
561/// Helper function for adapting Android assets to the cross-platform [`res`] API.
562///
563/// To implement Android resource extraction, bundle the resources in a tar that is itself bundled in `assets/res.tar` inside the APK.
564/// On startup, call this function, it handles resources extraction and versioning.
565///
566/// # Examples
567///
568/// ```
569/// # macro_rules! demo { () => {
570/// #[unsafe(no_mangle)]
571/// fn android_main(app: zng::view_process::default::android::AndroidApp) {
572///     zng::env::init!();
573///     zng::view_process::default::android::init_android_app(app.clone());
574///     zng::env::android_install_res(|| app.asset_manager().open(c"res.tar"));
575///     // zng::view_process::default::run_same_process(..);
576/// }
577/// # }}
578/// ```
579///
580/// The `open_res` closure is only called if this is the first instance of the current app version on the device, or if the user
581/// cleared all app data.
582///
583/// The resources are installed in the [`res`] directory, if the tar archive has only a root dir named `res` it is stripped.
584/// This function assumes that it is the only app component that writes to this directory.
585///
586/// Note that the tar file is not compressed, because the APK already compresses it. The `cargo zng res` tool `.zr-apk`
587/// tar resources by default, simply place the resources in `/assets/res/`.
588pub 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 applied to function so it shows on docs
601    #[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    // rename res/res to res if it is the only entry in res
614    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
629/// Sets a custom [`res`] path.
630///
631/// # Panics
632///
633/// Panics if not called at the beginning of the process.
634pub 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/// Sets a custom path for the "built resources" override checked by [`res`] in debug builds.
641///
642/// # Panics
643///
644/// Panics if not called at the beginning of the process.
645#[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
690/// Gets a path relative to the user config directory for the app.
691///
692/// * The config dir can be set by [`init_config`] before any env dir is used.
693/// * In Android returns `android_internal("config")`.
694/// * In Linux, macOS and Windows if a file in `res("config-dir")` is found the first non-empty and non-comment (#) line
695///   defines the config path.
696/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_config/`.
697/// * In all platforms attempts [`directories::ProjectDirs::config_dir`] and panic if it fails.
698/// * If the config dir selected by the previous method contains a `"config-dir"` file it will be
699///   used to redirect to another config dir, you can use this to implement config migration. Redirection only happens once.
700///
701/// The config directory is created if it is missing, checks once on init or first use.
702///
703/// [`directories::ProjectDirs::config_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.config_dir
704pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
705    CONFIG.join(relative_path)
706}
707
708/// Sets a custom [`original_config`] path.
709///
710/// # Panics
711///
712/// Panics if not called at the beginning of the process.
713pub 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
719/// Config path before migration.
720///
721/// If this is equal to [`config`] the config has not migrated.
722pub fn original_config() -> PathBuf {
723    ORIGINAL_CONFIG.clone()
724}
725lazy_static! {
726    static ref ORIGINAL_CONFIG: PathBuf = find_config();
727}
728
729/// Copies all config to `new_path` and saves it as the config path.
730///
731/// If copying and saving path succeeds make a best effort to wipe the previous config dir. If copy and save fails
732/// makes a best effort to undo already made copies.
733///
734/// The `new_path` must not exist or be empty.
735pub 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
871/// Gets a path relative to the cache directory for the app.
872///
873/// * The cache dir can be set by [`init_cache`] before any env dir is used.
874/// * In Android returns `android_internal("cache")`.
875/// * In Linux, macOS and Windows if a file `config("cache-dir")` is found the first non-empty and non-comment (#) line
876///   defines the res path.
877/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_cache/`.
878/// * In all platforms attempts [`directories::ProjectDirs::cache_dir`] and panic if it fails.
879///
880/// The cache dir is created if it is missing, checks once on init or first use.
881///
882/// [`directories::ProjectDirs::cache_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.cache_dir
883pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
884    CACHE.join(relative_path)
885}
886
887/// Sets a custom [`cache`] path.
888///
889/// # Panics
890///
891/// Panics if not called at the beginning of the process.
892pub 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
901/// Removes all cache files possible.
902///
903/// Continues removing after the first fail, returns the last error.
904pub 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
952/// Save `new_path` as the new cache path and make a best effort to move existing cache files.
953///
954/// Note that the move failure is not considered an error (it is only logged), the app is expected to
955/// rebuild missing cache entries.
956///
957/// Note that [`cache`] will still point to the previous path on success, the app must be restarted to use the new cache.
958///
959/// The `new_path` must not exist or be empty.
960pub 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    /// Initialize the Android app paths.
1111    ///
1112    /// This is called by `init_android_app` provided by view-process implementers.
1113    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    /// Gets a path relative to the internal storage reserved for the app.
1120    ///
1121    /// Prefer using [`config`] or [`cache`] over this directly.
1122    pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
1123        ANDROID_PATHS[0].join(relative_path)
1124    }
1125
1126    /// Gets a path relative to the external storage reserved for the app.
1127    ///
1128    /// This directory is user accessible.
1129    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}