Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. BinaryInstaller.php

class BinaryInstaller

Utility to handle installation of package "bin"/binaries

@author Jordi Boggiano <j.boggiano@seld.be> @author Konstantin Kudryashov <ever.zet@gmail.com> @author Helmut Hummel <info@helhum.io>

Hierarchy

  • class \Composer\Installer\BinaryInstaller

Expanded class hierarchy of BinaryInstaller

2 files declare their use of BinaryInstaller
EventDispatcher.php in vendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.php
Installer.php in vendor/composer/installers/src/Composer/Installers/Installer.php

File

vendor/composer/composer/src/Composer/Installer/BinaryInstaller.php, line 30

Namespace

Composer\Installer
View source
class BinaryInstaller {
    
    /** @var string */
    protected $binDir;
    
    /** @var string */
    protected $binCompat;
    
    /** @var IOInterface */
    protected $io;
    
    /** @var Filesystem */
    protected $filesystem;
    
    /** @var string|null */
    private $vendorDir;
    
    /**
     * @param Filesystem  $filesystem
     */
    public function __construct(IOInterface $io, string $binDir, string $binCompat, ?Filesystem $filesystem = null, ?string $vendorDir = null) {
        $this->binDir = $binDir;
        $this->binCompat = $binCompat;
        $this->io = $io;
        $this->filesystem = $filesystem ?: new Filesystem();
        $this->vendorDir = $vendorDir;
    }
    public function installBinaries(PackageInterface $package, string $installPath, bool $warnOnOverwrite = true) : void {
        $binaries = $this->getBinaries($package);
        if (!$binaries) {
            return;
        }
        Platform::workaroundFilesystemIssues();
        foreach ($binaries as $bin) {
            $binPath = $installPath . '/' . $bin;
            if (!file_exists($binPath)) {
                $this->io
                    ->writeError('    <warning>Skipped installation of bin ' . $bin . ' for package ' . $package->getName() . ': file not found in package</warning>');
                continue;
            }
            if (is_dir($binPath)) {
                $this->io
                    ->writeError('    <warning>Skipped installation of bin ' . $bin . ' for package ' . $package->getName() . ': found a directory at that path</warning>');
                continue;
            }
            if (!$this->filesystem
                ->isAbsolutePath($binPath)) {
                // in case a custom installer returned a relative path for the
                // $package, we can now safely turn it into a absolute path (as we
                // already checked the binary's existence). The following helpers
                // will require absolute paths to work properly.
                $binPath = realpath($binPath);
            }
            $this->initializeBinDir();
            $link = $this->binDir . '/' . basename($bin);
            if (file_exists($link)) {
                if (!is_link($link)) {
                    if ($warnOnOverwrite) {
                        $this->io
                            ->writeError('    Skipped installation of bin ' . $bin . ' for package ' . $package->getName() . ': name conflicts with an existing file');
                    }
                    continue;
                }
                if (realpath($link) === realpath($binPath)) {
                    // It is a linked binary from a previous installation, which can be replaced with a proxy file
                    $this->filesystem
                        ->unlink($link);
                }
            }
            $binCompat = $this->binCompat;
            if ($binCompat === "auto" && (Platform::isWindows() || Platform::isWindowsSubsystemForLinux())) {
                $binCompat = 'full';
            }
            if ($binCompat === "full") {
                $this->installFullBinaries($binPath, $link, $bin, $package);
            }
            else {
                $this->installUnixyProxyBinaries($binPath, $link);
            }
            Silencer::call('chmod', $binPath, 0777 & ~umask());
        }
    }
    public function removeBinaries(PackageInterface $package) : void {
        $this->initializeBinDir();
        $binaries = $this->getBinaries($package);
        if (!$binaries) {
            return;
        }
        foreach ($binaries as $bin) {
            $link = $this->binDir . '/' . basename($bin);
            if (is_link($link) || file_exists($link)) {
                // still checking for symlinks here for legacy support
                $this->filesystem
                    ->unlink($link);
            }
            if (is_file($link . '.bat')) {
                $this->filesystem
                    ->unlink($link . '.bat');
            }
        }
        // attempt removing the bin dir in case it is left empty
        if (is_dir($this->binDir) && $this->filesystem
            ->isDirEmpty($this->binDir)) {
            Silencer::call('rmdir', $this->binDir);
        }
    }
    public static function determineBinaryCaller(string $bin) : string {
        if ('.bat' === substr($bin, -4) || '.exe' === substr($bin, -4)) {
            return 'call';
        }
        $handle = fopen($bin, 'r');
        $line = fgets($handle);
        fclose($handle);
        if (Preg::isMatchStrictGroups('{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m', (string) $line, $match)) {
            return trim($match[1]);
        }
        return 'php';
    }
    
