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