use crate::error::{Error, Result};
use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
use std::path::Path;
pub fn proxy_content(vendor_dir: &Path, link: &Path, bin: &Path) -> Result<String> {
let mut head = [0u8; 500];
let n = {
use std::io::Read as _;
let mut f = std::fs::File::open(bin).map_err(Error::io(bin))?;
f.read(&mut head).map_err(Error::io(bin))?
};
let head = String::from_utf8_lossy(&head[..n]);
let link_s = link.to_string_lossy();
let bin_s = bin.to_string_lossy();
let vendor_s = vendor_dir.to_string_lossy();
let bin_path = find_shortest_path(&link_s, &bin_s, false);
let bin_exported = find_shortest_path_code(&link_s, &bin_s, false, true);
let autoload_exported =
find_shortest_path_code(&link_s, &format!("{vendor_s}/autoload.php"), false, true);
match php_header(&head) {
Some(PhpHeader { shebang }) => {
let proxy_code = shebang
.clone()
.unwrap_or_else(|| "#!/usr/bin/env php".to_owned());
let is_phpunit = normalize_path(&bin_s)
== normalize_path(&format!("{vendor_s}/phpunit/phpunit/phpunit"));
let mut globals = String::from("$GLOBALS['_composer_bin_dir'] = __DIR__;\n");
globals.push_str(&format!(
"$GLOBALS['_composer_autoload_path'] = {autoload_exported};\n"
));
if is_phpunit {
globals.push_str(&format!(
"$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath({bin_exported}));\n"
));
}
let needs_stream = shebang.is_some() || !head.starts_with("<?php");
let (stream_hint, stream_code) = if needs_stream {
(
" using a stream wrapper to prevent the shebang from being output on PHP<8\n *"
.to_owned(),
stream_proxy_code(&bin_exported, is_phpunit),
)
} else {
(String::new(), String::new())
};
Ok(format!(
"{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"
))
}
None => Ok(sh_proxy(&bin_path)),
}
}
struct PhpHeader {
shebang: Option<String>,
}
fn php_header(head: &str) -> Option<PhpHeader> {
let (shebang, rest) = if head.starts_with("#!") {
let end = head.find('\n')?;
(
Some(head[..end].trim_end_matches('\r').to_owned()),
&head[end + 1..],
)
} else {
(None, head)
};
let trimmed = rest.trim_start_matches(['\r', '\n', '\t', ' ']);
trimmed
.starts_with("<?php")
.then_some(PhpHeader { shebang })
}
fn stream_proxy_code(bin_exported: &str, is_phpunit: bool) -> String {
let hack1 = if is_phpunit {
"'phpvfscomposer://'."
} else {
""
};
let hack2 = if is_phpunit {
"\n $data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);\n $data = str_replace('__FILE__', var_export($this->realpath, true), $data);"
} else {
""
};
format!(
r#"if (PHP_VERSION_ID < 80000) {{
if (!class_exists('Composer\BinProxyWrapper')) {{
/**
* @internal
*/
final class BinProxyWrapper
{{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = {hack1}$this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}}
public function stream_read($count)
{{
$data = fread($this->handle, $count);
if ($this->position === 0) {{
$data = preg_replace('{{^#!.*\r?\n}}', '', $data);
}}{hack2}
$this->position += strlen($data);
return $data;
}}
public function stream_cast($castAs)
{{
return $this->handle;
}}
public function stream_close()
{{
fclose($this->handle);
}}
public function stream_lock($operation)
{{
return $operation ? flock($this->handle, $operation) : true;
}}
public function stream_seek($offset, $whence)
{{
if (0 === fseek($this->handle, $offset, $whence)) {{
$this->position = ftell($this->handle);
return true;
}}
return false;
}}
public function stream_tell()
{{
return $this->position;
}}
public function stream_eof()
{{
return feof($this->handle);
}}
public function stream_stat()
{{
return array();
}}
public function stream_set_option($option, $arg1, $arg2)
{{
return true;
}}
public function url_stat($path, $flags)
{{
$path = substr($path, 17);
if (file_exists($path)) {{
return stat($path);
}}
return false;
}}
}}
}}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {{
return include("phpvfscomposer://" . {bin_exported});
}}
}}
"#
)
}
fn sh_proxy(bin_path: &str) -> String {
let dir = bin_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(".");
let file = bin_path
.rsplit_once('/')
.map(|(_, f)| f)
.unwrap_or(bin_path);
format!(
r#"#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath "$selfArg" 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${{self%[/\\]*}}" > /dev/null; cd '{dir}' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${{self%[/\\]*}}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${{dir}}/{file}" "$@"
return
fi
fi
exec "${{dir}}/{file}" "$@"
"#
)
}
pub fn windows_proxy_content(link_bat: &Path, link_name: &str, bin: &Path) -> Result<String> {
let caller = windows_binary_caller(bin)?;
let target = if caller == "php" {
link_name.to_owned()
} else {
let link_s = link_bat.to_string_lossy();
let bin_s = bin.to_string_lossy();
find_shortest_path(&link_s, &bin_s, false)
};
Ok(format!(
"@ECHO OFF\r\n\
setlocal DISABLEDELAYEDEXPANSION\r\n\
SET BIN_TARGET=%~dp0/{target}\r\n\
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n\
{caller} \"%BIN_TARGET%\" %*\r\n"
))
}
pub fn windows_binary_caller(bin: &Path) -> Result<String> {
let bin_s = bin.to_string_lossy();
if bin_s.ends_with(".bat") || bin_s.ends_with(".exe") {
return Ok("call".to_owned());
}
let mut line = Vec::new();
{
use std::io::BufRead as _;
let f = std::fs::File::open(bin).map_err(Error::io(bin))?;
let mut reader = std::io::BufReader::new(f);
reader
.read_until(b'\n', &mut line)
.map_err(Error::io(bin))?;
}
let line = String::from_utf8_lossy(&line);
Ok(shebang_caller(&line).unwrap_or_else(|| "php".to_owned()))
}
fn shebang_caller(line: &str) -> Option<String> {
let line = line.strip_suffix('\n').unwrap_or(line);
let rest = line.strip_prefix("#!/")?;
let rest = rest.strip_prefix("usr/bin/env ").unwrap_or(rest);
let mut capture = rest;
loop {
match capture.find('/') {
Some(i) if i > 0 && i + 1 < capture.len() => capture = &capture[i + 1..],
_ => break,
}
}
if capture.is_empty() {
return None; }
Some(
capture
.trim_matches([' ', '\t', '\n', '\r', '\0', '\x0B'])
.to_owned(),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinCompat {
Full,
Proxy,
}
pub fn resolve_bin_compat(root_manifest: &serde_json::Value) -> Result<BinCompat> {
let env = std::env::var("COMPOSER_BIN_COMPAT").ok();
let global = crate::layout::global_config_value("bin-compat");
resolve_bin_compat_with(
env.as_deref(),
root_manifest,
global.as_ref(),
cfg!(windows) || is_windows_subsystem_for_linux(),
)
}
pub fn resolve_bin_compat_with(
env: Option<&str>,
root_manifest: &serde_json::Value,
global: Option<&serde_json::Value>,
windows_or_wsl: bool,
) -> Result<BinCompat> {
let env = env.filter(|v| !v.is_empty() && *v != "0");
let config = root_manifest
.get("config")
.and_then(|c| c.get("bin-compat"))
.and_then(serde_json::Value::as_str)
.or_else(|| global.and_then(serde_json::Value::as_str));
let value = env.or(config).unwrap_or("auto");
match value {
"full" => Ok(BinCompat::Full),
"auto" if windows_or_wsl => Ok(BinCompat::Full),
"auto" | "proxy" | "symlink" => Ok(BinCompat::Proxy),
other => Err(Error::Unsupported(format!(
"Invalid value for 'bin-compat': {other}. Expected auto, full or proxy"
))),
}
}
fn is_windows_subsystem_for_linux() -> bool {
if cfg!(windows) {
return false;
}
let Ok(version) = std::fs::read_to_string("/proc/version") else {
return false;
};
version.to_ascii_lowercase().contains("microsoft") && !is_docker()
}
fn is_docker() -> bool {
if [
"/.dockerenv",
"/run/.containerenv",
"/var/run/.containerenv",
]
.iter()
.any(|p| Path::new(p).exists())
{
return true;
}
["/proc/self/mountinfo", "/proc/1/cgroup"].iter().any(|p| {
std::fs::read_to_string(p).is_ok_and(|data| {
data.contains("/var/lib/docker/") || data.contains("/io.containerd.snapshotter")
})
})
}
pub fn install_binaries(
vendor_dir: &Path,
bin_dir: &Path,
package: &str,
package_dir: &Path,
bins: &[&str],
compat: BinCompat,
warn_on_overwrite: bool,
) -> Result<Vec<String>> {
std::fs::create_dir_all(bin_dir).map_err(Error::io(bin_dir))?;
let mut skipped = Vec::new();
for declared in bins {
let bin = declared.trim_start_matches("./");
let target = package_dir.join(bin);
let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
let link = bin_dir.join(link_name);
if !target.exists() {
continue; }
if let Ok(meta) = std::fs::symlink_metadata(&link) {
if !meta.file_type().is_symlink() {
if warn_on_overwrite {
skipped.push(format!(
" Skipped installation of bin {declared} for package {package}: name conflicts with an existing file"
));
}
continue;
}
std::fs::remove_file(&link).map_err(Error::io(&link))?;
}
match compat {
BinCompat::Full => install_full_binaries(vendor_dir, &link, link_name, &target)?,
BinCompat::Proxy => install_unixy_proxy(vendor_dir, &link, &target)?,
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = umask_mode_0777(bin_dir)?;
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(mode))
.map_err(Error::io(&target))?;
}
}
Ok(skipped)
}
pub fn remove_binaries(bin_dir: &Path, bins: &[&str]) -> Result<()> {
if bins.is_empty() {
return Ok(());
}
for bin in bins {
let bin = bin.trim_start_matches("./");
let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
for p in [
bin_dir.join(link_name),
bin_dir.join(format!("{link_name}.bat")),
] {
match std::fs::remove_file(&p) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(Error::io(&p)(e)),
}
}
}
remove_bin_dir_if_empty(bin_dir)
}
pub fn remove_bin_dir_if_empty(bin_dir: &Path) -> Result<()> {
if let Ok(mut entries) = std::fs::read_dir(bin_dir) {
if entries.next().is_none() {
std::fs::remove_dir(bin_dir).map_err(Error::io(bin_dir))?;
}
}
Ok(())
}
#[cfg(unix)]
fn umask_mode_0777(dir: &Path) -> Result<u32> {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let probe = dir.join(format!(".vivacity-umask-{}", std::process::id()));
let f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o777)
.open(&probe)
.map_err(Error::io(&probe))?;
let mode = f
.metadata()
.map_err(Error::io(&probe))?
.permissions()
.mode()
& 0o777;
drop(f);
let _ = std::fs::remove_file(&probe);
Ok(mode)
}
fn install_full_binaries(
vendor_dir: &Path,
link: &Path,
link_name: &str,
target: &Path,
) -> Result<()> {
let bat = if target.to_string_lossy().ends_with(".bat") {
link.to_path_buf()
} else {
install_unixy_proxy(vendor_dir, link, target)?;
link.with_file_name(format!("{link_name}.bat"))
};
if !bat.exists() {
let content = windows_proxy_content(&bat, link_name, target)?;
std::fs::write(&bat, content).map_err(Error::io(&bat))?;
set_executable(&bat)?;
}
Ok(())
}
fn install_unixy_proxy(vendor_dir: &Path, link: &Path, target: &Path) -> Result<()> {
let content = proxy_content(vendor_dir, link, target)?;
std::fs::write(link, content).map_err(Error::io(link))?;
set_executable(link)
}
fn set_executable(link: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(link, std::fs::Permissions::from_mode(0o755))
.map_err(Error::io(link))?;
}
#[cfg(not(unix))]
let _ = link;
Ok(())
}