#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
use std::{
fs,
io::{self, BufRead},
path::{Path, PathBuf},
str::FromStr,
};
use semver::Version;
use zng_txt::{ToTxt, Txt};
use zng_unique_id::{lazy_static, lazy_static_init};
mod process;
pub use process::*;
pub mod windows_subsystem;
lazy_static! {
static ref ABOUT: About = About::fallback_name();
}
#[allow(clippy::test_attr_in_doctest)]
#[macro_export]
macro_rules! init {
() => {
let _on_main_exit = $crate::init_parse!($crate);
};
}
#[doc(hidden)]
pub use zng_env_proc_macros::init_parse;
#[doc(hidden)]
pub fn init(about: About) -> Box<dyn std::any::Any> {
if !about.is_test {
if lazy_static_init(&ABOUT, about).is_err() {
panic!("env::init! already called\nnote: In `cfg(test)` builds init! can be called multiple times")
}
Box::new(process_init())
} else {
if lazy_static_init(&ABOUT, about).is_ok() {
Box::leak(Box::new(process_init()));
}
Box::new(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct About {
pub pkg_name: Txt,
pub pkg_authors: Box<[Txt]>,
pub version: Version,
pub app_id: Txt,
pub app: Txt,
pub org: Txt,
pub description: Txt,
pub homepage: Txt,
pub license: Txt,
pub has_about: bool,
pub meta: Vec<(Txt, Txt)>,
pub is_test: bool,
}
impl About {
pub fn crate_name(&self) -> Txt {
self.pkg_name.replace('-', "_").into()
}
pub fn qualifier(&self) -> Txt {
if let Some((_, v)) = self.meta.iter().find(|(k, _)| k == "qualifier") {
return v.clone();
}
self.try_qualifier().map(Txt::from_str).unwrap_or_default()
}
fn try_qualifier(&self) -> Option<&str> {
let last_dot = self.app_id.rfind('.')?;
let len = self.app_id[..last_dot].rfind('.')?;
Some(&self.app_id[..len])
}
pub fn get(&self, key: &str) -> Option<Txt> {
match key {
"pkg_name" => Some(self.pkg_name.clone()),
"pkg_authors" => {
let mut r = String::new();
let mut sep = "";
for a in &self.pkg_authors {
r.push_str(sep);
r.push_str(a);
sep = ", ";
}
Some(r.into())
}
"version" => Some(self.version.to_txt()),
"app_id" => Some(self.app_id.clone()),
"app" => Some(self.app.clone()),
"org" => Some(self.org.clone()),
"description" => Some(self.description.clone()),
"homepage" => Some(self.homepage.clone()),
"license" => Some(self.license.clone()),
"crate_name" => Some(self.crate_name()),
"qualifier" => Some(self.qualifier()),
_ => {
for (k, v) in self.meta.iter() {
if k == key {
return Some(v.clone());
}
}
None
}
}
}
pub fn windows_aumid(&self) -> Txt {
match self.get("windows_aumid") {
Some(id) => id,
None => self.app_id.clone(),
}
}
}
impl About {
fn fallback_name() -> Self {
Self {
pkg_name: Txt::from_static(""),
pkg_authors: Box::new([]),
version: Version::new(0, 0, 0),
app: fallback_name(),
org: Txt::from_static(""),
description: Txt::from_static(""),
homepage: Txt::from_static(""),
license: Txt::from_static(""),
has_about: false,
app_id: fallback_id(),
meta: vec![],
is_test: false,
}
}
#[cfg(feature = "parse")]
pub fn parse_manifest(cargo_toml: &str) -> Result<Self, toml::de::Error> {
#[derive(serde::Deserialize)]
struct Manifest {
package: Package,
}
#[derive(serde::Deserialize)]
struct Package {
name: Txt,
version: Version,
description: Option<Txt>,
homepage: Option<Txt>,
license: Option<Txt>,
authors: Option<Box<[Txt]>>,
metadata: Option<Metadata>,
}
#[derive(serde::Deserialize)]
struct Metadata {
zng: Option<Zng>,
}
#[derive(serde::Deserialize)]
struct Zng {
about: toml::Table,
}
let m: Manifest = toml::from_str(cargo_toml)?;
let mut about = About {
pkg_name: m.package.name,
pkg_authors: m.package.authors.unwrap_or_default(),
version: m.package.version,
description: m.package.description.unwrap_or_default(),
homepage: m.package.homepage.unwrap_or_default(),
license: m.package.license.unwrap_or_default(),
app: Txt::from_static(""),
org: Txt::from_static(""),
app_id: Txt::from_static(""),
has_about: false,
meta: vec![],
is_test: false,
};
if let Some(zng) = m.package.metadata.and_then(|m| m.zng)
&& !zng.about.is_empty()
{
let s = |key: &str| match zng.about.get(key) {
Some(toml::Value::String(s)) => Txt::from_str(s.as_str()),
_ => Txt::from_static(""),
};
about.has_about = true;
about.app = s("app");
about.org = s("org");
about.app_id = clean_id(&s("app_id"));
for (k, v) in zng.about {
if let toml::Value::String(v) = v
&& !["app", "org", "app_id"].contains(&k.as_str())
{
about.meta.push((k.into(), v.into()));
}
}
}
if about.app.is_empty() {
about.app = about.pkg_name.clone();
}
if about.org.is_empty() {
about.org = about.pkg_authors.first().cloned().unwrap_or_default();
}
if about.app_id.is_empty() {
about.app_id = clean_id(&format!(
"{}.{}.{}",
about.get("qualifier").unwrap_or_default(),
about.org,
about.app
));
}
Ok(about)
}
#[doc(hidden)]
#[expect(clippy::too_many_arguments)]
pub fn macro_new(
pkg_name: &'static str,
pkg_authors: &[&'static str],
(major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
app_id: &'static str,
app: &'static str,
org: &'static str,
description: &'static str,
homepage: &'static str,
license: &'static str,
has_about: bool,
meta: &[(&'static str, &'static str)],
is_test: bool,
) -> Self {
Self {
pkg_name: Txt::from_static(pkg_name),
pkg_authors: pkg_authors.iter().copied().map(Txt::from_static).collect(),
version: {
let mut v = Version::new(major, minor, patch);
v.pre = semver::Prerelease::from_str(pre).unwrap();
v.build = semver::BuildMetadata::from_str(build).unwrap();
v
},
app_id: Txt::from_static(app_id),
app: Txt::from_static(app),
org: Txt::from_static(org),
meta: meta.iter().map(|(k, v)| (Txt::from_static(k), Txt::from_static(v))).collect(),
description: Txt::from_static(description),
homepage: Txt::from_static(homepage),
license: Txt::from_static(license),
has_about,
is_test,
}
}
}
pub fn about() -> &'static About {
&ABOUT
}
fn fallback_name() -> Txt {
let exe = current_exe();
let exe_name = exe.file_name().unwrap().to_string_lossy();
let name = exe_name.split('.').find(|p| !p.is_empty()).unwrap();
Txt::from_str(name)
}
fn fallback_id() -> Txt {
let exe = current_exe();
let exe_name = exe.file_name().unwrap().to_string_lossy();
clean_id(&exe_name)
}
fn clean_id(raw: &str) -> Txt {
let mut r = String::new();
let mut sep = "";
for i in raw.split('.') {
let i = i.trim();
if i.is_empty() {
continue;
}
r.push_str(sep);
for (i, c) in i.trim().char_indices() {
if i == 0 {
if !c.is_ascii_alphabetic() {
r.push('i');
} else {
r.push(c.to_ascii_lowercase());
}
} else if c.is_ascii_alphanumeric() || c == '_' {
r.push(c.to_ascii_lowercase());
} else {
r.push('_');
}
}
sep = ".";
}
r.into()
}
pub fn bin(relative_path: impl AsRef<Path>) -> PathBuf {
BIN.join(relative_path)
}
lazy_static! {
static ref BIN: PathBuf = find_bin();
}
fn find_bin() -> PathBuf {
if cfg!(target_arch = "wasm32") {
PathBuf::from("./")
} else {
current_exe().parent().expect("current_exe path parent is required").to_owned()
}
}
pub fn res(relative_path: impl AsRef<Path>) -> PathBuf {
res_impl(relative_path.as_ref())
}
#[cfg(all(
any(debug_assertions, feature = "built_res"),
not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
))]
fn res_impl(relative_path: &Path) -> PathBuf {
let built = BUILT_RES.join(relative_path);
if built.exists() {
return built;
}
RES.join(relative_path)
}
#[cfg(not(all(
any(debug_assertions, feature = "built_res"),
not(any(target_os = "android", target_arch = "wasm32", target_os = "ios")),
)))]
fn res_impl(relative_path: &Path) -> PathBuf {
RES.join(relative_path)
}
pub fn android_install_res<Asset: std::io::Read>(open_res: impl FnOnce() -> Option<Asset>) {
#[cfg(target_os = "android")]
{
let version = res(format!(".zng-env.res.{}", about().version));
if !version.exists() {
if let Some(res) = open_res() {
if let Err(e) = install_res(version, res) {
tracing::error!("res install failed, {e}");
}
}
}
}
#[cfg(not(target_os = "android"))]
let _ = open_res;
}
#[cfg(target_os = "android")]
fn install_res(version: PathBuf, res: impl std::io::Read) -> std::io::Result<()> {
let res_path = version.parent().unwrap();
let _ = fs::remove_dir_all(res_path);
fs::create_dir(res_path)?;
let mut res = tar::Archive::new(res);
res.unpack(res_path)?;
let mut needs_pop = false;
for (i, entry) in fs::read_dir(&res_path)?.take(2).enumerate() {
needs_pop = i == 0 && entry?.file_name() == "res";
}
if needs_pop {
let tmp = res_path.parent().unwrap().join("res-tmp");
fs::rename(res_path.join("res"), &tmp)?;
fs::rename(tmp, res_path)?;
}
fs::File::create(&version)?;
Ok(())
}
pub fn init_res(path: impl Into<PathBuf>) {
if lazy_static_init(&RES, path.into()).is_err() {
panic!("cannot `init_res`, `res` has already inited")
}
}
#[cfg(any(debug_assertions, feature = "built_res"))]
pub fn init_built_res(path: impl Into<PathBuf>) {
if lazy_static_init(&BUILT_RES, path.into()).is_err() {
panic!("cannot `init_built_res`, `res` has already inited")
}
}
lazy_static! {
static ref RES: PathBuf = find_res();
#[cfg(any(debug_assertions, feature = "built_res"))]
static ref BUILT_RES: PathBuf = PathBuf::from("target/res");
}
#[cfg(target_os = "android")]
fn find_res() -> PathBuf {
android_internal("res")
}
#[cfg(not(target_os = "android"))]
fn find_res() -> PathBuf {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(mut p) = std::env::current_exe() {
p.set_extension("res-dir");
if let Ok(dir) = read_line(&p) {
return bin(dir);
}
}
if cfg!(debug_assertions) {
PathBuf::from("res")
} else if cfg!(target_arch = "wasm32") {
PathBuf::from("./res")
} else if cfg!(windows) {
bin("../res")
} else if cfg!(target_os = "macos") {
bin("../Resources")
} else if cfg!(target_family = "unix") {
let c = current_exe();
bin(format!("../share/{}", c.file_name().unwrap().to_string_lossy()))
} else {
panic!(
"resources dir not specified for platform {}, use a 'bin/current_exe_name.res-dir' file to specify an alternative",
std::env::consts::OS
)
}
}
pub fn config(relative_path: impl AsRef<Path>) -> PathBuf {
CONFIG.join(relative_path)
}
pub fn init_config(path: impl Into<PathBuf>) {
if lazy_static_init(&ORIGINAL_CONFIG, path.into()).is_err() {
panic!("cannot `init_config`, `original_config` has already inited")
}
}
pub fn original_config() -> PathBuf {
ORIGINAL_CONFIG.clone()
}
lazy_static! {
static ref ORIGINAL_CONFIG: PathBuf = find_config();
}
pub fn migrate_config(new_path: impl AsRef<Path>) -> io::Result<()> {
migrate_config_impl(new_path.as_ref())
}
fn migrate_config_impl(new_path: &Path) -> io::Result<()> {
let prev_path = CONFIG.as_path();
if prev_path == new_path {
return Ok(());
}
let original_path = ORIGINAL_CONFIG.as_path();
let is_return = new_path == original_path;
if !is_return && dir_exists_not_empty(new_path) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"can only migrate to new dir or empty dir",
));
}
let created = !new_path.exists();
if created {
fs::create_dir_all(new_path)?;
}
let migrate = |from: &Path, to: &Path| {
copy_dir_all(from, to)?;
if fs::remove_dir_all(from).is_ok() {
fs::create_dir(from)?;
}
let redirect = ORIGINAL_CONFIG.join("config-dir");
if is_return {
fs::remove_file(redirect)
} else {
fs::write(redirect, to.display().to_string().as_bytes())
}
};
if let Err(e) = migrate(prev_path, new_path) {
if fs::remove_dir_all(new_path).is_ok() && !created {
let _ = fs::create_dir(new_path);
}
return Err(e);
}
tracing::info!("changed config dir to `{}`", new_path.display());
Ok(())
}
fn copy_dir_all(from: &Path, to: &Path) -> io::Result<()> {
for entry in fs::read_dir(from)? {
let from = entry?.path();
if from.is_dir() {
let to = to.join(from.file_name().unwrap());
fs::create_dir(&to)?;
copy_dir_all(&from, &to)?;
} else if from.is_file() {
let to = to.join(from.file_name().unwrap());
fs::copy(&from, &to)?;
} else {
continue;
}
}
Ok(())
}
lazy_static! {
static ref CONFIG: PathBuf = redirect_config(original_config());
}
#[cfg(target_os = "android")]
fn find_config() -> PathBuf {
android_internal("config")
}
#[cfg(not(target_os = "android"))]
fn find_config() -> PathBuf {
let cfg_dir = res("config-dir");
if let Ok(dir) = read_line(&cfg_dir) {
return res(dir);
}
if cfg!(debug_assertions) {
return PathBuf::from("target/tmp/dev_config/");
}
let a = about();
if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
dirs.config_dir().to_owned()
} else {
panic!(
"config dir not specified for platform {}, use a '{}' file to specify an alternative",
std::env::consts::OS,
cfg_dir.display(),
)
}
}
fn redirect_config(cfg: PathBuf) -> PathBuf {
if cfg!(target_arch = "wasm32") {
return cfg;
}
if let Ok(dir) = read_line(&cfg.join("config-dir")) {
let mut dir = PathBuf::from(dir);
if dir.is_relative() {
dir = cfg.join(dir);
}
if dir.exists() {
let test_path = dir.join(".zng-config-test");
if let Err(e) = fs::create_dir_all(&dir)
.and_then(|_| fs::write(&test_path, "# check write access"))
.and_then(|_| fs::remove_file(&test_path))
{
eprintln!("error writing to migrated `{}`, {e}", dir.display());
tracing::error!("error writing to migrated `{}`, {e}", dir.display());
return cfg;
}
} else if let Err(e) = fs::create_dir_all(&dir) {
eprintln!("error creating migrated `{}`, {e}", dir.display());
tracing::error!("error creating migrated `{}`, {e}", dir.display());
return cfg;
}
dir
} else {
create_dir_opt(cfg)
}
}
fn create_dir_opt(dir: PathBuf) -> PathBuf {
if let Err(e) = std::fs::create_dir_all(&dir) {
eprintln!("error creating `{}`, {e}", dir.display());
tracing::error!("error creating `{}`, {e}", dir.display());
}
dir
}
pub fn cache(relative_path: impl AsRef<Path>) -> PathBuf {
CACHE.join(relative_path)
}
pub fn init_cache(path: impl Into<PathBuf>) {
match lazy_static_init(&CACHE, path.into()) {
Ok(p) => {
create_dir_opt(p.to_owned());
}
Err(_) => panic!("cannot `init_cache`, `cache` has already inited"),
}
}
pub fn clear_cache() -> io::Result<()> {
best_effort_clear(CACHE.as_path())
}
fn best_effort_clear(path: &Path) -> io::Result<()> {
let mut error = None;
match fs::read_dir(path) {
Ok(cache) => {
for entry in cache {
match entry {
Ok(e) => {
let path = e.path();
if path.is_dir() {
if fs::remove_dir_all(&path).is_err() {
match best_effort_clear(&path) {
Ok(()) => {
if let Err(e) = fs::remove_dir(&path) {
error = Some(e)
}
}
Err(e) => {
error = Some(e);
}
}
}
} else if path.is_file()
&& let Err(e) = fs::remove_file(&path)
{
error = Some(e);
}
}
Err(e) => {
error = Some(e);
}
}
}
}
Err(e) => {
error = Some(e);
}
}
match error {
Some(e) => Err(e),
None => Ok(()),
}
}
pub fn migrate_cache(new_path: impl AsRef<Path>) -> io::Result<()> {
migrate_cache_impl(new_path.as_ref())
}
fn migrate_cache_impl(new_path: &Path) -> io::Result<()> {
if dir_exists_not_empty(new_path) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"can only migrate to new dir or empty dir",
));
}
fs::create_dir_all(new_path)?;
let write_test = new_path.join(".zng-cache");
fs::write(&write_test, "# zng cache dir".as_bytes())?;
fs::remove_file(&write_test)?;
fs::write(config("cache-dir"), new_path.display().to_string().as_bytes())?;
tracing::info!("changed cache dir to `{}`", new_path.display());
let prev_path = CACHE.as_path();
if prev_path == new_path {
return Ok(());
}
if let Err(e) = best_effort_move(prev_path, new_path) {
eprintln!("failed to migrate all cache files, {e}");
tracing::error!("failed to migrate all cache files, {e}");
}
Ok(())
}
fn dir_exists_not_empty(dir: &Path) -> bool {
match fs::read_dir(dir) {
Ok(dir) => {
for entry in dir {
match entry {
Ok(_) => return true,
Err(e) => {
if e.kind() != io::ErrorKind::NotFound {
return true;
}
}
}
}
false
}
Err(e) => e.kind() != io::ErrorKind::NotFound,
}
}
fn best_effort_move(from: &Path, to: &Path) -> io::Result<()> {
let mut error = None;
match fs::read_dir(from) {
Ok(cache) => {
for entry in cache {
match entry {
Ok(e) => {
let from = e.path();
if from.is_dir() {
let to = to.join(from.file_name().unwrap());
if let Err(e) = fs::rename(&from, &to).or_else(|_| {
fs::create_dir(&to)?;
best_effort_move(&from, &to)?;
fs::remove_dir(&from)
}) {
error = Some(e)
}
} else if from.is_file() {
let to = to.join(from.file_name().unwrap());
if let Err(e) = fs::rename(&from, &to).or_else(|_| {
fs::copy(&from, &to)?;
fs::remove_file(&from)
}) {
error = Some(e);
}
}
}
Err(e) => {
error = Some(e);
}
}
}
}
Err(e) => {
error = Some(e);
}
}
match error {
Some(e) => Err(e),
None => Ok(()),
}
}
lazy_static! {
static ref CACHE: PathBuf = create_dir_opt(find_cache());
}
#[cfg(target_os = "android")]
fn find_cache() -> PathBuf {
android_internal("cache")
}
#[cfg(not(target_os = "android"))]
fn find_cache() -> PathBuf {
let cache_dir = config("cache-dir");
if let Ok(dir) = read_line(&cache_dir) {
return config(dir);
}
if cfg!(debug_assertions) {
return PathBuf::from("target/tmp/dev_cache/");
}
let a = about();
if let Some(dirs) = directories::ProjectDirs::from(&a.qualifier(), &a.org, &a.app) {
dirs.cache_dir().to_owned()
} else {
panic!(
"cache dir not specified for platform {}, use a '{}' file to specify an alternative",
std::env::consts::OS,
cache_dir.display(),
)
}
}
fn current_exe() -> PathBuf {
std::env::current_exe().expect("current_exe path is required")
}
fn read_line(path: &Path) -> io::Result<String> {
let file = fs::File::open(path)?;
for line in io::BufReader::new(file).lines() {
let line = line?;
let line = line.trim();
if line.starts_with('#') {
continue;
}
return Ok(line.into());
}
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "no uncommented line"))
}
#[cfg(target_os = "android")]
mod android {
use super::*;
lazy_static! {
static ref ANDROID_PATHS: [PathBuf; 2] = [PathBuf::new(), PathBuf::new()];
}
pub fn init_android_paths(internal: PathBuf, external: PathBuf) {
if lazy_static_init(&ANDROID_PATHS, [internal, external]).is_err() {
panic!("cannot `init_android_paths`, already inited")
}
}
pub fn android_internal(relative_path: impl AsRef<Path>) -> PathBuf {
ANDROID_PATHS[0].join(relative_path)
}
pub fn android_external(relative_path: impl AsRef<Path>) -> PathBuf {
ANDROID_PATHS[1].join(relative_path)
}
}
#[cfg(target_os = "android")]
pub use android::*;
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn parse_manifest() {
init!();
let a = about();
assert_eq!(a.pkg_name, "zng-env");
assert_eq!(a.app, "zng-env");
assert_eq!(&a.pkg_authors[..], &[Txt::from("The Zng Project Developers")]);
assert_eq!(a.org, "The Zng Project Developers");
}
}