use std::path::Path;
use std::process::Command;
use anyhow::{anyhow, Result};
pub fn runs(binary: &Path, report_url: &str) -> Result<String> {
let output = match Command::new(binary).arg("--version").output() {
Ok(output) => output,
Err(err) => {
let mut msg = format!(
"the downloaded binary cannot run on this system:\n {}: {err}",
binary.display()
);
if cfg!(target_os = "linux")
&& err.kind() == std::io::ErrorKind::NotFound
&& binary.exists()
{
msg.push_str(&format!(
"\nThe binary asks for the glibc loader, which this system does not have \
(Alpine or another musl-based distribution). accentup selects the fully static \
musl build on such systems from {} onward; if this is one and you still got \
here, please report it via {report_url}",
crate::release::OLDEST_MUSL
));
}
return Err(anyhow!(msg));
}
};
if output.status.success() {
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let mut msg = String::from("the downloaded binary cannot run on this system:");
for line in stderr.lines().filter(|line| !line.trim().is_empty()) {
msg.push_str("\n ");
msg.push_str(line);
}
if stderr.trim().is_empty() {
msg.push_str(&format!(
"\n {} --version: {}",
binary.display(),
output.status
));
}
if let Some((major, minor)) = highest_glibc_need(&stderr) {
let have = system_glibc().unwrap_or_else(|| "unknown".to_string());
msg.push_str(&format!(
"\nThis release needs glibc {major}.{minor} or newer; this system has glibc {have}.\n\
The supported Linux versions are listed at https://accentcms.dev/download.\n\
If this system is one of them, the release is at fault: please report it via {report_url}"
));
}
Err(anyhow!(msg))
}
pub fn highest_glibc_need(loader_stderr: &str) -> Option<(u32, u32)> {
loader_stderr
.match_indices("GLIBC_")
.filter_map(|(at, _)| {
let rest = &loader_stderr[at + "GLIBC_".len()..];
let version: String = rest
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let (major, minor) = version.split_once('.')?;
let minor = minor.split('.').next()?;
Some((major.parse().ok()?, minor.parse().ok()?))
})
.max()
}
fn system_glibc() -> Option<String> {
#[cfg(target_os = "linux")]
{
let output = Command::new("getconf")
.arg("GNU_LIBC_VERSION")
.output()
.ok()
.filter(|out| out.status.success())?;
let text = String::from_utf8_lossy(&output.stdout);
text.split_whitespace().nth(1).map(str::to_string)
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use crate::install::stub;
#[test]
fn the_highest_glibc_the_loader_names_is_the_floor() {
let stderr = "accent: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by accent)\n\
accent: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by accent)\n\
accent: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.4' not found (required by accent)\n";
assert_eq!(highest_glibc_need(stderr), Some((2, 39)));
assert_eq!(
highest_glibc_need("version `GLIBC_PRIVATE' not found"),
None
);
assert_eq!(highest_glibc_need("Segmentation fault"), None);
assert_eq!(highest_glibc_need(""), None);
}
#[cfg(unix)]
#[test]
fn a_binary_that_runs_reports_its_version_line() {
let dir = tempfile::tempdir().unwrap();
let bin = stub(
dir.path(),
"ok",
"#!/bin/sh\necho 'accent 0.26.0 (abc1234)'\n",
);
assert_eq!(
runs(&bin, "https://example.invalid").unwrap(),
"accent 0.26.0 (abc1234)"
);
}
#[cfg(unix)]
#[test]
fn a_loader_refusal_names_the_glibc_the_release_needs() {
let dir = tempfile::tempdir().unwrap();
let bin = stub(
dir.path(),
"refused",
"#!/bin/sh\n\
echo \"$0: /lib/x86_64-linux-gnu/libc.so.6: version \\`GLIBC_2.38' not found (required by $0)\" >&2\n\
echo \"$0: /lib/x86_64-linux-gnu/libc.so.6: version \\`GLIBC_2.39' not found (required by $0)\" >&2\n\
exit 127\n",
);
let text = runs(&bin, "https://example.invalid/discussions")
.unwrap_err()
.to_string();
assert!(
text.starts_with("the downloaded binary cannot run on this system:"),
"{text}"
);
assert!(text.contains("GLIBC_2.39' not found"), "{text}");
assert!(
text.contains("This release needs glibc 2.39 or newer; this system has glibc "),
"{text}"
);
assert!(text.contains("https://accentcms.dev/download"), "{text}");
assert!(
text.contains("report it via https://example.invalid/discussions"),
"{text}"
);
}
#[cfg(unix)]
#[test]
fn a_failure_without_glibc_in_it_is_quoted_without_a_diagnosis() {
let dir = tempfile::tempdir().unwrap();
let bin = stub(
dir.path(),
"crash",
"#!/bin/sh\necho 'Illegal instruction' >&2\nexit 132\n",
);
let text = runs(&bin, "https://example.invalid")
.unwrap_err()
.to_string();
assert!(text.contains(" Illegal instruction"), "{text}");
assert!(!text.contains("needs glibc"), "{text}");
}
#[cfg(unix)]
#[test]
fn a_file_that_cannot_be_executed_at_all_is_an_error_not_a_panic() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("missing");
let text = runs(&bin, "https://example.invalid")
.unwrap_err()
.to_string();
assert!(
text.starts_with("the downloaded binary cannot run on this system:"),
"{text}"
);
}
}