1
0
Fork 0

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 <j.boggiano@seld.be>
pull/10308/head
Helmut Hummel 2021-11-25 09:53:03 +01:00 committed by GitHub
parent dc526d354c
commit f12a5b8214
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 75 deletions

View File

@ -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.

View File

@ -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

View File

@ -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).
&larr; [Config](06-config.md) | [Community](08-community.md) &rarr;

View File

@ -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
<?php
include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
```
If you want to rely on this in your package you should however make sure to
also require `"composer-runtime-api": "^2.2"` to ensure that the package
gets installed with a Composer version supporting the feature.
## What about Windows and .bat files?
@ -79,8 +100,8 @@ Packages managed entirely by Composer do not *need* to contain any
of binaries in a special way when run in a Windows environment:
* A `.bat` file is generated automatically to reference the binary
* A Unix-style proxy file with the same name as the binary is generated
automatically (useful for Cygwin or Git Bash)
* A Unix-style proxy file with the same name as the binary is also
generated, which is useful for WSL, Linux VMs, etc.
Packages that need to support workflows that may not include Composer
are welcome to maintain custom `.bat` files. In this case, the package

View File

@ -251,8 +251,8 @@
"description": "Whether to use the Composer cache in read-only mode."
},
"bin-compat": {
"enum": ["auto", "full", "symlink"],
"description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed), can be \"full\" (compatible with both Windows and Unix-based systems) and \"symlink\" (symlink also for WSL)."
"enum": ["auto", "full", "proxy", "symlink"],
"description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed), can be \"full\" (compatible with both Windows and Unix-based systems) and \"proxy\" (only bash-style proxy)."
},
"discard-changes": {
"type": ["string", "boolean"],

View File

@ -65,7 +65,7 @@ class Composer
*
* @var string
*/
const RUNTIME_API_VERSION = '2.1.0';
const RUNTIME_API_VERSION = '2.2.0';
/**
* @return string

View File

@ -367,12 +367,16 @@ class Config
case 'bin-compat':
$value = $this->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':

View File

@ -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));

View File

@ -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(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package</warning>');
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" . <<<PROXY
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path ($binPath) using ob_start to remove the shebang if present
* to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
\$binPath = __DIR__ . "/" . $binPathExported;
$binPathExported = $this->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]) !== '<?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')) {
/**
@ -357,6 +347,25 @@ if (PHP_VERSION_ID < 80000) {
}
}
STREAMPROXY;
}
return $proxyCode . "\n" . <<<PROXY
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path ($binPath)
*$streamHint
* @generated
*/
namespace Composer;
\$binPath = $binPathExported;
$autoloadPathCode
$streamProxyCode
include \$binPath;
PROXY;

View File

@ -63,7 +63,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
$this->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);
}
/**