Skip to content

Commit 9d28092

Browse files
committed
feat: improve concurrency safety, cross-platform support and dynamic versioning
This change introduces: - File locking for shared state files using fs4 - Dynamic versioning with commit ID and build time via build.rs - Cross-platform support for macOS and ARM architectures - Session-specific environment updates using PVM_SHELL_PID - Safety guards and silent removal of temp files in shell wrappers - Centralized constants and semantic version sorting logic - Bug fixes in interactive menu and shell file discovery
1 parent 544d4d8 commit 9d28092

15 files changed

Lines changed: 442 additions & 482 deletions

File tree

Cargo.lock

Lines changed: 96 additions & 350 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,21 @@ dialoguer = "0.12.0"
1818
dirs = "6.0.0"
1919
env_logger = "0.11.9"
2020
flate2 = "1.1.9"
21+
fs4 = "0.13.1"
22+
futures-util = "0.3.32"
2123
indicatif = "0.18.4"
22-
log = "0.4.29"
23-
regex = "1.12.3"
24-
reqwest = { version = "0.13.2", features = ["stream", "rustls"] }
25-
scraper = "0.25.0"
24+
reqwest = { version = "0.13.2", features = ["stream", "rustls", "json"] }
25+
semver = "1.0.27"
2626
serde = { version = "1.0.228", features = ["derive"] }
2727
serde_json = "1.0.149"
2828
tar = "0.4.44"
29-
thiserror = "2.0.18"
30-
tokio = { version = "1.49.0", features = ["full"] }
29+
tokio = { version = "1.50.0", features = ["full"] }
3130

3231
[dev-dependencies]
3332
assert_cmd = "2.1.2"
3433
mockito = "1.7.2"
3534
predicates = "3.1.4"
36-
tempfile = "3.25.0"
35+
tempfile = "3.26.0"
3736

3837
# Optimize for binary size and performance
3938
[profile.release]

build.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::process::Command;
2+
3+
fn main() {
4+
let commit_hash = Command::new("git")
5+
.args(&["rev-parse", "--short", "HEAD"])
6+
.output()
7+
.ok()
8+
.and_then(|output| String::from_utf8(output.stdout).ok())
9+
.map(|s| s.trim().to_string())
10+
.unwrap_or_else(|| "unknown".to_string());
11+
12+
let build_time = Command::new("date")
13+
.args(&["-u", "+%Y-%m-%dT%H:%M:%SZ"])
14+
.output()
15+
.ok()
16+
.and_then(|output| String::from_utf8(output.stdout).ok())
17+
.map(|s| s.trim().to_string())
18+
.unwrap_or_else(|| "unknown".to_string());
19+
20+
let is_ci = std::env::var("GITHUB_ACTIONS").is_ok();
21+
22+
let version = if is_ci {
23+
let tag = Command::new("git")
24+
.args(&["describe", "--tags", "--always"])
25+
.output()
26+
.ok()
27+
.and_then(|output| String::from_utf8(output.stdout).ok())
28+
.map(|s| s.trim().to_string())
29+
.unwrap_or_else(|| "unknown".to_string());
30+
format!("{} (built at: {})", tag, build_time)
31+
} else {
32+
let pkg_version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
33+
format!("{} (commit: {}, built at: {})", pkg_version, commit_hash, build_time)
34+
};
35+
36+
println!("cargo:rustc-env=PVM_VERSION={}", version);
37+
}

src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clap::{Parser, Subcommand};
55
#[derive(Parser)]
66
#[command(
77
name = "pvm",
8-
version = env!("CARGO_PKG_VERSION"),
8+
version = env!("PVM_VERSION"),
99
author,
1010
about = "Fast and simple PHP version manager",
1111
disable_version_flag = true,

