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}
279impl About {
280    fn fallback_name() -> Self {
281        Self {
282            pkg_name: Txt::from_static(""),
283            pkg_authors: Box::new([]),
284            version: Version::new(0, 0, 0),
285            app: fallback_name(),
286            org: Txt::from_static(""),
287            description: Txt::from_static(""),
288            homepage: Txt::from_static(""),
289            license: Txt::from_static(""),
290            has_about: false,
291            app_id: fallback_id(),
292            meta: vec![],
293            is_test: false,
294        }
295    }
296
297    /// Parse a Cargo.toml string.
298    #[cfg(feature = "parse")]
299    pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
300        #[derive(serde::Deserialize)]
301        struct Manifest {
302            package: Package,
303        }
304        #[derive(serde::Deserialize)]
305        struct Package {
306            name: Txt,
307            version: Version,
308            description: Option<Txt>,
309            homepage: Option<Txt>,
310            license: Option<Txt>,
311            authors: Option<Box<[Txt]>>,
312            metadata: Option<Metadata>,
313        }
314        #[derive(serde::Deserialize)]
315        struct Metadata {
316            zng: Option<Zng>,
317        }
318        #[derive(serde::Deserialize)]
319        struct Zng {
320            about: toml::Table,
321        }
322
323        let m: Manifest = toml::from_str(cargo_toml)?;
324        let mut about = About {
325            pkg_name: m.package.name,
326            pkg_authors: m.package.authors.unwrap_or_default(),
327            version: m.package.version,
328            description: m.package.description.unwrap_or_default(),
329            homepage: m.package.homepage.unwrap_or_default(),
330            license: m.package.license.unwrap_or_default(),
331            app: Txt::from_static(""),
332            org: Txt::from_static(""),
333            app_id: Txt::from_static(""),
334            has_about: false,
335            meta: vec![],
336            is_test: false,
337        };
338        if let Some(zng) = m.package.metadata.and_then(|m| m.zng)
339            && !zng.about.is_empty()
340        {
341            let s = |key: &str| match zng.about.get(key) {
342                Some(toml::Value::String(s)) => Txt::from_str(s.as_str()),
343                _ => Txt::from_static(""),
344            };
345            about.has_about = true;
346            about.app = s("app");
347            about.org = s("org");
348            about.app_id = clean_id(&s("app_id"));
349            for (k, v) in zng.about {
350                if let toml::Value::String(v) = v
351                    && !["app", "org", "app_id"].contains(&k.as_str())
352                {
353                    about.meta.push((k.into(), v.into()));
354                }
355            }
356        }
357        if about.app.is_empty() {
358            about.app = about.pkg_name.clone();
359        }
360        if about.org.is_empty() {
361            about.org = about.pkg_authors.first().cloned().unwrap_or_default();
362        }
363        if about.app_id.is_empty() {
364            about.app_id = clean_id(&format!(
365                "{}.{}.{}",
366                about.get("qualifier").unwrap_or_default(),
367                about.org,
368                about.app
369            ));
370        }
371        Ok(about)
372    }
373
374    #[doc(hidden)]
375    #[expect(clippy::too_many_arguments)]
376    pub fn macro_new(
377        pkg_name: &'static str,
378        pkg_authors: &[&'static str],
379        (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
380        app_id: &'static str,
381        app: &'static str,
382        org: &'static str,
383        description: &'static str,
384        homepage: &'static str,
385        license: &'static str,
386        has_about: bool,
387        meta: &[(&'static str, &'static str)],
388        is_test: bool,
389    ) -> Self {
390        Self {
391            pkg_name: Txt::from_static(pkg_name),
392            pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
393            version: {
394                let mut v = Version::new(major, minor, patch);
395                v.pre = semver::Prerelease::from_str(pre).unwrap();
396                v.build = semver::BuildMetadata::from_str(build).unwrap();
397                v
398            },
399            app_id: Txt::from_static(app_id),
400            app: Txt::from_static(app),
401            org: Txt::from_static(org),
402            meta: meta.iter().map(|(k, v)| (Txt::from_static(k), Txt::from_static(v))).collect(),
403            description: Txt::from_static(description),
404            homepage: Txt::from_static(homepage),
405            license: Txt::from_static(license),
406            has_about,
407            is_test,
408        }
409    }
410}
411
412/// Gets metadata about the application.
413///
414/// The app must call [`init!`] at the beginning of the process, otherwise the metadata will fallback
415/// to just a name extracted from the current executable file path.
416///
417/// See the [`directories::ProjectDirs::from`] documentation for more details on how this metadata is
418/// used to create/find the app data directories.
419///
420/// [`directories::ProjectDirs::from`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.from
421pub fn about() -> &'static About {
422    &ABOUT
423}
424
425fn fallback_name() -> Txt {
426    let exe = current_exe();
427    let exe_name = exe.file_name().unwrap().to_string_lossy();
428    let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
429    Txt::from_str(name)
430}
431
432fn fallback_id() -> Txt {
433    let exe = current_exe();
434    let exe_name = exe.file_name().unwrap().to_string_lossy();
435    clean_id(&exe_name)
436}
437
438/// * At least one identifier, dot-separated.
439/// * Each identifier must contain ASCII letters, ASCII digits and underscore only.
440/// * Each identifier must start with a letter.
441/// * All lowercase.
442fn clean_id(raw: &str) -> Txt {
443    let mut r = String::new();
444    let mut sep = "";
445    for i in raw.split('.') {
446        let i = i.trim();
447        if i.is_empty() {
448            continue;
449        }
450        r.push_str(sep);
451        for (i, c) in i.trim().char_indices() {
452            if i == 0 {
453                if !c.is_ascii_alphabetic() {
454                    r.push('i');
455                } else {
456                    r.push(c.to_ascii_lowercase());
457                }
458            } else if c.is_ascii_alphanumeric() || c == '_' {
459                r.push(c.to_ascii_lowercase());
460            } else {
461                r.push('_');
462            }
463        }
464        sep = ".";
465    }
466    r.into()
467}
468
469/// Gets a path relative to the package binaries.
470///
471/// * In Wasm returns `./`, as in the relative URL.
472/// * In all other platforms returns `std::env::current_exe().parent()`.
473///
474/// # Panics
475///
476/// Panics if [`std::env::current_exe`] returns an error or has no parent directory.
477pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
478    BIN.join(relative_path)
479}
480lazy_static! {
481    static ref BIN: PathBuf = find_bin();
482}
483
484fn find_bin() -> PathBuf {
485    if cfg!(target_arch = "wasm32") {
486        PathBuf::from("./")
487    } else {
488        current_exe().parent().expect("current_exe path parent is required").to_owned()
489    }
490}
491
492/// Gets a path relative to the package resources.
493///
494/// * The res dir can be set by [`init_res`] before any env dir is used.
495/// * In Android returns `android_internal("res")`, assumes the package assets are extracted to this directory.
496/// * In Linux, macOS and Windows if a file `bin/current_exe_name.res-dir` is found the first non-empty and non-comment (#) line
497///   defines the res path.
498/// * In `cfg(debug_assertions)` builds returns `res`.
499/// * In Wasm returns `./res`, as in the relative URL.
500/// * In macOS returns `bin("../Resources")`, assumes the package is deployed using a desktop `.app` folder.
501/// * In all other Unix systems returns `bin("../share/current_exe_name")`, assumes the package is deployed
502///   using a Debian package.
503/// * In Windows returns `bin("../res")`. Note that there is no Windows standard, make sure to install
504///   the project using this structure.
505///
506/// # Built Resources
507///
508/// In `cfg(any(debug_assertions, feature="built_res"))` builds if the `target/res/{relative_path}` path exists it
509/// is returned instead. This is useful during development when the app depends on res that are generated locally and not
510/// included in version control.
511///
512/// Note that the built resources must be packaged with the other res at the same relative location, so that release builds can find them.
513///
514/// # Android
515///
516/// Unfortunately Android does not provide file system access to the bundled resources, you must use the `ndk::asset::AssetManager` to
517/// request files that are decompressed on demand from the APK file. We recommend extracting all cross-platform assets once on startup
518/// to avoid having to implement special Android handling for each resource usage. See [`android_install_res`] for more details.
519pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
520    res_impl(relative_path.as_ref())
521}
522#[cfg(all(
523    any(debug_assertions, feature = "built_res"),
524    not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
525))]
526fn res_impl(relative_path: &Path) -> PathBuf {
527    let built = BUILT_RES.join(relative_path);
528    if built.exists() {
529        return built;
530    }
531
532    RES.join(relative_path)
533}
534#[cfg(not(all(
535    any(debug_assertions, feature = "built_res"),
536    not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
537)))]
538fn res_impl(relative_path: &Path) -> PathBuf {
539    RES.join(relative_path)
540}
541
542/// Helper function for adapting Android assets to the cross-platform [`res`] API.
543///
544/// To implement Android resource extraction, bundle the resources in a tar that is itself bundled in `assets/res.tar` inside the APK.
545/// On startup, call this function, it handles resources extraction and versioning.
546///
547/// # Examples
548///
549/// ```
550/// # macro_rules! demo { () => {
551/// #[unsafe(no_mangle)]
552/// fn android_main(app: zng::view_process::default::android::AndroidApp) {
553///     zng::env::init!();
554///     zng::view_process::default::android::init_android_app(app.clone());
555///     zng::env::android_install_res(|| app.asset_manager().open(c"res.tar"));
556///     // zng::view_process::default::run_same_process(..);
557/// }
558/// # }}
559/// ```
560///
561/// 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
562/// cleared all app data.
563///
564/// The resources are installed in the [`res`] directory, if the tar archive has only a root dir named `res` it is stripped.
565/// This function assumes that it is the only app component that writes to this directory.
566///
567/// Note that the tar file is not compressed, because the APK already compresses it. The `cargo zng res` tool `.zr-apk`
568/// tar resources by default, simply place the resources in `/assets/res/`.
569pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
570    #[cfg(target_os = "android")]
571    {
572        let version = res(format!(".zng-env.res.{}", about().version));
573        if !version.exists() {
574            if let Some(res) = open_res() {
575                if let Err(e) = install_res(version, res) {
576                    tracing::error!("res install failed, {e}");
577                }
578            }
579        }
580    }
581    // cfg not applied to function so it shows on docs
582    #[cfg(not(target_os = "android"))]
583    let _ = open_res;
584}
585#[cfg(target_os = "android")]
586fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
587    let res_path = version.parent().unwrap();
588    let _ = fs::remove_dir_all(res_path);
589    fs::create_dir(res_path)?;
590
591    let mut res = tar::Archive::new(res);
592    res.unpack(res_path)?;
593
594    // rename res/res to res if it is the only entry in res
595    let mut needs_pop = false;
596    for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
597        needs_pop = i == 0 && entry?.file_name() == "res";
598    }
599    if needs_pop {
600        let tmp = res_path.parent().unwrap().join("res-tmp");
601        fs::rename(res_path.join("res"), &tmp)?;
602        fs::rename(tmp, res_path)?;
603    }
604
605    fs::File::create(&version)?;
606
607    Ok(())
608}
609
610/// Sets a custom [`res`] path.
611///
612/// # Panics
613///
614/// Panics if not called at the beginning of the process.
615pub fn init_res(path: impl Into<PathBuf>) {
616    if lazy_static_init(&RES, path.into()).is_err() {
617        panic!("cannot `init_res`, `res` has already inited")
618    }
619}
620
621/// Sets a custom path for the "built resources" override checked by [`res`] in debug builds.
622///
623/// # Panics
624///
625/// Panics if not called at the beginning of the process.
626#[cfg(any(debug_assertions, feature = "built_res"))]
627pub fn init_built_res(path: impl Into<PathBuf>) {
628    if lazy_static_init(&BUILT_RES, path.into()).is_err() {
629        panic!("cannot `init_built_res`, `res` has already inited")
630    }
631}
632
633lazy_static! {
634    static ref RES: PathBuf = find_res();
635
636    #[cfg(any(debug_assertions, feature = "built_res"))]
637    static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
638}
639#[cfg(target_os = "android")]
640fn find_res() -> PathBuf {
641    android_internal("res")
642}
643#[cfg(not(target_os = "android"))]
644fn find_res() -> PathBuf {
645    #[cfg(not(target_arch = "wasm32"))]
646    if let Ok(mut p) = std::env::current_exe() {
647        p.set_extension("res-dir");
648        if let Ok(dir) = read_line(&p) {
649            return bin(dir);
650        }
651    }
652    if cfg!(debug_assertions) {
653        PathBuf::from("res")
654    } else if cfg!(target_arch = "wasm32") {
655        PathBuf::from("./res")
656    } else if cfg!(windows) {
657        bin("../res")
658    } else if cfg!(target_os = "macos") {
659        bin("../Resources")
660    } else if cfg!(target_family = "unix") {
661        let c = current_exe();
662        bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
663    } else {
664        panic!(
665            "resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
666            std::env::consts::OS
667        )
668    }
669}
670
671/// Gets a path relative to the user config directory for the app.
672///
673/// * The config dir can be set by [`init_config`] before any env dir is used.
674/// * In Android returns `android_internal("config")`.
675/// * In Linux, macOS and Windows if a file in `res("config-dir")` is found the first non-empty and non-comment (#) line
676///   defines the res path.
677/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_config/`.
678/// * In all platforms attempts [`directories::ProjectDirs::config_dir`] and panic if it fails.
679/// * If the config dir selected by the previous method contains a `"config-dir"` file it will be
680///   used to redirect to another config dir, you can use this to implement config migration. Redirection only happens once.
681///
682/// The config directory is created if it is missing, checks once on init or first use.
683///
684/// [`directories::ProjectDirs::config_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.config_dir
685pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
686    CONFIG.join(relative_path)
687}
688
689/// Sets a custom [`original_config`] path.
690///
691/// # Panics
692///
693/// Panics if not called at the beginning of the process.
694pub fn init_config(path: impl Into<PathBuf>) {
695    if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
696        panic!("cannot `init_config`, `original_config` has already inited")
697    }
698}
699
700/// Config path before migration.
701///
702/// If this is equal to [`config`] the config has not migrated.
703pub fn original_config() -> PathBuf {
704    ORIGINAL_CONFIG.clone()
705}
706lazy_static! {
707    static ref ORIGINAL_CONFIG: PathBuf = find_config();
708}
709
710/// Copies all config to `new_path` and saves it as the config path.
711///
712/// If copying and saving path succeeds make a best effort to wipe the previous config dir. If copy and save fails
713/// makes a best effort to undo already made copies.
714///
715/// The `new_path` must not exist or be empty.
716pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
717    migrate_config_impl(new_path.as_ref())
718}
719fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
720    let prev_path = CONFIG.as_path();
721
722    if prev_path == new_path {
723        return Ok(());
724    }
725
726    let original_path = ORIGINAL_CONFIG.as_path();
727    let is_return = new_path == original_path;
728
729    if !is_return && dir_exists_not_empty(new_path) {
730        return Err(io::Error::new(
731            io::ErrorKind::AlreadyExists,
732            "can only migrate to new dir or empty dir",
733        ));
734    }
735    let created = !new_path.exists();
736    if created {
737        fs::create_dir_all(new_path)?;
738    }
739
740    let migrate = |from: &Path, to: &Path| {
741        copy_dir_all(from, to)?;
742        if fs::remove_dir_all(from).is_ok() {
743            fs::create_dir(from)?;
744        }
745
746        let redirect = ORIGINAL_CONFIG.join("config-dir");
747        if is_return {
748            fs::remove_file(redirect)
749        } else {
750            fs::write(redirect, to.display().to_string().as_bytes())
751        }
752    };
753
754    if let Err(e) = migrate(prev_path, new_path) {
755        if fs::remove_dir_all(new_path).is_ok() && !created {
756            let _ = fs::create_dir(new_path);
757        }
758        return Err(e);
759    }
760
761    tracing::info!("changed config dir to `{}`", new_path.display());
762
763    Ok(())
764}
765
766fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
767    for entry in fs::read_dir(from)? {
768        let from = entry?.path();
769        if from.is_dir() {
770            let to = to.join(from.file_name().unwrap());
771            fs::create_dir(&to)?;
772            copy_dir_all(&from, &to)?;
773        } else if from.is_file() {
774            let to = to.join(from.file_name().unwrap());
775            fs::copy(&from, &to)?;
776        } else {
777            continue;
778        }
779    }
780    Ok(())
781}
782
783lazy_static! {
784    static ref CONFIG: PathBuf = redirect_config(original_config());
785}
786
787#[cfg(target_os = "android")]
788fn find_config() -> PathBuf {
789    android_internal("config")
790}
791#[cfg(not(target_os = "android"))]
792fn find_config() -> PathBuf {
793    let cfg_dir = res("config-dir");
794    if let Ok(dir) = read_line(&cfg_dir) {
795        return res(dir);
796    }
797
798    if cfg!(debug_assertions) {
799        return PathBuf::from("target/tmp/dev_config/");
800    }
801
802    let a = about();
803    if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
804        dirs.config_dir().to_owned()
805    } else {
806        panic!(
807            "config dir not specified for platform {}, use a '{}' file to specify an alternative",
808            std::env::consts::OS,
809            cfg_dir.display(),
810        )
811    }
812}
813fn redirect_config(cfg: PathBuf) -> PathBuf {
814    if cfg!(target_arch = "wasm32") {
815        return cfg;
816    }
817
818    if let Ok(dir) = read_line(&cfg.join("config-dir")) {
819        let mut dir = PathBuf::from(dir);
820        if dir.is_relative() {
821            dir = cfg.join(dir);
822        }
823        if dir.exists() {
824            let test_path = dir.join(".zng-config-test");
825            if let Err(e) = fs::create_dir_all(&dir)
826                .and_then(|_| fs::write(&test_path, "# check write access"))
827                .and_then(|_| fs::remove_file(&test_path))
828            {
829                eprintln!("error writing to migrated `{}`, {e}", dir.display());
830                tracing::error!("error writing to migrated `{}`, {e}", dir.display());
831                return cfg;
832            }
833        } else if let Err(e) = fs::create_dir_all(&dir) {
834            eprintln!("error creating migrated `{}`, {e}", dir.display());
835            tracing::error!("error creating migrated `{}`, {e}", dir.display());
836            return cfg;
837        }
838        dir
839    } else {
840        create_dir_opt(cfg)
841    }
842}
843
844fn create_dir_opt(dir: PathBuf) -> PathBuf {
845    if let Err(e) = std::fs::create_dir_all(&dir) {
846        eprintln!("error creating `{}`, {e}", dir.display());
847        tracing::error!("error creating `{}`, {e}", dir.display());
848    }
849    dir
850}
851
852/// Gets a path relative to the cache directory for the app.
853///
854/// * The cache dir can be set by [`init_cache`] before any env dir is used.
855/// * In Android returns `android_internal("cache")`.
856/// * In Linux, macOS and Windows if a file `config("cache-dir")` is found the first non-empty and non-comment (#) line
857///   defines the res path.
858/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_cache/`.
859/// * In all platforms attempts [`directories::ProjectDirs::cache_dir`] and panic if it fails.
860///
861/// The cache dir is created if it is missing, checks once on init or first use.
862///
863/// [`directories::ProjectDirs::cache_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.cache_dir
864pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
865    CACHE.join(relative_path)
866}
867
868/// Sets a custom [`cache`] path.
869///
870/// # Panics
871///
872/// Panics if not called at the beginning of the process.
873pub fn init_cache(path: impl Into<PathBuf>) {
874    match lazy_static_init(&CACHE, path.into()) {
875        Ok(p) => {
876            create_dir_opt(p.to_owned());
877        }
878        Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
879    }
880}
881
882/// Removes all cache files possible.
883///
884/// Continues removing after the first fail, returns the last error.
885pub fn clear_cache() -> io::Result<()> {
886    best_effort_clear(CACHE.as_path())
887}
888fn best_effort_clear(path: &Path) -> io::Result<()> {
889    let mut error = None;
890
891    match fs::read_dir(path) {
892        Ok(cache) => {
893            for entry in cache {
894                match entry {
895                    Ok(e) => {
896                        let path = e.path();
897                        if path.is_dir() {
898                            if fs::remove_dir_all(&path).is_err() {
899                                match best_effort_clear(&path) {
900                                    Ok(()) => {
901                                        if let Err(e) = fs::remove_dir(&path) {
902                                            error = Some(e)
903                                        }
904                                    }
905                                    Err(e) => {
906                                        error = Some(e);
907                                    }
908                                }
909                            }
910                        } else if path.is_file()
911                            && let Err(e) = fs::remove_file(&path)
912                        {
913                            error = Some(e);
914                        }
915                    }
916                    Err(e) => {
917                        error = Some(e);
918                    }
919                }
920            }
921        }
922        Err(e) => {
923            error = Some(e);
924        }
925    }
926
927    match error {
928        Some(e) => Err(e),
929        None => Ok(()),
930    }
931}
932
933/// Save `new_path` as the new cache path and make a best effort to move existing cache files.
934///
935/// Note that the move failure is not considered an error (it is only logged), the app is expected to
936/// rebuild missing cache entries.
937///
938/// Note that [`cache`] will still point to the previous path on success, the app must be restarted to use the new cache.
939///
940/// The `new_path` must not exist or be empty.
941pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
942    migrate_cache_impl(new_path.as_ref())
943}
944fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
945    if dir_exists_not_empty(new_path) {
946        return Err(io::Error::new(
947            io::ErrorKind::AlreadyExists,
948            "can only migrate to new dir or empty dir",
949        ));
950    }
951    fs::create_dir_all(new_path)?;
952    let write_test = new_path.join(".zng-cache");
953    fs::write(&write_test, "# zng cache dir".as_bytes())?;
954    fs::remove_file(&write_test)?;
955
956    fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
957
958    tracing::info!("changed cache dir to `{}`", new_path.display());
959
960    let prev_path = CACHE.as_path();
961    if prev_path == new_path {
962        return Ok(());
963    }
964    if let Err(e) = best_effort_move(prev_path, new_path) {
965        eprintln!("failed to migrate all cache files, {e}");
966        tracing::error!("failed to migrate all cache files, {e}");
967    }
968
969    Ok(())
970}
971
972fn dir_exists_not_empty(dir: &Path) -> bool {
973    match fs::read_dir(dir) {
974        Ok(dir) => {
975            for entry in dir {
976                match entry {
977                    Ok(_) => return true,
978                    Err(e) => {
979                        if e.kind() != io::ErrorKind::NotFound {
980                            return true;
981                        }
982                    }
983                }
984            }
985            false
986        }
987        Err(e) => e.kind() != io::ErrorKind::NotFound,
988    }
989}
990
991fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
992    let mut error = None;
993
994    match fs::read_dir(from) {
995        Ok(cache) => {
996            for entry in cache {
997                match entry {
998                    Ok(e) => {
999                        let from = e.path();
1000                        if from.is_dir() {
1001                            let to = to.join(from.file_name().unwrap());
1002                            if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1003                                fs::create_dir(&to)?;
1004                                best_effort_move(&from, &to)?;
1005                                fs::remove_dir(&from)
1006                            }) {
1007                                error = Some(e)
1008                            }
1009                        } else if from.is_file() {
1010                            let to = to.join(from.file_name().unwrap());
1011                            if let Err(e) = fs::rename(&from, &to).or_else(|_| {
1012                                fs::copy(&from, &to)?;
1013                                fs::remove_file(&from)
1014                            }) {
1015                                error = Some(e);
1016                            }
1017                        }
1018                    }
1019                    Err(e) => {
1020                        error = Some(e);
1021                    }
1022                }
1023            }
1024        }
1025        Err(e) => {
1026            error = Some(e);
1027        }
1028    }
1029
1030    match error {
1031        Some(e) => Err(e),
1032        None => Ok(()),
1033    }
1034}
1035
1036lazy_static! {
1037    static ref CACHE: PathBuf = create_dir_opt(find_cache());
1038}
1039#[cfg(target_os = "android")]
1040fn find_cache() -> PathBuf {
1041    android_internal("cache")
1042}
1043#[cfg(not(target_os = "android"))]
1044fn find_cache() -> PathBuf {
1045    let cache_dir = config("cache-dir");
1046    if let Ok(dir) = read_line(&cache_dir) {
1047        return config(dir);
1048    }
1049
1050    if cfg!(debug_assertions) {
1051        return PathBuf::from("target/tmp/dev_cache/");
1052    }
1053
1054    let a = about();
1055    if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
1056        dirs.cache_dir().to_owned()
1057    } else {
1058        panic!(
1059            "cache dir not specified for platform {}, use a '{}' file to specify an alternative",
1060            std::env::consts::OS,
1061            cache_dir.display(),
1062        )
1063    }
1064}
1065
1066fn current_exe() -> PathBuf {
1067    std::env::current_exe().expect("current_exe path is required")
1068}
1069
1070fn read_line(path: &Path) -> io::Result<String> {
1071    let file = fs::File::open(path)?;
1072    for line in io::BufReader::new(file).lines() {
1073        let line = line?;
1074        let line = line.trim();
1075        if line.starts_with('#') {
1076            continue;
1077        }
1078        return Ok(line.into());
1079    }
1080    Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
1081}
1082
1083#[cfg(target_os = "android")]
1084mod android {
1085    use super::*;
1086
1087    lazy_static! {
1088        static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
1089    }
1090
1091    /// Initialize the Android app paths.
1092    ///
1093    /// This is called by `init_android_app` provided by view-process implementers.
1094    pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
1095        if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
1096            panic!("cannot `init_android_paths`, already inited")
1097        }
1098    }
1099
1100    /// Gets a path relative to the internal storage reserved for the app.
1101    ///
1102    /// Prefer using [`config`] or [`cache`] over this directly.
1103    pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
1104        ANDROID_PATHS[0].join(relative_path)
1105    }
1106
1107    /// Gets a path relative to the external storage reserved for the app.
1108    ///
1109    /// This directory is user accessible.
1110    pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
1111        ANDROID_PATHS[1].join(relative_path)
1112    }
1113}
1114#[cfg(target_os = "android")]
1115pub use android::*;
1116
1117#[cfg(test)]
1118mod tests {
1119    use crate::*;
1120
1121    #[test]
1122    fn parse_manifest() {
1123        init!();
1124        let a = about();
1125        assert_eq!(a.pkg_name, "zng-env");
1126        assert_eq!(a.app, "zng-env");
1127        assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
1128        assert_eq!(a.org, "The Zng Project Developers");
1129    }
1130}