use std::path::{Path, PathBuf};
use std::process::Command;
const SKIP_VAR: &str = "PAPERBOY_SKIP_DEP_CHECK";
struct Missing {
name: &'static str,
evidence: String,
needed_by: &'static str,
}
fn main() {
println!("cargo::rerun-if-changed=build.rs");
for var in [
SKIP_VAR,
"LIBXML2",
"PKG_CONFIG",
"PKG_CONFIG_PATH",
"PKG_CONFIG_LIBDIR",
"PKG_CONFIG_SYSROOT_DIR",
"LIBCLANG_PATH",
"CC",
"PERL",
"OPENSSL_SRC_PERL",
"MAKE",
] {
println!("cargo::rerun-if-env-changed={var}");
}
if std::env::var_os(SKIP_VAR).is_some() {
return;
}
let certain: Vec<Missing> = [
check_libxml2(),
check_compiler(),
check_perl(),
check_make(),
]
.into_iter()
.flatten()
.collect();
let uncertain: Vec<Missing> = check_libclang().into_iter().collect();
if certain.is_empty() && uncertain.is_empty() {
return;
}
let hint = install_hint();
let fatal = !certain.is_empty() && probe_is_authoritative();
report(&certain, &uncertain, &hint);
if fatal {
panic!("{}", failure_message(&certain, &uncertain, &hint));
}
if !certain.is_empty() {
warn("(Continuing anyway: this check can't be certain on this machine.)");
}
}
fn check_libxml2() -> Option<Missing> {
const NEEDED_BY: &str = "`hurl`, which PaperBoy uses to run requests. Hurl's XPath \
asserts and captures are libxml2, and the `libxml` crate binds the system \
copy rather than vendoring it, so Cargo cannot download it for you.";
if std::env::var_os("LIBXML2").is_some() {
return None;
}
if target_cfg("CARGO_CFG_TARGET_FAMILY").contains("windows")
&& target_cfg("CARGO_CFG_TARGET_ENV") == "msvc"
{
return None;
}
let pkg_config = std::env::var("PKG_CONFIG").unwrap_or_else(|_| "pkg-config".to_string());
match Command::new(&pkg_config)
.args(["--exists", "libxml-2.0"])
.output()
{
Ok(out) if out.status.success() => None,
Ok(_) => Some(Missing {
name: "libxml2",
evidence: format!("`{pkg_config} --exists libxml-2.0` says it isn't installed"),
needed_by: NEEDED_BY,
}),
Err(_) => Some(Missing {
name: "libxml2",
evidence: format!("`{pkg_config}`, which finds it, is not on PATH"),
needed_by: NEEDED_BY,
}),
}
}
fn check_compiler() -> Option<Missing> {
if std::env::var_os("CC").is_some() {
return None;
}
if ["cc", "gcc", "clang"].iter().any(|bin| has(bin)) {
return None;
}
Some(Missing {
name: "a C compiler",
evidence: "none of `cc`, `gcc` or `clang` is on PATH, and CC is unset".to_string(),
needed_by: "the vendored libcurl, OpenSSL and zlib builds. PaperBoy enables \
curl-sys's `static-curl`/`static-ssl` so those are compiled from source \
here, which is what spares you needing them as system packages.",
})
}
fn check_perl() -> Option<Missing> {
if std::env::var_os("OPENSSL_SRC_PERL").is_some() || std::env::var_os("PERL").is_some() {
return None;
}
if has("perl") {
return None;
}
Some(Missing {
name: "perl",
evidence: "`perl` is not on PATH, and neither PERL nor OPENSSL_SRC_PERL is set".to_string(),
needed_by: "the vendored OpenSSL build: `openssl-src` configures OpenSSL by \
running `perl Configure`.",
})
}
fn check_make() -> Option<Missing> {
if std::env::var_os("MAKE").is_some() {
return None;
}
if has("make") || has("gmake") {
return None;
}
Some(Missing {
name: "make",
evidence: "neither `make` nor `gmake` is on PATH".to_string(),
needed_by: "the vendored OpenSSL build, which drives OpenSSL's own makefile.",
})
}
fn check_libclang() -> Option<Missing> {
if std::env::var_os("LIBCLANG_PATH").is_some() {
return None;
}
let mut dirs: Vec<PathBuf> = vec![
PathBuf::from("/usr/lib"),
PathBuf::from("/usr/lib64"),
PathBuf::from("/usr/local/lib"),
PathBuf::from("/usr/lib/x86_64-linux-gnu"),
PathBuf::from("/usr/lib/aarch64-linux-gnu"),
PathBuf::from("/Library/Developer/CommandLineTools/usr/lib"),
PathBuf::from(
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib",
),
PathBuf::from("/opt/homebrew/opt/llvm/lib"),
PathBuf::from("/usr/local/opt/llvm/lib"),
];
if let Ok(entries) = std::fs::read_dir("/usr/lib") {
for entry in entries.flatten() {
if entry.file_name().to_string_lossy().starts_with("llvm") {
dirs.push(entry.path().join("lib"));
}
}
}
if let Ok(out) = Command::new("llvm-config").arg("--libdir").output() {
if out.status.success() {
let dir = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !dir.is_empty() {
dirs.push(PathBuf::from(dir));
}
}
}
if dirs.iter().any(|dir| contains_libclang(dir)) {
return None;
}
Some(Missing {
name: "libclang",
evidence: "no libclang shared library found in the usual places".to_string(),
needed_by: "`bindgen`, which generates `libxml`'s bindings and loads libclang \
while building. Without it the build fails with \"Unable to find libclang\".",
})
}
fn contains_libclang(dir: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(dir) else {
return false;
};
entries.flatten().any(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
name.starts_with("libclang") && (name.contains(".so") || name.contains(".dylib"))
})
}
fn probe_is_authoritative() -> bool {
let host = std::env::var("HOST").unwrap_or_default();
let target = std::env::var("TARGET").unwrap_or_default();
if host.is_empty() || target.is_empty() || host != target {
return false;
}
for base in [
"PKG_CONFIG",
"PKG_CONFIG_PATH",
"PKG_CONFIG_LIBDIR",
"PKG_CONFIG_SYSROOT_DIR",
] {
let candidates = [
format!("{base}_{target}"),
format!("{base}_{}", target.replace('-', "_")),
format!("HOST_{base}"),
];
if candidates.iter().any(|var| std::env::var_os(var).is_some()) {
return false;
}
}
true
}
fn target_cfg(var: &str) -> String {
std::env::var(var).unwrap_or_default()
}
fn report(certain: &[Missing], uncertain: &[Missing], hint: &[String]) {
warn("──────────────────────────────────────────────────────────────");
warn("PaperBoy needs some build dependencies that aren't installed:");
for item in certain {
warn(&format!(" · {} — {}", item.name, item.evidence));
}
for item in uncertain {
warn(&format!(" · {} — {} (unsure)", item.name, item.evidence));
}
warn(" Install them with:");
for line in hint {
warn(&format!(" {line}"));
}
warn("──────────────────────────────────────────────────────────────");
}
fn failure_message(certain: &[Missing], uncertain: &[Missing], hint: &[String]) -> String {
let subject = if certain.len() == 1 {
"a required build dependency is missing"
} else {
"required build dependencies are missing"
};
let mut message = format!("PaperBoy can't be built here: {subject}.\n\n");
for item in certain {
message.push_str(&format!(
" {} is a required build dependency, and it isn't installed.\n",
item.name
));
message.push_str(&format!(" How we know: {}.\n", item.evidence));
message.push_str(&wrapped(
&format!("Required by {}", item.needed_by),
" ",
));
message.push('\n');
}
for item in uncertain {
message.push_str(&format!(
" {} may also be missing — this check is a guess, so it isn't the\n \
reason the build stopped.\n",
item.name
));
message.push_str(&format!(" How we know: {}.\n", item.evidence));
message.push_str(&wrapped(
&format!("Required by {}", item.needed_by),
" ",
));
message.push('\n');
}
message.push_str(" Install what's missing with:\n");
for line in hint {
message.push_str(&format!(" {line}\n"));
}
message.push_str(concat!(
"\n",
" PaperBoy stops here on purpose. Left alone, the build fails later\n",
" anyway, inside a crate you never asked for and only after several\n",
" more minutes of compiling.\n",
"\n",
" The README's \"Build prerequisites\" section covers every platform.\n",
" If this check is wrong about your machine, set\n",
));
message.push_str(&format!(" {SKIP_VAR}=1 to bypass it.\n"));
message
}
fn wrapped(text: &str, indent: &str) -> String {
const WIDTH: usize = 66;
let mut out = String::new();
let mut line = String::new();
for word in text.split_whitespace() {
if !line.is_empty() && line.chars().count() + 1 + word.chars().count() > WIDTH {
out.push_str(&format!("{indent}{line}\n"));
line.clear();
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
}
if !line.is_empty() {
out.push_str(&format!("{indent}{line}\n"));
}
out
}
fn warn(line: &str) {
println!("cargo::warning={line}");
}
fn install_hint() -> Vec<String> {
if target_cfg("CARGO_CFG_TARGET_OS") == "macos" {
let mut hint = vec![
"xcode-select --install # C compiler, libclang, libxml2, perl, make".to_string(),
];
if has("brew") {
hint.push("brew install pkg-config".to_string());
} else if has("port") {
hint.push("sudo port install pkgconfig".to_string());
} else {
hint.push(
"install Homebrew (https://brew.sh), then: brew install pkg-config".to_string(),
);
}
return hint;
}
let candidates: &[(&str, &str)] = &[
(
"apt-get",
"sudo apt install build-essential pkg-config libxml2-dev libclang-dev perl",
),
(
"dnf",
"sudo dnf install pkgconf-pkg-config gcc make perl libxml2-devel clang-devel",
),
(
"yum",
"sudo yum install pkg-config gcc make perl libxml2-devel clang-devel",
),
(
"zypper",
"sudo zypper install pkg-config gcc make perl libxml2-devel clang-devel",
),
(
"pacman",
"sudo pacman -S pkgconf base-devel perl libxml2 clang",
),
(
"apk",
"sudo apk add build-base pkgconfig perl libxml2-dev clang-dev",
),
];
for (bin, command) in candidates {
if has(bin) {
return vec![(*command).to_string()];
}
}
vec![
"install your distribution's pkg-config, libxml2, clang, C compiler, perl and make packages"
.to_string(),
]
}
fn has(bin: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
let candidate: PathBuf = dir.join(bin);
candidate.is_file()
})
}