    /**
     * @return string[]
     */
    protected function getBinaries(PackageInterface $package) : array {
        return $package->getBinaries();
    }
    protected function installFullBinaries(string $binPath, string $link, string $bin, PackageInterface $package) : void {
        // add unixy support for cygwin and similar environments
        if ('.bat' !== substr($binPath, -4)) {
            $this->installUnixyProxyBinaries($binPath, $link);
            $link .= '.bat';
            if (file_exists($link)) {
                $this->io
                    ->writeError('    Skipped installation of bin ' . $bin . '.bat proxy for package ' . $package->getName() . ': a .bat proxy was already installed');
            }
        }
        if (!file_exists($link)) {
            file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link));
            Silencer::call('chmod', $link, 0777 & ~umask());
        }
    }
    protected function installUnixyProxyBinaries(string $binPath, string $link) : void {
        file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link));
        Silencer::call('chmod', $link, 0777 & ~umask());
    }
    protected function initializeBinDir() : void {
        $this->filesystem
            ->ensureDirectoryExists($this->binDir);
        $this->binDir = realpath($this->binDir);
    }
    protected function generateWindowsProxyCode(string $bin, string $link) : string {
        $binPath = $this->filesystem
            ->findShortestPath($link, $bin);
        $caller = self::determineBinaryCaller($bin);
        // if the target is a php file, we run the unixy proxy file
        // to ensure that _composer_autoload_path gets defined, instead
        // of running the binary directly
        if ($caller === 'php') {
            return "@ECHO OFF\r\n" . "setlocal DISABLEDELAYEDEXPANSION\r\n" . "SET BIN_TARGET=%~dp0/" . trim(ProcessExecutor::escape(basename($link, '.bat')), '"\'') . "\r\n" . "SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n" . "{$caller} \"%BIN_TARGET%\" %*\r\n";
        }
        return "@ECHO OFF\r\n" . "setlocal DISABLEDELAYEDEXPANSION\r\n" . "SET BIN_TARGET=%~dp0/" . trim(ProcessExecutor::escape($binPath), '"\'') . "\r\n" . "SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n" . "{$caller} \"%BIN_TARGET%\" %*\r\n";
    }
    protected function generateUnixyProxyCode(string $bin, string $link) : string {
        $binPath = $this->filesystem
            ->findShortestPath($link, $bin);
        $binDir = ProcessExecutor::escape(dirname($binPath));
        $binFile = basename($binPath);
        $binContents = (string) file_get_contents($bin, false, null, 0, 500);
        // For php files, we generate a PHP proxy instead of a shell one,
        // which allows calling the proxy with a custom php process
        if (Preg::isMatch('{^(#!.*\\r?\\n)?[\\r\\n\\t ]*<\\?php}', $binContents, $match)) {
            // carry over the existing shebang if present, otherwise add our own
            $proxyCode = $match[1] === null ? '#!/usr/bin/env php' : trim($match[1]);
            $binPathExported = $this->filesystem
                ->findShortestPathCode($link, $bin, false, true);
            $streamProxyCode = $streamHint = '';
            $globalsCode = '$GLOBALS[\'_composer_bin_dir\'] = __DIR__;' . "\n";
            $phpunitHack1 = $phpunitHack2 = '';
            // Don't expose autoload path when vendor dir was not set in custom installers
            if ($this->vendorDir !== null) {
                // ensure comparisons work accurately if the CWD is a symlink, as $link is realpath'd already
                $vendorDirReal = realpath($this->vendorDir);
                if ($vendorDirReal === false) {
                    $vendorDirReal = $this->vendorDir;
                }
                $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem
                    ->findShortestPathCode($link, $vendorDirReal . '/autoload.php', false, true) . ";\n";
            }
            // Add workaround for PHPUnit process isolation
            if ($this->filesystem
                ->normalizePath($bin) === $this->filesystem
                ->normalizePath($this->vendorDir . '/phpunit/phpunit/phpunit')) {
                // workaround issue on PHPUnit 6.5+ running on PHP 8+
                $globalsCode .= '$GLOBALS[\'__PHPUNIT_ISOLATION_EXCLUDE_LIST\'] = $GLOBALS[\'__PHPUNIT_ISOLATION_BLACKLIST\'] = array(realpath(' . $binPathExported . '));' . "\n";
                // workaround issue on all PHPUnit versions running on PHP <8
                $phpunitHack1 = "'phpvfscomposer://'.";
                $phpunitHack2 = '
                $data = str_replace(\'__DIR__\', var_export(dirname($this->realpath), true), $data);
                $data = str_replace(\'__FILE__\', var_export($this->realpath, true), $data);';
            }
            if (trim($match[0]) !== '<?php') {
                $streamHint = ' using a stream wrapper to prevent the shebang from being output on PHP<8' . "\n *";
                $streamProxyCode = <<<STREAMPROXY
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 = {<span class="php-variable">$phpunitHack1</span>}\$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);
                }{<span class="php-variable">$phpunitHack2</span>}

                \$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://" . {<span class="php-variable">$binPathExported</span>});
    }
}

STREAMPROXY;
            }
            return $proxyCode . "\n" . <<<PROXY
<?php

/**
 * Proxy PHP file generated by Composer
 *
 * This file includes the referenced bin path ({<span class="php-variable">$binPath</span>})
 *{<span class="php-variable">$streamHint</span>}
 * @generated
 */

namespace Composer;

{<span class="php-variable">$globalsCode</span>}
{<span class="php-variable">$streamProxyCode</span>}
return include {<span class="php-variable">$binPathExported</span>};

PROXY;
        }
        return <<<PROXY
#!/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 {<span class="php-variable">$binDir</span>} && 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}/{<span class="php-variable">$binFile</span>}" "\$@"
        return
    fi
fi

exec "\${dir}/{<span class="php-variable">$binFile</span>}" "\$@"

PROXY;
    }

}

Members

Title Sort descending Modifiers Object type Summary
BinaryInstaller::$binCompat protected property @var string
BinaryInstaller::$binDir protected property @var string
BinaryInstaller::$filesystem protected property @var Filesystem
BinaryInstaller::$io protected property @var IOInterface
BinaryInstaller::$vendorDir private property @var string|null
BinaryInstaller::determineBinaryCaller public static function
BinaryInstaller::generateUnixyProxyCode protected function
BinaryInstaller::generateWindowsProxyCode protected function
BinaryInstaller::getBinaries protected function
BinaryInstaller::initializeBinDir protected function
BinaryInstaller::installBinaries public function
BinaryInstaller::installFullBinaries protected function
BinaryInstaller::installUnixyProxyBinaries protected function
BinaryInstaller::removeBinaries public function
BinaryInstaller::__construct public function

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal