use std::{
fmt,
path::{Path, PathBuf},
sync::LazyLock,
};
use serde::Deserialize;
const BUILD_CHANNEL: Option<&str> = option_env!("S2_BUILD_CHANNEL");
const GIT_COMMIT: &str = env!("S2_GIT_COMMIT");
pub(super) const RECEIPT_FILE: &str = "s2-receipt.json";
pub(super) const INSTALL_SCRIPT_CHANNEL: &str = "install-script";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallChannel {
InstallScript,
Homebrew,
Cargo,
Docker,
GithubRelease,
SourceBuild,
}
impl fmt::Display for InstallChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InstallScript => "install script",
Self::Homebrew => "homebrew",
Self::Cargo => "cargo",
Self::Docker => "docker",
Self::GithubRelease => "github release",
Self::SourceBuild => "source build",
})
}
}
impl InstallChannel {
const fn id(self) -> &'static str {
match self {
Self::InstallScript => "install-script",
Self::Homebrew => "homebrew",
Self::Cargo => "cargo",
Self::Docker => "docker",
Self::GithubRelease => "github-release",
Self::SourceBuild => "source-build",
}
}
}
pub fn detect() -> InstallChannel {
static CHANNEL: LazyLock<InstallChannel> = LazyLock::new(|| {
let exe = std::env::current_exe()
.ok()
.and_then(|p| std::fs::canonicalize(p).ok());
resolve(exe.as_deref(), BUILD_CHANNEL)
});
*CHANNEL
}
pub fn long_version() -> &'static str {
static VERSION: LazyLock<String> =
LazyLock::new(|| format_version(env!("CARGO_PKG_VERSION"), GIT_COMMIT, detect()));
&VERSION
}
pub fn user_agent() -> &'static str {
static USER_AGENT: LazyLock<String> =
LazyLock::new(|| format_user_agent(env!("CARGO_PKG_VERSION"), detect()));
&USER_AGENT
}
fn format_version(version: &str, revision: &str, channel: InstallChannel) -> String {
format!("{version} (rev {revision}, via {channel})")
}
fn format_user_agent(version: &str, channel: InstallChannel) -> String {
format!("s2-cli/{version} (channel={})", channel.id())
}
fn resolve(exe: Option<&Path>, stamp: Option<&str>) -> InstallChannel {
if let Some(exe) = exe
&& receipt_matches(exe)
{
return InstallChannel::InstallScript;
}
if stamp == Some("docker") {
return InstallChannel::Docker;
}
if let Some(exe) = exe {
if is_homebrew(exe, std::env::var_os("HOMEBREW_CELLAR").map(PathBuf::from)) {
return InstallChannel::Homebrew;
}
if is_cargo(exe, cargo_bin_dirs()) {
return InstallChannel::Cargo;
}
}
if stamp == Some("release") {
return InstallChannel::GithubRelease;
}
InstallChannel::SourceBuild
}
#[derive(Deserialize)]
struct Receipt {
channel: String,
binary_path: PathBuf,
}
fn receipt_matches(exe: &Path) -> bool {
let Some(dir) = exe.parent() else {
return false;
};
let Ok(contents) = std::fs::read_to_string(dir.join(RECEIPT_FILE)) else {
return false;
};
let Ok(receipt) = serde_json::from_str::<Receipt>(&contents) else {
return false;
};
receipt.channel == INSTALL_SCRIPT_CHANNEL
&& std::fs::canonicalize(&receipt.binary_path).is_ok_and(|p| p == exe)
}
fn is_homebrew(exe: &Path, cellar: Option<PathBuf>) -> bool {
if cellar.is_some_and(|c| exe.starts_with(c)) {
return true;
}
let components: Vec<_> = exe.components().map(|c| c.as_os_str()).collect();
components
.iter()
.position(|c| *c == "Cellar")
.is_some_and(|cellar| components.len() >= cellar + 4)
}
fn cargo_bin_dirs() -> Vec<PathBuf> {
[
std::env::var_os("CARGO_INSTALL_ROOT").map(PathBuf::from),
std::env::var_os("CARGO_HOME").map(PathBuf::from),
dirs::home_dir().map(|home| home.join(".cargo")),
]
.into_iter()
.flatten()
.map(|root| root.join("bin"))
.collect()
}
fn is_cargo(exe: &Path, bin_dirs: Vec<PathBuf>) -> bool {
let Some(parent) = exe.parent() else {
return false;
};
bin_dirs.into_iter().any(|dir| {
parent == dir || std::fs::canonicalize(&dir).is_ok_and(|resolved| parent == resolved)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_includes_full_revision_and_channel() {
assert_eq!(
format_version(
"0.41.0",
"0123456789abcdef0123456789abcdef01234567",
InstallChannel::Homebrew,
),
"0.41.0 (rev 0123456789abcdef0123456789abcdef01234567, via homebrew)"
);
}
#[test]
fn stamp_resolution_without_receipt_or_known_paths() {
let exe = Path::new("/weird/place/s2");
assert_eq!(resolve(Some(exe), Some("docker")), InstallChannel::Docker);
assert_eq!(
resolve(Some(exe), Some("brew")),
InstallChannel::SourceBuild,
"Homebrew is detected from its resolved Cellar path, not a build stamp"
);
assert_eq!(
resolve(Some(exe), Some("release")),
InstallChannel::GithubRelease
);
assert_eq!(resolve(Some(exe), None), InstallChannel::SourceBuild);
assert_eq!(resolve(None, None), InstallChannel::SourceBuild);
}
#[test]
fn user_agent_includes_machine_readable_channel() {
for (channel, id) in [
(InstallChannel::InstallScript, "install-script"),
(InstallChannel::Homebrew, "homebrew"),
(InstallChannel::Cargo, "cargo"),
(InstallChannel::Docker, "docker"),
(InstallChannel::GithubRelease, "github-release"),
(InstallChannel::SourceBuild, "source-build"),
] {
assert_eq!(
format_user_agent("0.41.0", channel),
format!("s2-cli/0.41.0 (channel={id})")
);
}
}
#[test]
fn cellar_path_wins_over_release_stamp() {
let exe = Path::new("/opt/homebrew/Cellar/s2/0.40.1/bin/s2");
assert_eq!(
resolve(Some(exe), Some("release")),
InstallChannel::Homebrew
);
}
#[test]
fn homebrew_detection() {
assert!(is_homebrew(
Path::new("/opt/homebrew/Cellar/s2/0.40.1/bin/s2"),
None
));
assert!(is_homebrew(
Path::new("/home/linuxbrew/.linuxbrew/Cellar/s2/0.40.1/bin/s2"),
None
));
assert!(is_homebrew(
Path::new("/custom/kegs/s2/0.40.1/bin/s2"),
Some(PathBuf::from("/custom/kegs"))
));
assert!(!is_homebrew(Path::new("/Users/me/.s2/bin/s2"), None));
assert!(!is_homebrew(Path::new("/usr/local/bin/s2"), None));
assert!(!is_homebrew(Path::new("/Users/me/Cellar/s2"), None));
assert!(!is_homebrew(Path::new("/home/me/Cellar/projects/s2"), None));
}
#[test]
fn cargo_detection() {
let dirs = vec![PathBuf::from("/Users/me/.cargo/bin")];
assert!(is_cargo(Path::new("/Users/me/.cargo/bin/s2"), dirs.clone()));
assert!(!is_cargo(Path::new("/Users/me/.s2/bin/s2"), dirs.clone()));
assert!(!is_cargo(Path::new("/Users/me/.cargo/bin/sub/s2"), dirs));
}
#[test]
fn receipt_must_point_at_the_running_executable() {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("s2");
std::fs::write(&exe, "").unwrap();
let exe = std::fs::canonicalize(&exe).unwrap();
assert!(!receipt_matches(&exe));
std::fs::write(
dir.path().join(RECEIPT_FILE),
format!(
r#"{{"channel": "install-script", "version": "0.40.1",
"binary_path": {:?}, "installed_at": "2026-07-23T00:00:00Z"}}"#,
exe
),
)
.unwrap();
assert!(receipt_matches(&exe));
assert_eq!(
resolve(Some(&exe), Some("release")),
InstallChannel::InstallScript,
"receipt takes precedence over the stamp"
);
std::fs::write(
dir.path().join(RECEIPT_FILE),
r#"{"channel": "install-script", "binary_path": "/somewhere/else/s2"}"#,
)
.unwrap();
assert!(!receipt_matches(&exe));
std::fs::write(
dir.path().join(RECEIPT_FILE),
format!(r#"{{"channel": "mystery", "binary_path": {exe:?}}}"#),
)
.unwrap();
assert!(!receipt_matches(&exe));
}
}