Skip to content

Commit f06d145

Browse files
authored
Merge pull request #67 from firefly-zero/post-install
Post-installation hook
2 parents bd4d567 + 2d2cb24 commit f06d145

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

src/args.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub struct Cli {
1616

1717
#[derive(Subcommand)]
1818
pub enum Commands {
19+
#[clap(hide = true)]
20+
Postinstall,
21+
1922
/// Build the project and install it locally (into VFS).
2023
Build(BuildArgs),
2124

src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::path::PathBuf;
66
pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> {
77
use Commands::*;
88
match command {
9+
Postinstall => cmd_postinstall(),
910
Build(args) => cmd_build(vfs, args),
1011
Export(args) => cmd_export(&vfs, args),
1112
Import(args) => cmd_import(&vfs, args),

src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod logs;
1111
mod monitor;
1212
mod name;
1313
mod new;
14+
mod postinstall;
1415
mod repl;
1516
mod runtime;
1617
mod shots;
@@ -30,6 +31,7 @@ pub use logs::cmd_logs;
3031
pub use monitor::cmd_monitor;
3132
pub use name::{cmd_name_generate, cmd_name_get, cmd_name_set};
3233
pub use new::cmd_new;
34+
pub use postinstall::cmd_postinstall;
3335
pub use repl::cmd_repl;
3436
pub use runtime::{cmd_exit, cmd_id, cmd_launch, cmd_restart, cmd_screenshot};
3537
pub use shots::cmd_shots_download;

src/commands/postinstall.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use anyhow::{Context, Result, bail};
2+
use std::{
3+
io::Write,
4+
path::{Path, PathBuf},
5+
};
6+
7+
pub fn cmd_postinstall() -> Result<()> {
8+
let path = move_self()?;
9+
create_alias(&path)?;
10+
Ok(())
11+
}
12+
13+
/// Move the currently running executable into $PATH.
14+
fn move_self() -> Result<PathBuf> {
15+
if let Some(path) = find_writable_path() {
16+
move_self_to(&path)?;
17+
return Ok(path);
18+
}
19+
if let Some(home) = std::env::home_dir() {
20+
let path = home.join(".local").join("bin");
21+
if is_writable(&path) {
22+
move_self_to(&path)?;
23+
add_path(&path)?;
24+
return Ok(path);
25+
}
26+
}
27+
bail!("cannot write writable dir in $PATH")
28+
}
29+
30+
/// Move the currently running executable into the given path.
31+
fn move_self_to(new_path: &Path) -> Result<()> {
32+
let Some(old_path) = std::env::args().next() else {
33+
bail!("cannot access process args");
34+
};
35+
let old_path = PathBuf::from(old_path);
36+
if !old_path.exists() {
37+
bail!("the binary is execute not by its path");
38+
}
39+
let new_path = new_path.join("firefly_cli");
40+
std::fs::rename(old_path, new_path).context("move binary")?;
41+
Ok(())
42+
}
43+
44+
/// Create `ff` shortcut for `firefly_cli`.
45+
fn create_alias(dir_path: &Path) -> Result<()> {
46+
#[cfg(unix)]
47+
create_alias_unix(dir_path)?;
48+
#[cfg(not(unix))]
49+
println!("⚠️ The `ff` alias can be created only on UNIX systems.");
50+
Ok(())
51+
}
52+
53+
#[cfg(unix)]
54+
fn create_alias_unix(dir_path: &Path) -> Result<()> {
55+
let old_path = dir_path.join("firefly_cli");
56+
let new_path = dir_path.join("ff");
57+
std::os::unix::fs::symlink(old_path, new_path)?;
58+
Ok(())
59+
}
60+
61+
/// Find a path in `$PATH` in which the current user can create files.
62+
fn find_writable_path() -> Option<PathBuf> {
63+
let paths = load_paths();
64+
65+
// Prefer writable paths in the user home directory.
66+
if let Some(home) = std::env::home_dir() {
67+
for path in &paths {
68+
let in_home = path.starts_with(&home);
69+
if in_home && is_writable(path) {
70+
return Some(path.clone());
71+
}
72+
}
73+
}
74+
75+
// If no writable paths in the home dir, find a writable path naywhere else.
76+
for path in &paths {
77+
if is_writable(path) {
78+
return Some(path.clone());
79+
}
80+
}
81+
82+
// No writable paths in $PATH.
83+
None
84+
}
85+
86+
/// Check if the current user can create files in the given directory.
87+
fn is_writable(path: &Path) -> bool {
88+
let Ok(meta) = std::fs::metadata(path) else {
89+
return false;
90+
};
91+
let readonly = meta.permissions().readonly();
92+
if readonly {
93+
return false;
94+
}
95+
96+
// Even if the dir is not marked as readonly, file writes to it may still fail.
97+
// So, there is only one way to know for sure.
98+
let file_path = path.join("_temp-file-by-firefly-cli-pls-delete");
99+
let res = std::fs::write(&file_path, "");
100+
_ = std::fs::remove_file(file_path);
101+
res.is_ok()
102+
}
103+
104+
/// Read and parse paths from `$PATH`.
105+
fn load_paths() -> Vec<PathBuf> {
106+
let Ok(raw) = std::env::var("PATH") else {
107+
return Vec::new();
108+
};
109+
parse_paths(&raw)
110+
}
111+
112+
fn parse_paths(raw: &str) -> Vec<PathBuf> {
113+
#[cfg(windows)]
114+
const SEP: char = ';';
115+
#[cfg(not(windows))]
116+
const SEP: char = ':';
117+
118+
let mut paths = Vec::new();
119+
for path in raw.split(SEP) {
120+
paths.push(PathBuf::from(path));
121+
}
122+
paths
123+
}
124+
125+
/// Add the given directory into `$PATH`.
126+
fn add_path(path: &Path) -> Result<()> {
127+
let Some(home) = std::env::home_dir() else {
128+
bail!("home dir not found");
129+
};
130+
let zshrc = home.join(".zshrc");
131+
if zshrc.exists() {
132+
return add_path_to(&zshrc, path);
133+
}
134+
let bashhrc = home.join(".bashhrc");
135+
if bashhrc.exists() {
136+
return add_path_to(&bashhrc, path);
137+
}
138+
bail!("cannot find .zshrc or .bashrc")
139+
}
140+
141+
fn add_path_to(profile: &Path, path: &Path) -> Result<()> {
142+
let mut file = std::fs::OpenOptions::new().append(true).open(profile)?;
143+
let path_bin = path.as_os_str().as_encoded_bytes();
144+
file.write_all(b"\n\nexport PATH=\"$PATH:")?;
145+
file.write_all(path_bin)?;
146+
file.write_all(b"\"\n")?;
147+
Ok(())
148+
}

0 commit comments

Comments
 (0)