src/commands/env.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::constants::PVM_DIR_VAR;
12
use crate::shell;
23
use anyhow::Result;
34
use clap::Parser;
@@ -18,13 +19,15 @@ pub struct Env {
1819

1920
impl Env {
2021
pub async fn call(self) -> Result<()> {
22+
let pvm_dir = crate::fs::get_pvm_dir()?;
2123
let s: Box<dyn shell::Shell> = match self.shell.as_deref() {
2224
Some("bash") => Box::new(shell::Bash),
2325
Some("zsh") => Box::new(shell::Zsh),
2426
Some("fish") => Box::new(shell::Fish),
2527
_ => shell::detect_shell(),
2628
};
2729

30+
println!("{}", s.set_env_var(PVM_DIR_VAR, &pvm_dir.to_string_lossy()));
2831
println!("{}", s.wrapper_fn());
2932
println!("{}", s.use_on_cd());
3033

src/commands/install.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
use crate::constants::MULTISHELL_PATH_VAR;
12
use crate::{fs, network};
23
use anyhow::Result;
34
use clap::Parser;
45
use colored::Colorize;
6+
use fs4::fs_std::FileExt;
7+
use std::io::Write;
58

69
/// Install a specific PHP version
710
#[derive(Parser, Debug)]
@@ -72,23 +75,33 @@ pub async fn execute_install(version: &str) -> Result<()> {
7275
if let Ok(bin_dir) = crate::fs::get_version_bin_dir(&v) {
7376
let s = crate::shell::detect_shell();
7477
let export_str1 =
75-
s.set_env_var("PVM_MULTISHELL_PATH", &bin_dir.to_string_lossy());
78+
s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy());
7679
let export_str2 = s.path(&bin_dir);
7780

78-
if let Ok(pvm_dir) = crate::fs::get_pvm_dir() {
79-
let env_file = pvm_dir.join(".env_update");
80-
std::fs::write(
81-
&env_file,
82-
format!("{}\n{}", export_str1, export_str2),
83-
)
84-
.ok();
81+
if let Ok(env_file) = crate::fs::get_env_update_path() {
82+
// Atomic write with advisory lock
83+
if let Ok(file) = std::fs::OpenOptions::new()
84+
.create(true)
85+
.write(true)
86+
.truncate(true)
87+
.open(&env_file)
88+
{
89+
file.lock_exclusive().ok();
90+
let mut writer = std::io::BufWriter::new(&file);
91+
writeln!(writer, "{}\n{}", export_str1, export_str2).ok();
92+
writer.flush().ok();
93+
file.unlock().ok();
94+
}
8595
}
8696

8797
unsafe {
88-
std::env::set_var("PVM_MULTISHELL_PATH", &bin_dir);
98+
std::env::set_var(MULTISHELL_PATH_VAR, &bin_dir);
8999
if let Some(path) = std::env::var_os("PATH") {
90100
let mut new_path = std::ffi::OsString::new();
91101
new_path.push(&bin_dir);
102+
#[cfg(windows)]
103+
new_path.push(";");
104+
#[cfg(not(windows))]
92105
new_path.push(":");
93106
new_path.push(&path);
94107
std::env::set_var("PATH", new_path);

src/commands/use_cmd.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use crate::constants::{MULTISHELL_PATH_VAR, PHP_VERSION_FILE};
12
use crate::{fs, shell, update};
23
use anyhow::Result;
34
use clap::Parser;
45
use colored::Colorize;
5-
use dialoguer::{Confirm, Select, theme::ColorfulTheme};
6+
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
7+
use fs4::fs_std::FileExt;
8+
use std::io::Write;
69
use std::path::Path;
710

811
/// Change PHP version
@@ -71,12 +74,13 @@ impl Use {
7174
}
7275

7376
// Smart prompt logic
74-
if Path::new(".php-version").exists()
75-
&& let Ok(current_file_ver) = std::fs::read_to_string(".php-version")
77+
if Path::new(PHP_VERSION_FILE).exists()
78+
&& let Ok(current_file_ver) = std::fs::read_to_string(PHP_VERSION_FILE)
7679
&& current_file_ver.trim() != version
7780
{
7881
let prompt = format!(
79-
"A .php-version file is present ({}). Do you want to apply this change to the directory?",
82+
"A {} file is present ({}). Do you want to apply this change to the directory?",
83+
PHP_VERSION_FILE,
8084
current_file_ver.trim().yellow()
8185
);
8286
if Confirm::with_theme(&ColorfulTheme::default())
@@ -85,28 +89,42 @@ impl Use {
8589
.interact_opt()?
8690
.unwrap_or(false)
8791
{
88-
std::fs::write(".php-version", &version).ok();
89-
eprintln!("{} Updated .php-version to {}", "✓".green(), version.bold());
92+
std::fs::write(PHP_VERSION_FILE, &version).ok();
93+
eprintln!("{} Updated {} to {}", "✓".green(), PHP_VERSION_FILE, version.bold());
9094
}
9195
}
9296

9397
let bin_dir = fs::get_version_bin_dir(&version)?;
9498
let s = shell::detect_shell();
9599

96100
// These evaluate in the user's shell hook via wrapper
97-
let export_str1 = s.set_env_var("PVM_MULTISHELL_PATH", &bin_dir.to_string_lossy());
101+
let export_str1 = s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy());
98102
let export_str2 = s.path(&bin_dir);
99103

100-
let pvm_dir = fs::get_pvm_dir()?;
101-
let env_file = pvm_dir.join(".env_update");
102-
std::fs::write(&env_file, format!("{}\n{}", export_str1, export_str2)).ok();
104+
let env_file = fs::get_env_update_path()?;
105+
106+
// Atomic write with advisory lock
107+
let file = std::fs::OpenOptions::new()
108+
.create(true)
109+
.write(true)
110+
.truncate(true)
111+
.open(&env_file)?;
112+
file.lock_exclusive()?;
113+
let mut writer = std::io::BufWriter::new(&file);
114+
writeln!(writer, "{}", export_str1)?;
115+
writeln!(writer, "{}", export_str2)?;
116+
writer.flush()?;
117+
file.unlock()?;
103118

104119
// Also update the current Rust binary's environment so spawned subs (or interactive loop) see it
105120
unsafe {
106-
std::env::set_var("PVM_MULTISHELL_PATH", &bin_dir);
121+
std::env::set_var(MULTISHELL_PATH_VAR, &bin_dir);
107122
if let Some(path) = std::env::var_os("PATH") {
108123
let mut new_path = std::ffi::OsString::new();
109124
new_path.push(&bin_dir);
125+
#[cfg(windows)]
126+
new_path.push(";");
127+
#[cfg(not(windows))]
110128
new_path.push(":");
111129
new_path.push(&path);
112130
std::env::set_var("PATH", new_path);

src/constants.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// The name of the environment variable used to store the path to the PVM installation.
2+
pub const PVM_DIR_VAR: &str = "PVM_DIR";
3+
4+
/// The name of the environment variable used to store the path to the currently active PHP version's bin directory.
5+
pub const MULTISHELL_PATH_VAR: &str = "PVM_MULTISHELL_PATH";
6+
7+
/// The name of the file used to store the environment variables to be updated in the shell.
8+
pub const ENV_UPDATE_FILE: &str = ".env_update";
9+
10+
/// The name of the file used to store the remote versions cache.
11+
pub const REMOTE_CACHE_FILE: &str = "remote_cache.json";
12+
13+
/// The name of the file used as a guard for the update check.
14+
pub const UPDATE_CHECK_GUARD_FILE: &str = ".update_check_guard";
15+
16+
/// The name of the file used to store the PHP version for a directory.
17+
pub const PHP_VERSION_FILE: &str = ".php-version";
18+
19+
/// The base URL for fetching available PHP versions.
20+
pub const BASE_URL: &str = "https://dl.static-php.dev/static-php-cli/common/";

src/fs.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::constants::{MULTISHELL_PATH_VAR, PVM_DIR_VAR};
12
use anyhow::{Context, Result};
23
use std::path::PathBuf;
34

@@ -8,7 +9,7 @@ pub struct VersionItem {
89
}
910

1011
pub fn get_pvm_dir() -> Result<PathBuf> {
11-
if let Ok(pvm_dir) = std::env::var("PVM_DIR") {
12+
if let Ok(pvm_dir) = std::env::var(PVM_DIR_VAR) {
1213
return Ok(PathBuf::from(pvm_dir));
1314
}
1415
let home = dirs::data_local_dir().context("Could not find local data directory")?;
@@ -44,16 +45,12 @@ pub fn list_installed_versions() -> Result<Vec<String>> {
4445
}
4546
}
4647

47-
versions.sort_by(|a, b| {
48-
let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
49-
let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
50-
a_parts.cmp(&b_parts)
51-
});
48+
crate::utils::sort_versions(&mut versions);
5249
Ok(versions)
5350
}
5451

5552
pub fn get_current_version() -> String {
56-
if let Ok(path) = std::env::var("PVM_MULTISHELL_PATH") {
53+
if let Ok(path) = std::env::var(MULTISHELL_PATH_VAR) {
5754
let p = PathBuf::from(path);
5855
if let Some(parent) = p.parent()
5956
&& let Some(name) = parent.file_name()
@@ -64,6 +61,17 @@ pub fn get_current_version() -> String {
6461
"system".to_string()
6562
}
6663

64+
pub fn get_env_update_path() -> Result<PathBuf> {
65+
let pvm_dir = get_pvm_dir()?;
66+
let shell_pid = std::env::var("PVM_SHELL_PID").unwrap_or_default();
67+
let filename = if shell_pid.is_empty() {
68+
crate::constants::ENV_UPDATE_FILE.to_string()
69+
} else {
70+
format!("{}_{}", crate::constants::ENV_UPDATE_FILE, shell_pid)
71+
};
72+
Ok(pvm_dir.join(filename))
73+
}
74+
6775
pub fn get_aliased_versions() -> Result<Vec<VersionItem>> {
6876
let mut installed = list_installed_versions()?;
6977
if installed.is_empty() {

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod cli;
22
mod commands;
3+
mod constants;
34
mod fs;
45
mod interactive;
56
mod network;
67
mod shell;
78
mod update;
9+
mod utils;
810

911
use anyhow::Result;
1012
use clap::Parser;

0 commit comments

Comments
 (0)