use std::{
ffi::{OsStr, OsString},
path::{Path, PathBuf},
};
const PROGRAM: &str = "artifactize-desktop";
#[cfg(target_os = "linux")]
const APPIMAGE_RECORD: &str = "appimage-path";
#[cfg(windows)]
const STORE_PACKAGE_FAMILY: &str = "lhj6102.Artifactize_ayfzfzbsv48sg";
pub(crate) fn installed() -> Option<PathBuf> {
let executable = std::env::current_exe()
.ok()
.map(|executable| std::fs::canonicalize(&executable).unwrap_or(executable));
let cwd = std::env::current_dir().ok();
let home = super::home_directory();
find(
executable.as_deref(),
cwd.as_deref(),
super::environment::var,
locations(super::environment::var, home.as_deref()),
)
}
fn find(
executable: Option<&Path>,
cwd: Option<&Path>,
variable: impl Fn(&str) -> Option<OsString>,
locations: Vec<PathBuf>,
) -> Option<PathBuf> {
executable
.and_then(Path::parent)
.map(|bin| bin.join(format!("{PROGRAM}{}", std::env::consts::EXE_SUFFIX)))
.filter(|beside| beside.is_file())
.or_else(|| {
cwd.and_then(|cwd| {
super::program::resolve_with(OsStr::new(PROGRAM), cwd, variable).ok()
})
})
.or_else(|| locations.into_iter().find(|location| location.exists()))
}
#[cfg(target_os = "macos")]
fn locations(_variable: impl Fn(&str) -> Option<OsString>, home: Option<&Path>) -> Vec<PathBuf> {
let bundle = Path::new("Artifactize.app/Contents/MacOS").join(PROGRAM);
std::iter::once(PathBuf::from("/Applications"))
.chain(home.map(|home| home.join("Applications")))
.map(|applications| applications.join(&bundle))
.collect()
}
#[cfg(target_os = "linux")]
fn locations(variable: impl Fn(&str) -> Option<OsString>, home: Option<&Path>) -> Vec<PathBuf> {
let Some(artifactize) = home.map(|home| data_home(variable, home).join("artifactize")) else {
return Vec::new();
};
let mut locations = vec![artifactize.join("desktop/AppRun")];
locations.extend(
std::fs::read(artifactize.join(APPIMAGE_RECORD))
.ok()
.filter(|path| !path.is_empty())
.map(|path| super::path_from_bytes(&path)),
);
locations
}
#[cfg(windows)]
fn locations(variable: impl Fn(&str) -> Option<OsString>, _home: Option<&Path>) -> Vec<PathBuf> {
let Some(local) = variable("LOCALAPPDATA")
.filter(|path| !path.is_empty())
.map(PathBuf::from)
else {
return Vec::new();
};
vec![
local
.join("Programs/artifactize")
.join(format!("{PROGRAM}.exe")),
local
.join("Microsoft/WindowsApps")
.join(STORE_PACKAGE_FAMILY),
]
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn locations(_variable: impl Fn(&str) -> Option<OsString>, _home: Option<&Path>) -> Vec<PathBuf> {
Vec::new()
}
#[cfg(target_os = "linux")]
pub(super) fn data_home(variable: impl Fn(&str) -> Option<OsString>, home: &Path) -> PathBuf {
variable("XDG_DATA_HOME")
.filter(|path| !path.is_empty())
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.unwrap_or_else(|| home.join(".local/share"))
}
#[cfg(target_os = "linux")]
pub(super) fn record_appimage(artifactize: &Path, appimage: &Path) -> std::io::Result<()> {
use std::{io::Write, os::unix::ffi::OsStrExt};
let record = artifactize.join(APPIMAGE_RECORD);
let bytes = appimage.as_os_str().as_bytes();
if std::fs::read(&record).is_ok_and(|recorded| recorded == bytes) {
return Ok(());
}
std::fs::create_dir_all(artifactize)?;
let mut temporary = tempfile::NamedTempFile::new_in(artifactize)?;
temporary.write_all(bytes)?;
temporary.persist(record).map_err(|error| error.error)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn variables(pairs: &[(&str, &Path)]) -> impl Fn(&str) -> Option<OsString> {
let pairs: Vec<(String, OsString)> = pairs
.iter()
.map(|(name, value)| (name.to_string(), value.as_os_str().to_owned()))
.chain([("PATHEXT".to_owned(), OsString::from(".EXE"))])
.collect();
move |name| {
pairs
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.clone())
}
}
#[test]
fn the_app_beside_the_running_command_is_found_first() {
let root = crate::test_os::tempdir();
let bin = root.path().join("bin");
std::fs::create_dir(&bin).unwrap();
let app = bin.join(format!("{PROGRAM}{}", std::env::consts::EXE_SUFFIX));
std::fs::write(&app, "").unwrap();
let elsewhere = root.path().join("elsewhere");
std::fs::write(&elsewhere, "").unwrap();
assert_eq!(
find(
Some(&bin.join("artifactize")),
Some(root.path()),
variables(&[]),
vec![elsewhere],
),
Some(app)
);
}
#[test]
fn the_app_on_path_is_found_when_none_is_beside_the_command() {
let root = crate::test_os::tempdir();
let path = root.path().join("path");
std::fs::create_dir(&path).unwrap();
let app = path.join(PROGRAM);
std::fs::write(&app, "").unwrap();
crate::test_os::make_executable(&app);
let found = find(
Some(&root.path().join("artifactize")),
Some(root.path()),
variables(&[("PATH", &path)]),
Vec::new(),
)
.expect("the app on PATH");
assert_eq!(found.parent(), Some(path.as_path()));
}
#[test]
fn an_existing_install_location_is_found_and_absent_ones_are_not() {
let root = crate::test_os::tempdir();
let missing = root.path().join("missing");
let installed = root.path().join("Artifactize.app");
assert_eq!(
find(None, None, variables(&[]), vec![missing.clone()]),
None
);
std::fs::create_dir(&installed).unwrap();
assert_eq!(
find(None, None, variables(&[]), vec![missing, installed.clone()]),
Some(installed)
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_locations_are_the_system_and_home_applications_folders() {
let root = crate::test_os::tempdir();
let home = root.path();
assert_eq!(
locations(variables(&[]), Some(home)),
[
PathBuf::from("/Applications/Artifactize.app/Contents/MacOS/artifactize-desktop"),
home.join("Applications/Artifactize.app/Contents/MacOS/artifactize-desktop"),
]
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_locations_include_the_unpacked_app_and_the_recorded_appimage() {
let root = crate::test_os::tempdir();
let data = root.path().join("data");
let artifactize = data.join("artifactize");
let read = || locations(variables(&[("XDG_DATA_HOME", &data)]), Some(root.path()));
assert_eq!(read(), [artifactize.join("desktop/AppRun")]);
let appimage = root.path().join("Downloads/Artifactize.AppImage");
record_appimage(&artifactize, &appimage).unwrap();
record_appimage(&artifactize, &appimage).unwrap();
assert_eq!(read(), [artifactize.join("desktop/AppRun"), appimage]);
assert_eq!(
locations(
variables(&[("XDG_DATA_HOME", Path::new("relative"))]),
Some(root.path())
),
[root.path().join(".local/share/artifactize/desktop/AppRun")]
);
}
#[cfg(windows)]
#[test]
fn windows_locations_are_the_install_folder_and_the_store_package() {
let root = crate::test_os::tempdir();
let local = root.path();
assert_eq!(
locations(variables(&[("LOCALAPPDATA", local)]), None),
[
local.join("Programs/artifactize/artifactize-desktop.exe"),
local
.join("Microsoft/WindowsApps")
.join(STORE_PACKAGE_FAMILY),
]
);
}
}