From 6c5f68c7d24150dee982dd5400c45e64163305dd Mon Sep 17 00:00:00 2001 From: gram Date: Tue, 6 Jan 2026 12:33:26 +0100 Subject: [PATCH 1/3] draft postinstall --- src/commands/mod.rs | 1 + src/commands/postinstall.rs | 100 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/commands/postinstall.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e4bab2b..0daca20 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -11,6 +11,7 @@ mod logs; mod monitor; mod name; mod new; +mod postinstall; mod repl; mod runtime; mod shots; diff --git a/src/commands/postinstall.rs b/src/commands/postinstall.rs new file mode 100644 index 0000000..4a74240 --- /dev/null +++ b/src/commands/postinstall.rs @@ -0,0 +1,100 @@ +use anyhow::{Result, bail}; +use std::path::{Path, PathBuf}; + +pub fn cmd_postinstall() -> Result<()> { + let Some(path) = find_writable_path() else { + bail!("cannot write writable dir in PATH") + }; + move_self_to(&path)?; + create_alias(&path)?; + Ok(()) +} + +fn move_self_to(new_path: &Path) -> Result<()> { + let Some(old_path) = std::env::args().next() else { + bail!("cannot access process args"); + }; + let old_path = PathBuf::from(old_path); + let new_path = new_path.join("firefly_cli"); + std::fs::rename(old_path, new_path)?; + Ok(()) +} + +fn create_alias(dir_path: &Path) -> Result<()> { + #[cfg(unix)] + create_alias_unix(dir_path)?; + #[cfg(not(unix))] + println!("⚠️ The `ff` alias can be created only on UNIX systems."); + Ok(()) +} + +#[cfg(unix)] +fn create_alias_unix(dir_path: &Path) -> Result<()> { + let old_path = dir_path.join("firefly_cli"); + let new_path = dir_path.join("ff"); + std::os::unix::fs::symlink(old_path, new_path)?; + Ok(()) +} + +/// Find a path in `$PATH` in which the current user can create files. +fn find_writable_path() -> Option { + let paths = load_paths(); + + // Prefer writable paths in the user home directory. + if let Some(home) = std::env::home_dir() { + for path in &paths { + let in_home = path.starts_with(&home); + if in_home && is_writable(path) { + return Some(path.clone()); + } + } + } + + // If no writable paths in the home dir, find a writable path naywhere else. + for path in &paths { + if is_writable(path) { + return Some(path.clone()); + } + } + + // No writable paths in $PATH. + None +} + +fn is_writable(path: &Path) -> bool { + let Ok(meta) = std::fs::metadata(path) else { + return false; + }; + let readonly = meta.permissions().readonly(); + if readonly { + return false; + } + + // Even if the dir is not marked as readonly, file writes to it may still fail. + // So, there is only one way to know for sure. + let file_path = path.join("_temp-file-by-firefly-cli-pls-delete"); + let res = std::fs::write(&file_path, ""); + _ = std::fs::remove_file(file_path); + res.is_ok() +} + +/// Read and parse paths from `$PATH`. +fn load_paths() -> Vec { + let Ok(raw) = std::env::var("PATH") else { + return Vec::new(); + }; + parse_paths(&raw) +} + +fn parse_paths(raw: &str) -> Vec { + #[cfg(windows)] + const SEP: char = ';'; + #[cfg(not(windows))] + const SEP: char = ':'; + + let mut paths = Vec::new(); + for path in raw.split(SEP) { + paths.push(PathBuf::from(path)); + } + paths +} From 3e2b5e5b55c68c5d8605306a11415236608aa573 Mon Sep 17 00:00:00 2001 From: gram Date: Tue, 6 Jan 2026 13:33:53 +0100 Subject: [PATCH 2/3] support adding binary to $PATH --- src/commands/postinstall.rs | 55 +++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/commands/postinstall.rs b/src/commands/postinstall.rs index 4a74240..cf718dc 100644 --- a/src/commands/postinstall.rs +++ b/src/commands/postinstall.rs @@ -1,15 +1,33 @@ use anyhow::{Result, bail}; -use std::path::{Path, PathBuf}; +use std::{ + io::Write, + path::{Path, PathBuf}, +}; pub fn cmd_postinstall() -> Result<()> { - let Some(path) = find_writable_path() else { - bail!("cannot write writable dir in PATH") - }; - move_self_to(&path)?; + let path = move_self()?; create_alias(&path)?; Ok(()) } +/// Move the currently running executable into $PATH. +fn move_self() -> Result { + if let Some(path) = find_writable_path() { + move_self_to(&path)?; + return Ok(path); + } + if let Some(home) = std::env::home_dir() { + let path = home.join(".local").join("bin"); + if is_writable(&path) { + move_self_to(&path)?; + add_path(&path)?; + return Ok(path); + } + } + bail!("cannot write writable dir in $PATH") +} + +/// Move the currently running executable into the given path. fn move_self_to(new_path: &Path) -> Result<()> { let Some(old_path) = std::env::args().next() else { bail!("cannot access process args"); @@ -20,6 +38,7 @@ fn move_self_to(new_path: &Path) -> Result<()> { Ok(()) } +/// Create `ff` shortcut for `firefly_cli`. fn create_alias(dir_path: &Path) -> Result<()> { #[cfg(unix)] create_alias_unix(dir_path)?; @@ -61,6 +80,7 @@ fn find_writable_path() -> Option { None } +/// Check if the current user can create files in the given directory. fn is_writable(path: &Path) -> bool { let Ok(meta) = std::fs::metadata(path) else { return false; @@ -98,3 +118,28 @@ fn parse_paths(raw: &str) -> Vec { } paths } + +/// Add the given directory into `$PATH`. +fn add_path(path: &Path) -> Result<()> { + let Some(home) = std::env::home_dir() else { + bail!("home dir not found"); + }; + let zshrc = home.join(".zshrc"); + if zshrc.exists() { + return add_path_to(&zshrc, path); + } + let bashhrc = home.join(".bashhrc"); + if bashhrc.exists() { + return add_path_to(&bashhrc, path); + } + bail!("cannot find .zshrc or .bashrc") +} + +fn add_path_to(profile: &Path, path: &Path) -> Result<()> { + let mut file = std::fs::OpenOptions::new().append(true).open(profile)?; + let path_bin = path.as_os_str().as_encoded_bytes(); + file.write_all(b"\n\nexport PATH=\"$PATH:")?; + file.write_all(path_bin)?; + file.write_all(b"\"\n")?; + Ok(()) +} From 2d2cb247a30abe44764365e11147c99ecc96d6b2 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 7 Jan 2026 16:54:38 +0100 Subject: [PATCH 3/3] expose postinstall --- src/args.rs | 3 +++ src/cli.rs | 1 + src/commands/mod.rs | 1 + src/commands/postinstall.rs | 7 +++++-- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/args.rs b/src/args.rs index 9f30e86..40ca40b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -16,6 +16,9 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { + #[clap(hide = true)] + Postinstall, + /// Build the project and install it locally (into VFS). Build(BuildArgs), diff --git a/src/cli.rs b/src/cli.rs index a8046e3..0bf4dc6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { use Commands::*; match command { + Postinstall => cmd_postinstall(), Build(args) => cmd_build(vfs, args), Export(args) => cmd_export(&vfs, args), Import(args) => cmd_import(&vfs, args), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0daca20..ece4d12 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -31,6 +31,7 @@ pub use logs::cmd_logs; pub use monitor::cmd_monitor; pub use name::{cmd_name_generate, cmd_name_get, cmd_name_set}; pub use new::cmd_new; +pub use postinstall::cmd_postinstall; pub use repl::cmd_repl; pub use runtime::{cmd_exit, cmd_id, cmd_launch, cmd_restart, cmd_screenshot}; pub use shots::cmd_shots_download; diff --git a/src/commands/postinstall.rs b/src/commands/postinstall.rs index cf718dc..76d7907 100644 --- a/src/commands/postinstall.rs +++ b/src/commands/postinstall.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use std::{ io::Write, path::{Path, PathBuf}, @@ -33,8 +33,11 @@ fn move_self_to(new_path: &Path) -> Result<()> { bail!("cannot access process args"); }; let old_path = PathBuf::from(old_path); + if !old_path.exists() { + bail!("the binary is execute not by its path"); + } let new_path = new_path.join("firefly_cli"); - std::fs::rename(old_path, new_path)?; + std::fs::rename(old_path, new_path).context("move binary")?; Ok(()) }