Bundle CLI launchers with desktop app
This commit is contained in:
1 parent
d21f5bd467
commit
f84f7247d2
4 files changed
+208
-2
No files matched your search
@@ -0,0 +1,167 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const COMMANDS: [&str; 2] = ["ironstorage", "ironstorage-tui"];
|
||||
|
||||
pub(crate) fn install_packaged_launchers() -> io::Result<[PathBuf; 2]> {
|
||||
let executable = env::current_exe()?;
|
||||
let bundle_directory = executable.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"the desktop executable has no parent directory",
|
||||
)
|
||||
})?;
|
||||
let home = env::var_os("HOME")
|
||||
.or_else(|| env::var_os("USERPROFILE"))
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))?;
|
||||
install_launchers(bundle_directory, &home)
|
||||
}
|
||||
|
||||
fn install_launchers(bundle_directory: &Path, home: &Path) -> io::Result<[PathBuf; 2]> {
|
||||
let sources = COMMANDS
|
||||
.map(|command| bundle_directory.join(format!("{command}{}", env::consts::EXE_SUFFIX)));
|
||||
for source in &sources {
|
||||
if !source.is_file() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("packaged command not found: {}", source.display()),
|
||||
));
|
||||
}
|
||||
}
|
||||
let sources = [sources[0].canonicalize()?, sources[1].canonicalize()?];
|
||||
|
||||
let directory = home.join(".local/bin");
|
||||
let targets = COMMANDS.map(|command| directory.join(command));
|
||||
for target in &targets {
|
||||
match fs::symlink_metadata(target) {
|
||||
Ok(metadata) if metadata.is_dir() => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::IsADirectory,
|
||||
format!("command link target is a directory: {}", target.display()),
|
||||
));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&directory)?;
|
||||
for (source, target) in sources.iter().zip(&targets) {
|
||||
if fs::symlink_metadata(target).is_ok() {
|
||||
fs::remove_file(target)?;
|
||||
}
|
||||
symlink(source, target)?;
|
||||
}
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn symlink(source: &Path, target: &Path) -> io::Result<()> {
|
||||
std::os::unix::fs::symlink(source, target)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn symlink(source: &Path, target: &Path) -> io::Result<()> {
|
||||
std::os::windows::fs::symlink_file(source, target)
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
fn symlink(_source: &Path, _target: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"command links are not supported on this platform",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn packaged_commands(directory: &Path) -> [PathBuf; 2] {
|
||||
let sources =
|
||||
COMMANDS.map(|command| directory.join(format!("{command}{}", env::consts::EXE_SUFFIX)));
|
||||
for source in &sources {
|
||||
fs::write(source, command_contents(source)).expect("write packaged command");
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
fn command_contents(path: &Path) -> String {
|
||||
path.file_name()
|
||||
.expect("command name")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installs_both_packaged_commands_and_replaces_existing_files() {
|
||||
let bundle = tempfile::tempdir().expect("bundle directory");
|
||||
let home = tempfile::tempdir().expect("home directory");
|
||||
let sources = packaged_commands(bundle.path());
|
||||
let bin = home.path().join(".local/bin");
|
||||
fs::create_dir_all(&bin).expect("bin directory");
|
||||
fs::write(bin.join("ironstorage"), "old command").expect("old command");
|
||||
|
||||
let targets = install_launchers(bundle.path(), home.path()).expect("install launchers");
|
||||
install_launchers(bundle.path(), home.path()).expect("reinstall launchers");
|
||||
|
||||
for ((source, target), expected_target) in sources
|
||||
.iter()
|
||||
.zip(&targets)
|
||||
.zip([bin.join("ironstorage"), bin.join("ironstorage-tui")])
|
||||
{
|
||||
assert_eq!(target, &expected_target);
|
||||
assert_eq!(
|
||||
fs::read_link(target).expect("read link"),
|
||||
source.canonicalize().expect("canonical source")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_all_sources_before_changing_existing_commands() {
|
||||
let bundle = tempfile::tempdir().expect("bundle directory");
|
||||
let home = tempfile::tempdir().expect("home directory");
|
||||
let first = bundle
|
||||
.path()
|
||||
.join(format!("ironstorage{}", env::consts::EXE_SUFFIX));
|
||||
fs::write(&first, "new command").expect("packaged command");
|
||||
let bin = home.path().join(".local/bin");
|
||||
fs::create_dir_all(&bin).expect("bin directory");
|
||||
let existing = bin.join("ironstorage");
|
||||
fs::write(&existing, "old command").expect("old command");
|
||||
|
||||
let error = install_launchers(bundle.path(), home.path()).expect_err("missing TUI");
|
||||
|
||||
assert_eq!(error.kind(), io::ErrorKind::NotFound);
|
||||
assert_eq!(
|
||||
fs::read_to_string(existing).expect("existing command"),
|
||||
"old command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_directory_targets_before_changing_other_commands() {
|
||||
let bundle = tempfile::tempdir().expect("bundle directory");
|
||||
let home = tempfile::tempdir().expect("home directory");
|
||||
packaged_commands(bundle.path());
|
||||
let bin = home.path().join(".local/bin");
|
||||
fs::create_dir_all(bin.join("ironstorage-tui")).expect("directory target");
|
||||
let existing = bin.join("ironstorage");
|
||||
fs::write(&existing, "old command").expect("old command");
|
||||
|
||||
let error = install_launchers(bundle.path(), home.path()).expect_err("directory target");
|
||||
|
||||
assert_eq!(error.kind(), io::ErrorKind::IsADirectory);
|
||||
assert_eq!(
|
||||
fs::read_to_string(existing).expect("existing command"),
|
||||
"old command"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user