vivacity-core 0.6.0

Manifests, content hash, platform checks, dist fetching, content-addressed store and installation for vivacity
Documentation
//! vendor/bin proxies: byte-for-byte port of
//! `BinaryInstaller::generateUnixyProxyCode` (docs/reference/BinaryInstaller.php).
//! Three shapes: PHP target with shebang (anti-shebang stream wrapper for
//! PHP<8, special phpunit hack), bare PHP target, non-PHP target (sh proxy).
//! Parity is held by the differential test against the proxies generated by
//! Composer in the Laravel fixture (tests/fixtures_binproxy.rs).

use crate::error::{Error, Result};
use crate::pathutil::{find_shortest_path, find_shortest_path_code, normalize_path};
use std::path::Path;

/// Generates the content of the proxy `link` (vendor/bin/<name>) to the binary
/// `bin` (absolute, inside vendor/ or outside it with composer/installers),
/// reading the target's header. Relative paths are those of
/// `BinaryInstaller::installUnixyProxyBinaries` (findShortestPath from the
/// proxy file).
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"
                ));
            }
            // Stream wrapper only if the target does not start directly
            // with `<?php` (shebang or whitespace in front).
            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>,
}

/// Composer's regex: `^(#!.*\r?\n)?[\r\n\t ]*<\?php`.
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);
    // ProcessExecutor::escape on a simple path = single quotes.
    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}" "$@"
"#
    )
}

/// Installs a package's proxies (laid out in `package_dir`) into vendor/bin (0755).
pub fn install_binaries(vendor_dir: &Path, package_dir: &Path, bins: &[&str]) -> Result<()> {
    let bin_dir = vendor_dir.join("bin");
    std::fs::create_dir_all(&bin_dir).map_err(Error::io(&bin_dir))?;
    for bin in bins {
        let bin = bin.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; // binary declared but missing from the dist: Composer skips it too
        }
        let content = proxy_content(vendor_dir, &link, &target)?;
        std::fs::write(&link, content).map_err(Error::io(&link))?;
        #[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))?;
        }
    }
    Ok(())
}