1use crate::error::{Error, Result};
9use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
10use std::path::Path;
11
12pub fn proxy_content(vendor_dir: &Path, link: &Path, bin: &Path) -> Result<String> {
18 let mut head = [0u8; 500];
19 let n = {
20 use std::io::Read as _;
21 let mut f = std::fs::File::open(bin).map_err(Error::io(bin))?;
22 f.read(&mut head).map_err(Error::io(bin))?
23 };
24 let head = String::from_utf8_lossy(&head[..n]);
25
26 let link_s = link.to_string_lossy();
27 let bin_s = bin.to_string_lossy();
28 let vendor_s = vendor_dir.to_string_lossy();
29 let bin_path = find_shortest_path(&link_s, &bin_s, false);
30 let bin_exported = find_shortest_path_code(&link_s, &bin_s, false, true);
31 let autoload_exported =
32 find_shortest_path_code(&link_s, &format!("{vendor_s}/autoload.php"), false, true);
33
34 match php_header(&head) {
35 Some(PhpHeader { shebang }) => {
36 let proxy_code = shebang
37 .clone()
38 .unwrap_or_else(|| "#!/usr/bin/env php".to_owned());
39 let is_phpunit = normalize_path(&bin_s)
40 == normalize_path(&format!("{vendor_s}/phpunit/phpunit/phpunit"));
41 let mut globals = String::from("$GLOBALS['_composer_bin_dir'] = __DIR__;\n");
42 globals.push_str(&format!(
43 "$GLOBALS['_composer_autoload_path'] = {autoload_exported};\n"
44 ));
45 if is_phpunit {
46 globals.push_str(&format!(
47 "$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath({bin_exported}));\n"
48 ));
49 }
50 let needs_stream = shebang.is_some() || !head.starts_with("<?php");
53 let (stream_hint, stream_code) = if needs_stream {
54 (
55 " using a stream wrapper to prevent the shebang from being output on PHP<8\n *"
56 .to_owned(),
57 stream_proxy_code(&bin_exported, is_phpunit),
58 )
59 } else {
60 (String::new(), String::new())
61 };
62 Ok(format!(
63 "{proxy_code}\n<?php\n\n/**\n * Proxy PHP file generated by Composer\n *\n * This file includes the referenced bin path ({bin_path})\n *{stream_hint}\n * @generated\n */\n\nnamespace Composer;\n\n{globals}\n{stream_code}\nreturn include {bin_exported};\n"
64 ))
65 }
66 None => Ok(sh_proxy(&bin_path)),
67 }
68}
69
70struct PhpHeader {
71 shebang: Option<String>,
72}
73
74fn php_header(head: &str) -> Option<PhpHeader> {
76 let (shebang, rest) = if head.starts_with("#!") {
77 let end = head.find('\n')?;
78 (
79 Some(head[..end].trim_end_matches('\r').to_owned()),
80 &head[end + 1..],
81 )
82 } else {
83 (None, head)
84 };
85 let trimmed = rest.trim_start_matches(['\r', '\n', '\t', ' ']);
86 trimmed
87 .starts_with("<?php")
88 .then_some(PhpHeader { shebang })
89}
90
91fn stream_proxy_code(bin_exported: &str, is_phpunit: bool) -> String {
92 let hack1 = if is_phpunit {
93 "'phpvfscomposer://'."
94 } else {
95 ""
96 };
97 let hack2 = if is_phpunit {
98 "\n $data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);\n $data = str_replace('__FILE__', var_export($this->realpath, true), $data);"
99 } else {
100 ""
101 };
102 format!(
103 r#"if (PHP_VERSION_ID < 80000) {{
104 if (!class_exists('Composer\BinProxyWrapper')) {{
105 /**
106 * @internal
107 */
108 final class BinProxyWrapper
109 {{
110 private $handle;
111 private $position;
112 private $realpath;
113
114 public function stream_open($path, $mode, $options, &$opened_path)
115 {{
116 // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
117 $opened_path = substr($path, 17);
118 $this->realpath = realpath($opened_path) ?: $opened_path;
119 $opened_path = {hack1}$this->realpath;
120 $this->handle = fopen($this->realpath, $mode);
121 $this->position = 0;
122
123 return (bool) $this->handle;
124 }}
125
126 public function stream_read($count)
127 {{
128 $data = fread($this->handle, $count);
129
130 if ($this->position === 0) {{
131 $data = preg_replace('{{^#!.*\r?\n}}', '', $data);
132 }}{hack2}
133
134 $this->position += strlen($data);
135
136 return $data;
137 }}
138
139 public function stream_cast($castAs)
140 {{
141 return $this->handle;
142 }}
143
144 public function stream_close()
145 {{
146 fclose($this->handle);
147 }}
148
149 public function stream_lock($operation)
150 {{
151 return $operation ? flock($this->handle, $operation) : true;
152 }}
153
154 public function stream_seek($offset, $whence)
155 {{
156 if (0 === fseek($this->handle, $offset, $whence)) {{
157 $this->position = ftell($this->handle);
158 return true;
159 }}
160
161 return false;
162 }}
163
164 public function stream_tell()
165 {{
166 return $this->position;
167 }}
168
169 public function stream_eof()
170 {{
171 return feof($this->handle);
172 }}
173
174 public function stream_stat()
175 {{
176 return array();
177 }}
178
179 public function stream_set_option($option, $arg1, $arg2)
180 {{
181 return true;
182 }}
183
184 public function url_stat($path, $flags)
185 {{
186 $path = substr($path, 17);
187 if (file_exists($path)) {{
188 return stat($path);
189 }}
190
191 return false;
192 }}
193 }}
194 }}
195
196 if (
197 (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
198 || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
199 ) {{
200 return include("phpvfscomposer://" . {bin_exported});
201 }}
202}}
203"#
204 )
205}
206
207fn sh_proxy(bin_path: &str) -> String {
208 let dir = bin_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(".");
209 let file = bin_path
210 .rsplit_once('/')
211 .map(|(_, f)| f)
212 .unwrap_or(bin_path);
213 format!(
215 r#"#!/usr/bin/env sh
216
217# Support bash to support `source` with fallback on $0 if this does not run with bash
218# https://stackoverflow.com/a/35006505/6512
219selfArg="$BASH_SOURCE"
220if [ -z "$selfArg" ]; then
221 selfArg="$0"
222fi
223
224self=$(realpath "$selfArg" 2> /dev/null)
225if [ -z "$self" ]; then
226 self="$selfArg"
227fi
228
229dir=$(cd "${{self%[/\\]*}}" > /dev/null; cd '{dir}' && pwd)
230
231if [ -d /proc/cygdrive ]; then
232 case $(which php) in
233 $(readlink -n /proc/cygdrive)/*)
234 # We are in Cygwin using Windows php, so the path must be translated
235 dir=$(cygpath -m "$dir");
236 ;;
237 esac
238fi
239
240export COMPOSER_RUNTIME_BIN_DIR="$(cd "${{self%[/\\]*}}" > /dev/null; pwd)"
241
242# If bash is sourcing this file, we have to source the target as well
243bashSource="$BASH_SOURCE"
244if [ -n "$bashSource" ]; then
245 if [ "$bashSource" != "$0" ]; then
246 source "${{dir}}/{file}" "$@"
247 return
248 fi
249fi
250
251exec "${{dir}}/{file}" "$@"
252"#
253 )
254}
255
256pub fn windows_proxy_content(link_bat: &Path, link_name: &str, bin: &Path) -> Result<String> {
269 let caller = windows_binary_caller(bin)?;
270 let target = if caller == "php" {
271 link_name.to_owned()
272 } else {
273 let link_s = link_bat.to_string_lossy();
274 let bin_s = bin.to_string_lossy();
275 find_shortest_path(&link_s, &bin_s, false)
276 };
277 Ok(format!(
278 "@ECHO OFF\r\n\
279 setlocal DISABLEDELAYEDEXPANSION\r\n\
280 SET BIN_TARGET=%~dp0/{target}\r\n\
281 SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n\
282 {caller} \"%BIN_TARGET%\" %*\r\n"
283 ))
284}
285
286pub fn windows_binary_caller(bin: &Path) -> Result<String> {
292 let bin_s = bin.to_string_lossy();
293 if bin_s.ends_with(".bat") || bin_s.ends_with(".exe") {
294 return Ok("call".to_owned());
295 }
296 let mut line = Vec::new();
298 {
299 use std::io::BufRead as _;
300 let f = std::fs::File::open(bin).map_err(Error::io(bin))?;
301 let mut reader = std::io::BufReader::new(f);
302 reader
303 .read_until(b'\n', &mut line)
304 .map_err(Error::io(bin))?;
305 }
306 let line = String::from_utf8_lossy(&line);
307 Ok(shebang_caller(&line).unwrap_or_else(|| "php".to_owned()))
308}
309
310fn shebang_caller(line: &str) -> Option<String> {
318 let line = line.strip_suffix('\n').unwrap_or(line);
321 let rest = line.strip_prefix("#!/")?;
322 let rest = rest.strip_prefix("usr/bin/env ").unwrap_or(rest);
323 let mut capture = rest;
326 loop {
327 match capture.find('/') {
328 Some(i) if i > 0 && i + 1 < capture.len() => capture = &capture[i + 1..],
329 _ => break,
330 }
331 }
332 if capture.is_empty() {
333 return None; }
335 Some(
337 capture
338 .trim_matches([' ', '\t', '\n', '\r', '\0', '\x0B'])
339 .to_owned(),
340 )
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum BinCompat {
349 Full,
350 Proxy,
351}
352
353pub fn resolve_bin_compat(root_manifest: &serde_json::Value) -> Result<BinCompat> {
362 let env = std::env::var("COMPOSER_BIN_COMPAT").ok();
363 let global = crate::layout::global_config_value("bin-compat");
364 resolve_bin_compat_with(
365 env.as_deref(),
366 root_manifest,
367 global.as_ref(),
368 cfg!(windows) || is_windows_subsystem_for_linux(),
369 )
370}
371
372pub fn resolve_bin_compat_with(
379 env: Option<&str>,
380 root_manifest: &serde_json::Value,
381 global: Option<&serde_json::Value>,
382 windows_or_wsl: bool,
383) -> Result<BinCompat> {
384 let env = env.filter(|v| !v.is_empty() && *v != "0");
386 let config = root_manifest
387 .get("config")
388 .and_then(|c| c.get("bin-compat"))
389 .and_then(serde_json::Value::as_str)
390 .or_else(|| global.and_then(serde_json::Value::as_str));
391 let value = env.or(config).unwrap_or("auto");
392 match value {
393 "full" => Ok(BinCompat::Full),
394 "auto" if windows_or_wsl => Ok(BinCompat::Full),
395 "auto" | "proxy" | "symlink" => Ok(BinCompat::Proxy),
396 other => Err(Error::Unsupported(format!(
397 "Invalid value for 'bin-compat': {other}. Expected auto, full or proxy"
398 ))),
399 }
400}
401
402fn is_windows_subsystem_for_linux() -> bool {
408 if cfg!(windows) {
409 return false;
410 }
411 let Ok(version) = std::fs::read_to_string("/proc/version") else {
412 return false;
413 };
414 version.to_ascii_lowercase().contains("microsoft") && !is_docker()
415}
416
417fn is_docker() -> bool {
420 if [
421 "/.dockerenv",
422 "/run/.containerenv",
423 "/var/run/.containerenv",
424 ]
425 .iter()
426 .any(|p| Path::new(p).exists())
427 {
428 return true;
429 }
430 ["/proc/self/mountinfo", "/proc/1/cgroup"].iter().any(|p| {
431 std::fs::read_to_string(p).is_ok_and(|data| {
432 data.contains("/var/lib/docker/") || data.contains("/io.containerd.snapshotter")
433 })
434 })
435}
436
437pub fn install_binaries(
444 vendor_dir: &Path,
445 package_dir: &Path,
446 bins: &[&str],
447 compat: BinCompat,
448) -> Result<()> {
449 let bin_dir = vendor_dir.join("bin");
450 std::fs::create_dir_all(&bin_dir).map_err(Error::io(&bin_dir))?;
451 for bin in bins {
452 let bin = bin.trim_start_matches("./");
453 let target = package_dir.join(bin);
454 let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
455 let link = bin_dir.join(link_name);
456 if !target.exists() {
457 continue; }
459 match compat {
460 BinCompat::Full => install_full_binaries(vendor_dir, &link, link_name, &target)?,
461 BinCompat::Proxy => install_unixy_proxy(vendor_dir, &link, &target)?,
462 }
463 }
464 Ok(())
465}
466
467fn install_full_binaries(
474 vendor_dir: &Path,
475 link: &Path,
476 link_name: &str,
477 target: &Path,
478) -> Result<()> {
479 let bat = if target.to_string_lossy().ends_with(".bat") {
480 link.to_path_buf()
481 } else {
482 install_unixy_proxy(vendor_dir, link, target)?;
483 link.with_file_name(format!("{link_name}.bat"))
484 };
485 if !bat.exists() {
486 let content = windows_proxy_content(&bat, link_name, target)?;
487 std::fs::write(&bat, content).map_err(Error::io(&bat))?;
488 set_executable(&bat)?;
489 }
490 Ok(())
491}
492
493fn install_unixy_proxy(vendor_dir: &Path, link: &Path, target: &Path) -> Result<()> {
495 let content = proxy_content(vendor_dir, link, target)?;
496 std::fs::write(link, content).map_err(Error::io(link))?;
497 set_executable(link)
498}
499
500fn set_executable(link: &Path) -> Result<()> {
503 #[cfg(unix)]
504 {
505 use std::os::unix::fs::PermissionsExt as _;
506 std::fs::set_permissions(link, std::fs::Permissions::from_mode(0o755))
507 .map_err(Error::io(link))?;
508 }
509 #[cfg(not(unix))]
510 let _ = link;
511 Ok(())
512}