From f12a5b82140bff33062f09ac2b1d3d907ebc0911 Mon Sep 17 00:00:00 2001 From: Helmut Hummel Date: Thu, 25 Nov 2021 09:53:03 +0100 Subject: [PATCH] Expose path to autoload in a global var for binaries (#10137) Always create proxy files for package binaries, to avoid not working binaries in case the package was installed from a path repository and is itself linked If the binary is a PHP script, a global variable is now exposed, which holds the path to the vendor/autoload.php file. This variable can the be used in the binaries to include this file without guessing where the path to the vendor folder might be. Additionally it is now checked on binary creation whether the reference binary has a shebang and if not, generates a much simple proxy code, because the stream wrapper code, that is required for PHP <8 to omit the shebang from the output, can be skipped. Fixes: #10119 Co-authored-by: Jordi Boggiano --- doc/04-schema.md | 4 +- doc/06-config.md | 4 +- doc/07-runtime.md | 6 + doc/articles/vendor-binaries.md | 31 ++++- res/composer-schema.json | 4 +- src/Composer/Composer.php | 2 +- src/Composer/Config.php | 8 +- src/Composer/Factory.php | 2 +- src/Composer/Installer/BinaryInstaller.php | 127 +++++++++++--------- src/Composer/Installer/LibraryInstaller.php | 2 +- 10 files changed, 115 insertions(+), 75 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index dbc02ff24..926c2504f 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -884,8 +884,8 @@ Optional. ### bin -A set of files that should be treated as binaries and symlinked into the `bin-dir` -(from config). +A set of files that should be treated as binaries and made available +into the `bin-dir` (from config). See [Vendor Binaries](articles/vendor-binaries.md) for more details. diff --git a/doc/06-config.md b/doc/06-config.md index 96bd1840e..2a31bc512 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -257,8 +257,8 @@ If it is `auto` then Composer only installs .bat proxy files when on Windows or set to `full` then both .bat files for Windows and scripts for Unix-based operating systems will be installed for each binary. This is mainly useful if you run Composer inside a linux VM but still want the `.bat` proxies available for use -in the Windows host OS. If set to `symlink` Composer will always symlink even on -Windows/WSL. +in the Windows host OS. If set to `proxy` Composer will only create bash/Unix-style +proxy files and no .bat files even on Windows/WSL. ## prepend-autoloader diff --git a/doc/07-runtime.md b/doc/07-runtime.md index 79f0022fd..9f4f90c8e 100644 --- a/doc/07-runtime.md +++ b/doc/07-runtime.md @@ -152,4 +152,10 @@ not its exact version. `lib-*` requirements are never supported/checked by the platform check feature. +## Autoloader path in binaries + +composer-runtime-api 2.2 introduced a new `$_composer_autoload_path` global +variable set when running binaries installed with Composer. Read more +about this [on the vendor binaries docs](articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary). + ← [Config](06-config.md) | [Community](08-community.md) → diff --git a/doc/articles/vendor-binaries.md b/doc/articles/vendor-binaries.md index 0022b90b9..120b73dec 100644 --- a/doc/articles/vendor-binaries.md +++ b/doc/articles/vendor-binaries.md @@ -40,7 +40,8 @@ For the binaries that a package defines directly, nothing happens. ## What happens when Composer is run on a composer.json that has dependencies with vendor binaries listed? Composer looks for the binaries defined in all of the dependencies. A -symlink is created from each dependency's binaries to `vendor/bin`. +proxy file (or two on Windows/WSL) is created from each dependency's +binaries to `vendor/bin`. Say package `my-vendor/project-a` has binaries setup like this: @@ -69,8 +70,28 @@ Running `composer install` for this `composer.json` will look at all of project-a's binaries and install them to `vendor/bin`. In this case, Composer will make `vendor/my-vendor/project-a/bin/project-a-bin` -available as `vendor/bin/project-a-bin`. On a Unix-like platform -this is accomplished by creating a symlink. +available as `vendor/bin/project-a-bin`. + +## Finding the Composer autoloader from a binary + +As of Composer 2.2, a new `$_composer_autoload_path` global variable +is defined by the bin proxy file, so that when your binary gets executed +it can use it to easily locate the project's autoloader. + +This global will not be available however when running binaries defined +by the root package itself, so you need to have a fallback in place. + +This can look like this for example: + +```php +getComposerEnv('COMPOSER_BIN_COMPAT') ?: $this->config[$key]; - if (!in_array($value, array('auto', 'full', 'symlink'))) { + if (!in_array($value, array('auto', 'full', 'proxy', 'symlink'))) { throw new \RuntimeException( - "Invalid value for 'bin-compat': {$value}. Expected auto, full or symlink" + "Invalid value for 'bin-compat': {$value}. Expected auto, full or proxy" ); } + if ($value === 'symlink') { + trigger_error('config.bin-compat "symlink" is deprecated since Composer 2.2, use auto, full (for Windows compatibility) or proxy instead.', E_USER_DEPRECATED); + } + return $value; case 'discard-changes': diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 6b5e38c03..bc9f7ef29 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -594,7 +594,7 @@ class Factory protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null) { $fs = new Filesystem($process); - $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs); + $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs, rtrim($composer->getConfig()->get('vendor-dir'), '/')); $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller)); $im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller)); diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index 084bef18a..5b4771db9 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -36,19 +36,23 @@ class BinaryInstaller protected $io; /** @var Filesystem */ protected $filesystem; + /** @var string|null */ + private $vendorDir; /** * @param IOInterface $io * @param string $binDir * @param string $binCompat * @param Filesystem $filesystem + * @param string|null $vendorDir */ - public function __construct(IOInterface $io, $binDir, $binCompat, Filesystem $filesystem = null) + public function __construct(IOInterface $io, $binDir, $binCompat, Filesystem $filesystem = null, $vendorDir = null) { $this->binDir = $binDir; $this->binCompat = $binCompat; $this->io = $io; $this->filesystem = $filesystem ?: new Filesystem(); + $this->vendorDir = $vendorDir; } /** @@ -72,38 +76,37 @@ class BinaryInstaller $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package'); continue; } - - // 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); - + 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)) { - // likely leftover from a previous install, make sure - // that the target is still executable in case this - // is a fresh install of the vendor. - Silencer::call('chmod', $link, 0777 & ~umask()); + 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 ($warnOnOverwrite) { - $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); + 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); } - continue; } - if ($this->binCompat === "auto") { - if (Platform::isWindows() || Platform::isWindowsSubsystemForLinux()) { - $this->installFullBinaries($binPath, $link, $bin, $package); - } else { - $this->installSymlinkBinaries($binPath, $link); - } - } elseif ($this->binCompat === "full") { + $binCompat = $this->binCompat; + if ($binCompat === "auto" && (Platform::isWindows() || Platform::isWindowsSubsystemForLinux())) { + $binCompat = 'full'; + } + + if ($this->binCompat === "full") { $this->installFullBinaries($binPath, $link, $bin, $package); - } elseif ($this->binCompat === "symlink") { - $this->installSymlinkBinaries($binPath, $link); + } else { + $this->installUnixyProxyBinaries($binPath, $link); } Silencer::call('chmod', $binPath, 0777 & ~umask()); } @@ -122,10 +125,10 @@ class BinaryInstaller } foreach ($binaries as $bin) { $link = $this->binDir.'/'.basename($bin); - if (is_link($link) || file_exists($link)) { + if (is_link($link) || file_exists($link)) { // still checking for symlinks here for legacy support $this->filesystem->unlink($link); } - if (file_exists($link.'.bat')) { + if (is_file($link.'.bat')) { $this->filesystem->unlink($link.'.bat'); } } @@ -188,19 +191,6 @@ class BinaryInstaller } } - /** - * @param string $binPath - * @param string $link - * - * @return void - */ - protected function installSymlinkBinaries($binPath, $link) - { - if (!$this->filesystem->relativeSymlink($binPath, $link)) { - $this->installUnixyProxyBinaries($binPath, $link); - } - } - /** * @param string $binPath * @param string $link @@ -233,6 +223,16 @@ class BinaryInstaller $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". + "{$caller} \"%BIN_TARGET%\" %*\r\n"; + } + return "@ECHO OFF\r\n". "setlocal DISABLEDELAYEDEXPANSION\r\n". "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"\'')."\r\n". @@ -258,25 +258,15 @@ class BinaryInstaller if (preg_match('{^(#!.*\r?\n)?<\?php}', $binContents, $match)) { // carry over the existing shebang if present, otherwise add our own $proxyCode = empty($match[1]) ? '#!/usr/bin/env php' : trim($match[1]); - - $binPathExported = var_export($binPath, true); - - return $proxyCode . "\n" . <<filesystem->findShortestPathCode($link, $bin, false, true); + $autoloadPathCode = $streamProxyCode = $streamHint = ''; + // Don't expose autoload path when vendor dir was not set in custom installers + if ($this->vendorDir) { + $autoloadPathCode = '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $this->vendorDir . '/autoload.php', false, true).";\n"; + } + if (trim($match[0]) !== 'filesystem = $filesystem ?: new Filesystem(); $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); - $this->binaryInstaller = $binaryInstaller ?: new BinaryInstaller($this->io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $this->filesystem); + $this->binaryInstaller = $binaryInstaller ?: new BinaryInstaller($this->io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $this->filesystem, $this->vendorDir); } /**