Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
208 lines (183 loc) · 8.12 KB
/
Copy pathbuild.rs
File metadata and controls
208 lines (183 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! Build script for bext-php: compiles the C SAPI bridge and links libphp.
//!
//! Requires PHP installed with `--enable-embed --enable-zts`:
//! apt install libphp-embed php-dev (Debian/Ubuntu)
//! dnf install php-embedded php-devel (Fedora/RHEL)
//! brew install php (macOS — ZTS may need source build)
//!
//! Set BEXT_PHP_CONFIG to override the `php-config` binary path.
use std::env;
use std::process::Command;
fn main() {
// Locate php-config
let php_config = env::var("BEXT_PHP_CONFIG").unwrap_or_else(|_| "php-config".into());
// Get PHP compile flags
let includes = php_config_flag(&php_config, "--includes");
let ldflags = php_config_flag(&php_config, "--ldflags");
let _libs = php_config_flag(&php_config, "--libs");
let prefix = php_config_flag(&php_config, "--prefix");
let extension_dir = php_config_flag(&php_config, "--extension-dir");
// Detect ZTS (Thread Safety) support.
//
// We check php-config --configure-options AND whether the TSRM header
// defines ZTS. NTS (non-thread-safe) PHP works for single-worker mode;
// ZTS is required for multi-threaded worker pools.
let configure_options = php_config_flag(&php_config, "--configure-options");
let has_zts = configure_options.contains("enable-zts")
|| configure_options.contains("zts")
|| php_has_zts_define(&includes);
// Register php_zts as a known cfg so Rust doesn't warn about it
println!("cargo:rustc-check-cfg=cfg(php_zts)");
if has_zts {
println!("cargo:warning=PHP ZTS (thread safety) detected — multi-threaded pool enabled");
println!("cargo:rustc-cfg=php_zts");
} else {
println!(
"cargo:warning=PHP NTS detected. bext-php will run with a single worker. \
For multi-threaded PHP, rebuild PHP with --enable-zts."
);
}
// Compile our C SAPI bridge
let mut build = cc::Build::new();
build.file("sapi/bext_php_sapi.c").warnings(true);
// Parse include flags from php-config
for flag in includes.split_whitespace() {
if let Some(path) = flag.strip_prefix("-I") {
build.include(path);
}
}
// ZTS: define ZTS but NOT ZEND_ENABLE_STATIC_TSRMLS_CACHE.
// lld (Rust's linker) doesn't do TLS symbol interposition, so we
// can't have our own _tsrm_ls_cache alongside libphp.so's.
// Instead, EG()/SG()/PG()/CG() use tsrm_get_ls_cache() calls.
if has_zts {
build.define("ZTS", None);
}
// pthreads
build.flag("-pthread");
build.compile("bext_php_sapi");
// Link against libphp (the embed SAPI shared library).
//
// We link libphp *dynamically*. Its transitive dependencies (libxml2,
// libsodium, libargon2, etc.) are resolved by the dynamic linker at
// runtime — we do NOT need to link them explicitly. php-config --libs
// reports them for static linking scenarios only.
//
// This means the build machine only needs:
// - libphp-embed (provides libphp.so)
// - php-dev (provides headers + php-config)
// NOT the -dev packages for every PHP dependency.
let lib_dir = format!("{}/lib", prefix);
println!("cargo:rustc-link-search=native={}", lib_dir);
// Also check extension dir parent and common system lib paths
if let Some(parent) = std::path::Path::new(&extension_dir).parent() {
println!("cargo:rustc-link-search=native={}", parent.display());
}
// libphp.so is often in /usr/lib directly
println!("cargo:rustc-link-search=native=/usr/lib");
// Parse ldflags for any -L paths (but skip -l flags — we handle linking below)
for flag in ldflags.split_whitespace() {
if let Some(path) = flag.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={}", path);
}
}
// Link libphp.
//
// Strategy: prefer static linking (libphp.a) for portability. Fall back
// to dynamic (libphp.so) if no static lib is available. When dynamic,
// embed an RPATH so the binary finds libphp.so at runtime without
// LD_LIBRARY_PATH.
let static_lib = std::path::Path::new(&lib_dir).join("libphp.a");
if static_lib.exists() {
println!("cargo:warning=Linking libphp statically for portability");
println!("cargo:rustc-link-lib=static=php");
// Static PHP needs its transitive deps linked explicitly
for flag in _libs.split_whitespace() {
if let Some(lib) = flag.strip_prefix("-l") {
println!("cargo:rustc-link-lib=dylib={}", lib);
}
}
} else {
println!("cargo:rustc-link-lib=dylib=php");
// Embed RPATH so the final binary finds libphp.so at runtime.
// This makes the binary portable — no LD_LIBRARY_PATH needed.
// We write the lib_dir to a DEP_ env var that bext-server's build.rs
// can read. We also set it directly (works for cdylib outputs).
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir);
println!("cargo:php_lib_dir={}", lib_dir);
}
// Only link the truly essential system libs that our C code directly uses.
// PHP's own transitive deps (xml2, sodium, argon2, etc.) are resolved
// by the dynamic linker via libphp.so's DT_NEEDED entries.
println!("cargo:rustc-link-lib=dylib=pthread");
#[cfg(target_os = "linux")]
{
println!("cargo:rustc-link-lib=dylib=dl");
println!("cargo:rustc-link-lib=dylib=resolv");
}
// Re-run if C source changes
println!("cargo:rerun-if-changed=sapi/bext_php_sapi.c");
println!("cargo:rerun-if-changed=sapi/bext_php_sapi.h");
println!("cargo:rerun-if-env-changed=BEXT_PHP_CONFIG");
}
/// Check whether the PHP headers define ZTS by scanning for it in TSRM.h or main/php_config.h.
fn php_has_zts_define(includes: &str) -> bool {
for flag in includes.split_whitespace() {
if let Some(path) = flag.strip_prefix("-I") {
// Check main/php_config.h for #define ZTS 1
let config_h = std::path::Path::new(path).join("main/php_config.h");
if let Ok(content) = std::fs::read_to_string(&config_h) {
if content.contains("#define ZTS 1") || content.contains("#define ZTS") {
return true;
}
}
}
}
false
}
/// Run php-config with the given flag and return trimmed stdout.
///
/// Security: validates that `php_config` is a real file path (not a shell
/// command string) before executing. This prevents command injection via
/// the `BEXT_PHP_CONFIG` environment variable in a compromised CI.
fn php_config_flag(php_config: &str, flag: &str) -> String {
// Reject values containing shell metacharacters or whitespace that would
// indicate a command string rather than a binary path (e.g. "sh -c '...'").
let forbidden = [' ', '\t', ';', '|', '&', '`', '$', '(', ')', '{', '}', '<', '>', '\n', '\r', '\'', '"'];
if php_config.contains(forbidden.as_slice()) {
panic!(
"BEXT_PHP_CONFIG contains shell metacharacters: {:?}. \
It must be a path to the php-config binary (e.g. /usr/bin/php-config).",
php_config
);
}
// Verify the path exists and is a file (not a directory).
let path = std::path::Path::new(php_config);
if path.is_absolute() && !path.is_file() {
panic!(
"BEXT_PHP_CONFIG points to {:?} which is not a file. \
Set it to the path of the php-config binary.",
php_config
);
}
match Command::new(php_config).arg(flag).output() {
Ok(output) if output.status.success() => {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!(
"php-config {} failed (exit {}): {}",
flag, output.status, stderr
);
}
Err(e) => {
panic!(
"Failed to run `{} {}`: {}. \
Is PHP (embed SAPI) installed? \
Set BEXT_PHP_CONFIG to override the php-config path.",
php_config, flag, e
);
}
}
}