zng_env/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
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::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/// Inits process metadata, calls process start handlers and defines the process lifetime in `main`.
30///
31/// This **must** be called in main.
32///
33/// Init [`about`] an [`About`] for the process metadata. See [`on_process_start!`] for process start handlers.
34/// See [`on_process_exit`] for exit handlers called at the end of the `main` function.
35///
36/// # Process Start
37///
38/// A single Zng executable can be built with multiple components that spawn different instances
39/// of the executable that must run as different processes. If the current instance is requested
40/// by component `init!` runs it and exits the process, never returning flow to the normal main function.
41///
42/// ```
43/// # mod zng { pub mod env { pub use zng_env::*; } }
44/// fn main() {
45///     println!("print in all processes");
46///     zng::env::init!();
47///     println!("print only in the app-process");
48///
49///     // directories are available after `init!`.
50///     let _res = zng::env::res("");
51///     
52///     // APP.defaults().run(...);
53///
54///     // on_exit handlers are called here
55/// }
56/// ```
57///
58/// # Web Start
59///
60/// WebAssembly builds (`target_arch = "wasm32"`) must share the app wasm module reference by setting the custom attribute
61/// `__zng_env_init_module` on the Javascript `window` object.
62///
63/// The `init!` call **will panic** if the attribute is not found.
64///
65/// ```html
66/// <script type="module">
67/// import init, * as my_app_wasm from './my_app.js';
68/// window.__zng_env_init_module = my_app_wasm;
69/// async function main() {
70///   await init();
71/// }
72/// main();
73/// </script>
74/// ```
75///
76/// The example above imports and runs an app built using [`wasm-pack`] with `--target web` options.
77///
78/// # Android Start
79///
80/// Android builds (`target_os = "android"`) receive an `AndroidApp` instance from the `android_main`. This type
81/// is tightly coupled with the view-process implementation and so it is defined by the `zng-view` crate. In builds
82/// feature `"view"` you must call `zng::view_process::default::android::init_android_app` just after `init!`.
83///
84/// ```
85/// # macro_rules! demo { () => {
86/// #[unsafe(no_mangle)]
87/// fn android_main(app: zng::view_process::default::android::AndroidApp) {
88///     zng::env::init!();
89///     zng::view_process::default::android::init_android_app(app);
90///     // zng::view_process::default::run_same_process(..);
91/// }
92/// # }}
93/// ```
94///
95/// See the [multi example] for more details on how to support Android and other platforms.
96///
97/// [`wasm-pack`]: https://crates.io/crates/wasm-pack
98/// [multi example]: https://github.com/zng-ui/zng/tree/main/examples#multi
99#[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/// Metadata about the app and main crate.
117///
118/// See [`about`] for more details.
119#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
120pub struct About {
121    /// package.name
122    pub pkg_name: Txt,
123    /// package.authors
124    pub pkg_authors: Box<[Txt]>,
125    /// package.name in snake_case
126    pub crate_name: Txt,
127    /// package.version
128    pub version: Version,
129    /// package.metadata.zng.about.app or `pkg_name`
130    pub app: Txt,
131    /// package.metadata.zng.about.org or the first `pkg_authors`
132    pub org: Txt,
133    /// package.metadata.zng.about.qualifier
134    ///
135    /// Reverse domain name notation, excluding the name of the application.
136    pub qualifier: Txt,
137    /// package.description
138    pub description: Txt,
139    /// package.homepage
140    pub homepage: Txt,
141
142    /// package.license
143    pub license: Txt,
144
145    /// If package.metadata.zng.about is set on the Cargo.toml manifest.
146    ///
147    /// The presence of this section is used by `cargo zng res` to find the main
148    /// crate if the workspace has multiple bin crates.
149    pub has_about: bool,
150}
151impl About {
152    fn fallback_name() -> Self {
153        Self {
154            pkg_name: Txt::from_static(""),
155            pkg_authors: Box::new([]),
156            version: Version::new(0, 0, 0),
157            app: fallback_name(),
158            crate_name: Txt::from_static(""),
159            org: Txt::from_static(""),
160            qualifier: Txt::from_static(""),
161            description: Txt::from_static(""),
162            homepage: Txt::from_static(""),
163            license: Txt::from_static(""),
164            has_about: false,
165        }
166    }
167
168    /// Parse a Cargo.toml string.
169    pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
170        let m: Manifest = toml::from_str(cargo_toml)?;
171        let mut about = About {
172            crate_name: m.package.name.replace('-', "_").into(),
173            pkg_name: m.package.name,
174            pkg_authors: m.package.authors.unwrap_or_default(),
175            version: m.package.version,
176            description: m.package.description.unwrap_or_default(),
177            homepage: m.package.homepage.unwrap_or_default(),
178            license: m.package.license.unwrap_or_default(),
179            app: Txt::from_static(""),
180            org: Txt::from_static(""),
181            qualifier: Txt::from_static(""),
182            has_about: false,
183        };
184        if let Some(m) = m.package.metadata.and_then(|m| m.zng).and_then(|z| z.about) {
185            about.has_about = true;
186            about.app = m.app.unwrap_or_default();
187            about.org = m.org.unwrap_or_default();
188            about.qualifier = m.qualifier.unwrap_or_default();
189        }
190        if about.app.is_empty() {
191            about.app = about.pkg_name.clone();
192        }
193        if about.org.is_empty() {
194            about.org = about.pkg_authors.first().cloned().unwrap_or_default();
195        }
196        Ok(about)
197    }
198
199    #[doc(hidden)]
200    #[expect(clippy::too_many_arguments)]
201    pub fn macro_new(
202        pkg_name: &'static str,
203        pkg_authors: &[&'static str],
204        crate_name: &'static str,
205        (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
206        app: &'static str,
207        org: &'static str,
208        qualifier: &'static str,
209        description: &'static str,
210        homepage: &'static str,
211        license: &'static str,
212        has_about: bool,
213    ) -> Self {
214        Self {
215            pkg_name: Txt::from_static(pkg_name),
216            pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
217            crate_name: Txt::from_static(crate_name),
218            version: {
219                let mut v = Version::new(major, minor, patch);
220                v.pre = semver::Prerelease::from_str(pre).unwrap();
221                v.build = semver::BuildMetadata::from_str(build).unwrap();
222                v
223            },
224            app: Txt::from_static(app),
225            org: Txt::from_static(org),
226            qualifier: Txt::from_static(qualifier),
227            description: Txt::from_static(description),
228            homepage: Txt::from_static(homepage),
229            license: Txt::from_static(license),
230            has_about,
231        }
232    }
233}
234#[derive(serde::Deserialize)]
235struct Manifest {
236    package: Package,
237}
238#[derive(serde::Deserialize)]
239struct Package {
240    name: Txt,
241    version: Version,
242    description: Option<Txt>,
243    homepage: Option<Txt>,
244    license: Option<Txt>,
245    authors: Option<Box<[Txt]>>,
246    metadata: Option<Metadata>,
247}
248#[derive(serde::Deserialize)]
249struct Metadata {
250    zng: Option<Zng>,
251}
252#[derive(serde::Deserialize)]
253struct Zng {
254    about: Option<MetadataAbout>,
255}
256#[derive(serde::Deserialize)]
257struct MetadataAbout {
258    app: Option<Txt>,
259    org: Option<Txt>,
260    qualifier: Option<Txt>,
261}
262
263/// Gets metadata about the application.
264///
265/// The app must call [`init!`] at the beginning of the process, otherwise the metadata will fallback
266/// to just a name extracted from the current executable file path.
267///
268/// See the [`directories::ProjectDirs::from`] documentation for more details on how this metadata is
269/// used to create/find the app data directories.
270///
271/// [`directories::ProjectDirs::from`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.from
272pub fn about() -> &'static About {
273    &ABOUT
274}
275
276fn fallback_name() -> Txt {
277    let exe = current_exe();
278    let exe_name = exe.file_name().unwrap().to_string_lossy();
279    let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
280    Txt::from_str(name)
281}
282
283/// Gets a path relative to the package binaries.
284///
285/// * In Wasm returns `./`, as in the relative URL.
286/// * In all other platforms returns `std::env::current_exe().parent()`.
287///
288/// # Panics
289///
290/// Panics if [`std::env::current_exe`] returns an error or has no parent directory.
291pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
292    BIN.join(relative_path)
293}
294lazy_static! {
295    static ref BIN: PathBuf = find_bin();
296}
297
298fn find_bin() -> PathBuf {
299    if cfg!(target_arch = "wasm32") {
300        PathBuf::from("./")
301    } else {
302        current_exe().parent().expect("current_exe path parent is required").to_owned()
303    }
304}
305
306/// Gets a path relative to the package resources.
307///
308/// * The res dir can be set by [`init_res`] before any env dir is used.
309/// * In Android returns `android_internal("res")`, assumes the package assets are extracted to this directory.
310/// * In Linux, macOS and Windows if a file `bin/current_exe_name.res-dir` is found the first non-empty and non-comment (#) line
311///   defines the res path.
312/// * In `cfg(debug_assertions)` builds returns `res`.
313/// * In Wasm returns `./res`, as in the relative URL.
314/// * In macOS returns `bin("../Resources")`, assumes the package is deployed using a desktop `.app` folder.
315/// * In all other Unix systems returns `bin("../share/current_exe_name")`, assumes the package is deployed
316///   using a Debian package.
317/// * In Windows returns `bin("../res")`. Note that there is no Windows standard, make sure to install
318///   the project using this structure.
319///
320/// # Built Resources
321///
322/// In `cfg(any(debug_assertions, feature="built_res"))` builds if the `target/res/{relative_path}` path exists it
323/// is returned instead. This is useful during development when the app depends on res that are generated locally and not
324/// included in version control.
325///
326/// Note that the built resources must be packaged with the other res at the same relative location, so that release builds can find them.
327///
328/// # Android
329///
330/// Unfortunately Android does not provide file system access to the bundled resources, you must use the `ndk::asset::AssetManager` to
331/// request files that are decompressed on demand from the APK file. We recommend extracting all cross-platform assets once on startup
332/// to avoid having to implement special Android handling for each resource usage. See [`android_install_res`] for more details.
333pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
334    res_impl(relative_path.as_ref())
335}
336#[cfg(all(
337    any(debug_assertions, feature = "built_res"),
338    not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
339))]
340fn res_impl(relative_path: &Path) -> PathBuf {
341    let built = BUILT_RES.join(relative_path);
342    if built.exists() {
343        return built;
344    }
345
346    RES.join(relative_path)
347}
348#[cfg(not(all(
349    any(debug_assertions, feature = "built_res"),
350    not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
351)))]
352fn res_impl(relative_path: &Path) -> PathBuf {
353    RES.join(relative_path)
354}
355
356/// Helper function for adapting Android assets to the cross-platform [`res`] API.
357///
358/// To implement Android resource extraction, bundle the resources in a tar that is itself bundled in `assets/res.tar` inside the APK.
359/// On startup, call this function, it handles resources extraction and versioning.
360///
361/// # Examples
362///
363/// ```
364/// # macro_rules! demo { () => {
365/// #[unsafe(no_mangle)]
366/// fn android_main(app: zng::view_process::default::android::AndroidApp) {
367///     zng::env::init!();
368///     zng::view_process::default::android::init_android_app(app.clone());
369///     zng::env::android_install_res(|| app.asset_manager().open(c"res.tar"));
370///     // zng::view_process::default::run_same_process(..);
371/// }
372/// # }}
373/// ```
374///
375/// 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
376/// cleared all app data.
377///
378/// The resources are installed in the [`res`] directory, if the tar archive has only a root dir named `res` it is stripped.
379/// This function assumes that it is the only app component that writes to this directory.
380///
381/// Note that the tar file is not compressed, because the APK already compresses it. The `cargo zng res` tool `.zr-apk`
382/// tar resources by default, simply place the resources in `/assets/res/`.
383pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
384    #[cfg(target_os = "android")]
385    {
386        let version = res(format!(".zng-env.res.{}", about().version));
387        if !version.exists() {
388            if let Some(res) = open_res() {
389                if let Err(e) = install_res(version, res) {
390                    tracing::error!("res install failed, {e}");
391                }
392            }
393        }
394    }
395    // cfg not applied to function so it shows on docs
396    #[cfg(not(target_os = "android"))]
397    let _ = open_res;
398}
399#[cfg(target_os = "android")]
400fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
401    let res_path = version.parent().unwrap();
402    let _ = fs::remove_dir_all(res_path);
403    fs::create_dir(res_path)?;
404
405    let mut res = tar::Archive::new(res);
406    res.unpack(res_path)?;
407
408    // rename res/res to res if it is the only entry in res
409    let mut needs_pop = false;
410    for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
411        needs_pop = i == 0 && entry?.file_name() == "res";
412    }
413    if needs_pop {
414        let tmp = res_path.parent().unwrap().join("res-tmp");
415        fs::rename(res_path.join("res"), &tmp)?;
416        fs::rename(tmp, res_path)?;
417    }
418
419    fs::File::create(&version)?;
420
421    Ok(())
422}
423
424/// Sets a custom [`res`] path.
425///
426/// # Panics
427///
428/// Panics if not called at the beginning of the process.
429pub fn init_res(path: impl Into<PathBuf>) {
430    if lazy_static_init(&RES, path.into()).is_err() {
431        panic!("cannot `init_res`, `res` has already inited")
432    }
433}
434
435/// Sets a custom path for the "built resources" override checked by [`res`] in debug builds.
436///
437/// # Panics
438///
439/// Panics if not called at the beginning of the process.
440#[cfg(any(debug_assertions, feature = "built_res"))]
441pub fn init_built_res(path: impl Into<PathBuf>) {
442    if lazy_static_init(&BUILT_RES, path.into()).is_err() {
443        panic!("cannot `init_built_res`, `res` has already inited")
444    }
445}
446
447lazy_static! {
448    static ref RES: PathBuf = find_res();
449
450    #[cfg(any(debug_assertions, feature="built_res"))]
451    static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
452}
453#[cfg(target_os = "android")]
454fn find_res() -> PathBuf {
455    android_internal("res")
456}
457#[cfg(not(target_os = "android"))]
458fn find_res() -> PathBuf {
459    #[cfg(not(target_arch = "wasm32"))]
460    if let Ok(mut p) = std::env::current_exe() {
461        p.set_extension("res-dir");
462        if let Ok(dir) = read_line(&p) {
463            return bin(dir);
464        }
465    }
466    if cfg!(debug_assertions) {
467        PathBuf::from("res")
468    } else if cfg!(target_arch = "wasm32") {
469        PathBuf::from("./res")
470    } else if cfg!(windows) {
471        bin("../res")
472    } else if cfg!(target_os = "macos") {
473        bin("../Resources")
474    } else if cfg!(target_family = "unix") {
475        let c = current_exe();
476        bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
477    } else {
478        panic!(
479            "resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
480            std::env::consts::OS
481        )
482    }
483}
484
485/// Gets a path relative to the user config directory for the app.
486///
487/// * The config dir can be set by [`init_config`] before any env dir is used.
488/// * In Android returns `android_internal("config")`.
489/// * In Linux, macOS and Windows if a file in `res("config-dir")` is found the first non-empty and non-comment (#) line
490///   defines the res path.
491/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_config/`.
492/// * In all platforms attempts [`directories::ProjectDirs::config_dir`] and panic if it fails.
493/// * If the config dir selected by the previous method contains a `"config-dir"` file it will be
494///   used to redirect to another config dir, you can use this to implement config migration. Redirection only happens once.
495///
496/// The config directory is created if it is missing, checks once on init or first use.
497///
498/// [`directories::ProjectDirs::config_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.config_dir
499pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
500    CONFIG.join(relative_path)
501}
502
503/// Sets a custom [`original_config`] path.
504///
505/// # Panics
506///
507/// Panics if not called at the beginning of the process.
508pub fn init_config(path: impl Into<PathBuf>) {
509    if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
510        panic!("cannot `init_config`, `original_config` has already inited")
511    }
512}
513
514/// Config path before migration.
515///
516/// If this is equal to [`config`] the config has not migrated.
517pub fn original_config() -> PathBuf {
518    ORIGINAL_CONFIG.clone()
519}
520lazy_static! {
521    static ref ORIGINAL_CONFIG: PathBuf = find_config();
522}
523
524/// Copied all config to `new_path` and saves it as the config path.
525///
526/// If copying and saving path succeeds make a best effort to wipe the previous config dir. If copy and save fails
527/// makes a best effort to undo already made copies.
528///
529/// The `new_path` must not exist or be empty.
530pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
531    migrate_config_impl(new_path.as_ref())
532}
533fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
534    let prev_path = CONFIG.as_path();
535
536    if prev_path == new_path {
537        return Ok(());
538    }
539
540    let original_path = ORIGINAL_CONFIG.as_path();
541    let is_return = new_path == original_path;
542
543    if !is_return && dir_exists_not_empty(new_path) {
544        return Err(io::Error::new(
545            io::ErrorKind::AlreadyExists,
546            "can only migrate to new dir or empty dir",
547        ));
548    }
549    let created = !new_path.exists();
550    if created {
551        fs::create_dir_all(new_path)?;
552    }
553
554    let migrate = |from: &Path, to: &Path| {
555        copy_dir_all(from, to)?;
556        if fs::remove_dir_all(from).is_ok() {
557            fs::create_dir(from)?;
558        }
559
560        let redirect = ORIGINAL_CONFIG.join("config-dir");
561        if is_return {
562            fs::remove_file(redirect)
563        } else {
564            fs::write(redirect, to.display().to_string().as_bytes())
565        }
566    };
567
568    if let Err(e) = migrate(prev_path, new_path) {
569        eprintln!("migration failed, {e}");
570        if fs::remove_dir_all(new_path).is_ok() && !created {
571            let _ = fs::create_dir(new_path);
572        }
573    }
574
575    tracing::info!("changed config dir to `{}`", new_path.display());
576
577    Ok(())
578}
579
580fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
581    for entry in fs::read_dir(from)? {
582        let from = entry?.path();
583        if from.is_dir() {
584            let to = to.join(from.file_name().unwrap());
585            fs::create_dir(&to)?;
586            copy_dir_all(&from, &to)?;
587        } else if from.is_file() {
588            let to = to.join(from.file_name().unwrap());
589            fs::copy(&from, &to)?;
590        } else {
591            continue;
592        }
593    }
594    Ok(())
595}
596
597lazy_static! {
598    static ref CONFIG: PathBuf = redirect_config(original_config());
599}
600
601#[cfg(target_os = "android")]
602fn find_config() -> PathBuf {
603    android_internal("config")
604}
605#[cfg(not(target_os = "android"))]
606fn find_config() -> PathBuf {
607    let cfg_dir = res("config-dir");
608    if let Ok(dir) = read_line(&cfg_dir) {
609        return res(dir);
610    }
611
612    if cfg!(debug_assertions) {
613        return PathBuf::from("target/tmp/dev_config/");
614    }
615
616    let a = about();
617    if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier, &a.org, &a.app) {
618        dirs.config_dir().to_owned()
619    } else {
620        panic!(
621            "config dir not specified for platform {}, use a '{}' file to specify an alternative",
622            std::env::consts::OS,
623            cfg_dir.display(),
624        )
625    }
626}
627fn redirect_config(cfg: PathBuf) -> PathBuf {
628    if cfg!(target_arch = "wasm32") {
629        return cfg;
630    }
631
632    if let Ok(dir) = read_line(&cfg.join("config-dir")) {
633        let mut dir = PathBuf::from(dir);
634        if dir.is_relative() {
635            dir = cfg.join(dir);
636        }
637        if dir.exists() {
638            let test_path = dir.join(".zng-config-test");
639            if let Err(e) = fs::create_dir_all(&dir)
640                .and_then(|_| fs::write(&test_path, "# check write access"))
641                .and_then(|_| fs::remove_file(&test_path))
642            {
643                eprintln!("error writing to migrated `{}`, {e}", dir.display());
644                tracing::error!("error writing to migrated `{}`, {e}", dir.display());
645                return cfg;
646            }
647        } else if let Err(e) = fs::create_dir_all(&dir) {
648            eprintln!("error creating migrated `{}`, {e}", dir.display());
649            tracing::error!("error creating migrated `{}`, {e}", dir.display());
650            return cfg;
651        }
652        dir
653    } else {
654        create_dir_opt(cfg)
655    }
656}
657
658fn create_dir_opt(dir: PathBuf) -> PathBuf {
659    if let Err(e) = std::fs::create_dir_all(&dir) {
660        eprintln!("error creating `{}`, {e}", dir.display());
661        tracing::error!("error creating `{}`, {e}", dir.display());
662    }
663    dir
664}
665
666/// Gets a path relative to the cache directory for the app.
667///
668/// * The cache dir can be set by [`init_cache`] before any env dir is used.
669/// * In Android returns `android_internal("cache")`.
670/// * In Linux, macOS and Windows if a file `config("cache-dir")` is found the first non-empty and non-comment (#) line
671///   defines the res path.
672/// * In `cfg(debug_assertions)` builds returns `target/tmp/dev_cache/`.
673/// * In all platforms attempts [`directories::ProjectDirs::cache_dir`] and panic if it fails.
674///
675/// The cache dir is created if it is missing, checks once on init or first use.
676///
677/// [`directories::ProjectDirs::cache_dir`]: https://docs.rs/directories/5.0/directories/struct.ProjectDirs.html#method.cache_dir
678pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
679    CACHE.join(relative_path)
680}
681
682/// Sets a custom [`cache`] path.
683///
684/// # Panics
685///
686/// Panics if not called at the beginning of the process.
687pub fn init_cache(path: impl Into<PathBuf>) {
688    match lazy_static_init(&CONFIG, path.into()) {
689        Ok(p) => {
690            create_dir_opt(p.to_owned());
691        }
692        Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
693    }
694}
695
696/// Removes all cache files possible.
697///
698/// Continues removing after the first fail, returns the last error.
699pub fn clear_cache() -> io::Result<()> {
700    best_effort_clear(CACHE.as_path())
701}
702fn best_effort_clear(path: &Path) -> io::Result<()> {
703    let mut error = None;
704
705    match fs::read_dir(path) {
706        Ok(cache) => {
707            for entry in cache {
708                match entry {
709                    Ok(e) => {
710                        let path = e.path();
711                        if path.is_dir() {
712                            if fs::remove_dir_all(&path).is_err() {
713                                match best_effort_clear(&path) {
714                                    Ok(()) => {
715                                        if let Err(e) = fs::remove_dir(&path) {
716                                            error = Some(e)
717                                        }
718                                    }
719                                    Err(e) => {
720                                        error = Some(e);
721                                    }
722                                }
723                            }
724                        } else if path.is_file() {
725                            if let Err(e) = fs::remove_file(&path) {
726                                error = Some(e);
727                            }
728                        }
729                    }
730                    Err(e) => {
731                        error = Some(e);
732                    }
733                }
734            }
735        }
736        Err(e) => {
737            error = Some(e);
738        }
739    }
740
741    match error {
742        Some(e) => Err(e),
743        None => Ok(()),
744    }
745}
746
747/// Save `new_path` as the new cache path and make a best effort to move existing cache files.
748///
749/// Note that the move failure is not considered an error (it is only logged), the app is expected to
750/// rebuild missing cache entries.
751///
752/// Note that [`cache`] will still point to the previous path on success, the app must be restarted to use the new cache.
753///
754/// The `new_path` must not exist or be empty.
755pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
756    migrate_cache_impl(new_path.as_ref())
757}
758fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
759    if dir_exists_not_empty(new_path) {
760        return Err(io::Error::new(
761            io::ErrorKind::AlreadyExists,
762            "can only migrate to new dir or empty dir",
763        ));
764    }
765    fs::create_dir_all(new_path)?;
766    let write_test = new_path.join(".zng-cache");
767    fs::write(&write_test, "# zng cache dir".as_bytes())?;
768    fs::remove_file(&write_test)?;
769
770    fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
771
772    tracing::info!("changed cache dir to `{}`", new_path.display());
773
774    let prev_path = CACHE.as_path();
775    if prev_path == new_path {
776        return Ok(());
777    }
778    if let Err(e) = best_effort_move(prev_path, new_path) {
779        eprintln!("failed to migrate all cache files, {e}");
780        tracing::error!("failed to migrate all cache files, {e}");
781    }
782
783    Ok(())
784}
785
786fn dir_exists_not_empty(dir: &Path) -> bool {
787    match fs::read_dir(dir) {
788        Ok(dir) => {
789            for entry in dir {
790                match entry {
791                    Ok(_) => return true,
792                    Err(e) => {
793                        if e.kind() != io::ErrorKind::NotFound {
794                            return true;
795                        }
796                    }
797                }
798            }
799            false
800        }
801        Err(e) => e.kind() != io::ErrorKind::NotFound,
802    }
803}
804
805fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
806    let mut error = None;
807
808    match fs::read_dir(from) {
809        Ok(cache) => {
810            for entry in cache {
811                match entry {
812                    Ok(e) => {
813                        let from = e.path();
814                        if from.is_dir() {
815                            let to = to.join(from.file_name().unwrap());
816                            if let Err(e) = fs::rename(&from, &to).or_else(|_| {
817                                fs::create_dir(&to)?;
818                                best_effort_move(&from, &to)?;
819                                fs::remove_dir(&from)
820                            }) {
821                                error = Some(e)
822                            }
823                        } else if from.is_file() {
824                            let to = to.join(from.file_name().unwrap());
825                            if let Err(e) = fs::rename(&from, &to).or_else(|_| {
826                                fs::copy(&from, &to)?;
827                                fs::remove_file(&from)
828                            }) {
829                                error = Some(e);
830                            }
831                        }
832                    }
833                    Err(e) => {
834                        error = Some(e);
835                    }
836                }
837            }
838        }
839        Err(e) => {
840            error = Some(e);
841        }
842    }
843
844    match error {
845        Some(e) => Err(e),
846        None => Ok(()),
847    }
848}
849
850lazy_static! {
851    static ref CACHE: PathBuf = create_dir_opt(find_cache());
852}
853#[cfg(target_os = "android")]
854fn find_cache() -> PathBuf {
855    android_internal("cache")
856}
857#[cfg(not(target_os = "android"))]
858fn find_cache() -> PathBuf {
859    let cache_dir = config("cache-dir");
860    if let Ok(dir) = read_line(&cache_dir) {
861        return config(dir);
862    }
863
864    if cfg!(debug_assertions) {
865        return PathBuf::from("target/tmp/dev_cache/");
866    }
867
868    let a = about();
869    if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier, &a.org, &a.app) {
870        dirs.cache_dir().to_owned()
871    } else {
872        panic!(
873            "cache dir not specified for platform {}, use a '{}' file to specify an alternative",
874            std::env::consts::OS,
875            cache_dir.display(),
876        )
877    }
878}
879
880fn current_exe() -> PathBuf {
881    std::env::current_exe().expect("current_exe path is required")
882}
883
884fn read_line(path: &Path) -> io::Result<String> {
885    let file = fs::File::open(path)?;
886    for line in io::BufReader::new(file).lines() {
887        let line = line?;
888        let line = line.trim();
889        if line.starts_with('#') {
890            continue;
891        }
892        return Ok(line.into());
893    }
894    Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
895}
896
897#[cfg(target_os = "android")]
898mod android {
899    use super::*;
900
901    lazy_static! {
902        static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
903    }
904
905    /// Initialize the Android app paths.
906    ///
907    /// This is called by `init_android_app` provided by view-process implementers.
908    pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
909        if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
910            panic!("cannot `init_android_paths`, already inited")
911        }
912    }
913
914    /// Gets a path relative to the internal storage reserved for the app.
915    ///
916    /// Prefer using [`config`] or [`cache`] over this directly.
917    pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
918        ANDROID_PATHS[0].join(relative_path)
919    }
920
921    /// Gets a path relative to the external storage reserved for the app.
922    ///
923    /// This directory is user accessible.
924    pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
925        ANDROID_PATHS[1].join(relative_path)
926    }
927}
928#[cfg(target_os = "android")]
929pub use android::*;
930
931#[cfg(test)]
932mod tests {
933    use crate::*;
934
935    #[test]
936    fn parse_manifest() {
937        init!();
938        let a = about();
939        assert_eq!(a.pkg_name, "zng-env");
940        assert_eq!(a.app, "zng-env");
941        assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
942        assert_eq!(a.org, "The Zng Project Developers");
943    }
944}