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
parent
dc526d354c
commit
f12a5b8214
|
@ -884,8 +884,8 @@ Optional.
|
||||||
|
|
||||||
### bin
|
### bin
|
||||||
|
|
||||||
A set of files that should be treated as binaries and symlinked into the `bin-dir`
|
A set of files that should be treated as binaries and made available
|
||||||
(from config).
|
into the `bin-dir` (from config).
|
||||||
|
|
||||||
See [Vendor Binaries](articles/vendor-binaries.md) for more details.
|
See [Vendor Binaries](articles/vendor-binaries.md) for more details.
|
||||||
|
|
||||||
|
|
|
@ -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
|
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
|
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
|
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
|
in the Windows host OS. If set to `proxy` Composer will only create bash/Unix-style
|
||||||
Windows/WSL.
|
proxy files and no .bat files even on Windows/WSL.
|
||||||
|
|
||||||
## prepend-autoloader
|
## prepend-autoloader
|
||||||
|
|
||||||
|
|
|
@ -152,4 +152,10 @@ not its exact version.
|
||||||
|
|
||||||
`lib-*` requirements are never supported/checked by the platform check feature.
|
`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) →
|
← [Config](06-config.md) | [Community](08-community.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?
|
## 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
|
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:
|
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`.
|
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`
|
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
|
available as `vendor/bin/project-a-bin`.
|
||||||
this is accomplished by creating a symlink.
|
|
||||||
|
## 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?
|
## 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:
|
of binaries in a special way when run in a Windows environment:
|
||||||
|
|
||||||
* A `.bat` file is generated automatically to reference the binary
|
* A `.bat` file is generated automatically to reference the binary
|
||||||
* A Unix-style proxy file with the same name as the binary is generated
|
* A Unix-style proxy file with the same name as the binary is also
|
||||||
automatically (useful for Cygwin or Git Bash)
|
generated, which is useful for WSL, Linux VMs, etc.
|
||||||
|
|
||||||
Packages that need to support workflows that may not include Composer
|
Packages that need to support workflows that may not include Composer
|
||||||
are welcome to maintain custom `.bat` files. In this case, the package
|
are welcome to maintain custom `.bat` files. In this case, the package
|
||||||
|
|
|
@ -251,8 +251,8 @@
|
||||||
"description": "Whether to use the Composer cache in read-only mode."
|
"description": "Whether to use the Composer cache in read-only mode."
|
||||||
},
|
},
|
||||||
"bin-compat": {
|
"bin-compat": {
|
||||||
"enum": ["auto", "full", "symlink"],
|
"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 \"symlink\" (symlink also for WSL)."
|
"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": {
|
"discard-changes": {
|
||||||
"type": ["string", "boolean"],
|
"type": ["string", "boolean"],
|
||||||
|
|
|
@ -65,7 +65,7 @@ class Composer
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
const RUNTIME_API_VERSION = '2.1.0';
|
const RUNTIME_API_VERSION = '2.2.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
|
|
|
@ -367,12 +367,16 @@ class Config
|
||||||
case 'bin-compat':
|
case 'bin-compat':
|
||||||
$value = $this->getComposerEnv('COMPOSER_BIN_COMPAT') ?: $this->config[$key];
|
$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(
|
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;
|
return $value;
|
||||||
|
|
||||||
case 'discard-changes':
|
case 'discard-changes':
|
||||||
|
|
|
@ -594,7 +594,7 @@ class Factory
|
||||||
protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null)
|
protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null)
|
||||||
{
|
{
|
||||||
$fs = new Filesystem($process);
|
$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\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller));
|
||||||
$im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller));
|
$im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller));
|
||||||
|
|
|
@ -36,19 +36,23 @@ class BinaryInstaller
|
||||||
protected $io;
|
protected $io;
|
||||||
/** @var Filesystem */
|
/** @var Filesystem */
|
||||||
protected $filesystem;
|
protected $filesystem;
|
||||||
|
/** @var string|null */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param IOInterface $io
|
* @param IOInterface $io
|
||||||
* @param string $binDir
|
* @param string $binDir
|
||||||
* @param string $binCompat
|
* @param string $binCompat
|
||||||
* @param Filesystem $filesystem
|
* @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->binDir = $binDir;
|
||||||
$this->binCompat = $binCompat;
|
$this->binCompat = $binCompat;
|
||||||
$this->io = $io;
|
$this->io = $io;
|
||||||
$this->filesystem = $filesystem ?: new Filesystem();
|
$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>');
|
$this->io->writeError(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package</warning>');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!$this->filesystem->isAbsolutePath($binPath)) {
|
||||||
// in case a custom installer returned a relative path for the
|
// in case a custom installer returned a relative path for the
|
||||||
// $package, we can now safely turn it into a absolute path (as we
|
// $package, we can now safely turn it into a absolute path (as we
|
||||||
// already checked the binary's existence). The following helpers
|
// already checked the binary's existence). The following helpers
|
||||||
// will require absolute paths to work properly.
|
// will require absolute paths to work properly.
|
||||||
$binPath = realpath($binPath);
|
$binPath = realpath($binPath);
|
||||||
|
}
|
||||||
$this->initializeBinDir();
|
$this->initializeBinDir();
|
||||||
$link = $this->binDir.'/'.basename($bin);
|
$link = $this->binDir.'/'.basename($bin);
|
||||||
if (file_exists($link)) {
|
if (file_exists($link)) {
|
||||||
if (is_link($link)) {
|
if (!is_link($link)) {
|
||||||
// likely leftover from a previous install, make sure
|
if ($warnOnOverwrite) {
|
||||||
// that the target is still executable in case this
|
$this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
|
||||||
// is a fresh install of the vendor.
|
}
|
||||||
Silencer::call('chmod', $link, 0777 & ~umask());
|
continue;
|
||||||
}
|
}
|
||||||
if ($warnOnOverwrite) {
|
if (realpath($link) === realpath($binPath)) {
|
||||||
$this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
|
// 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") {
|
$binCompat = $this->binCompat;
|
||||||
if (Platform::isWindows() || Platform::isWindowsSubsystemForLinux()) {
|
if ($binCompat === "auto" && (Platform::isWindows() || Platform::isWindowsSubsystemForLinux())) {
|
||||||
$this->installFullBinaries($binPath, $link, $bin, $package);
|
$binCompat = 'full';
|
||||||
} else {
|
}
|
||||||
$this->installSymlinkBinaries($binPath, $link);
|
|
||||||
}
|
if ($this->binCompat === "full") {
|
||||||
} elseif ($this->binCompat === "full") {
|
|
||||||
$this->installFullBinaries($binPath, $link, $bin, $package);
|
$this->installFullBinaries($binPath, $link, $bin, $package);
|
||||||
} elseif ($this->binCompat === "symlink") {
|
} else {
|
||||||
$this->installSymlinkBinaries($binPath, $link);
|
$this->installUnixyProxyBinaries($binPath, $link);
|
||||||
}
|
}
|
||||||
Silencer::call('chmod', $binPath, 0777 & ~umask());
|
Silencer::call('chmod', $binPath, 0777 & ~umask());
|
||||||
}
|
}
|
||||||
|
@ -122,10 +125,10 @@ class BinaryInstaller
|
||||||
}
|
}
|
||||||
foreach ($binaries as $bin) {
|
foreach ($binaries as $bin) {
|
||||||
$link = $this->binDir.'/'.basename($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);
|
$this->filesystem->unlink($link);
|
||||||
}
|
}
|
||||||
if (file_exists($link.'.bat')) {
|
if (is_file($link.'.bat')) {
|
||||||
$this->filesystem->unlink($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 $binPath
|
||||||
* @param string $link
|
* @param string $link
|
||||||
|
@ -233,6 +223,16 @@ class BinaryInstaller
|
||||||
$binPath = $this->filesystem->findShortestPath($link, $bin);
|
$binPath = $this->filesystem->findShortestPath($link, $bin);
|
||||||
$caller = self::determineBinaryCaller($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".
|
return "@ECHO OFF\r\n".
|
||||||
"setlocal DISABLEDELAYEDEXPANSION\r\n".
|
"setlocal DISABLEDELAYEDEXPANSION\r\n".
|
||||||
"SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"\'')."\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)) {
|
if (preg_match('{^(#!.*\r?\n)?<\?php}', $binContents, $match)) {
|
||||||
// carry over the existing shebang if present, otherwise add our own
|
// carry over the existing shebang if present, otherwise add our own
|
||||||
$proxyCode = empty($match[1]) ? '#!/usr/bin/env php' : trim($match[1]);
|
$proxyCode = empty($match[1]) ? '#!/usr/bin/env php' : trim($match[1]);
|
||||||
|
$binPathExported = $this->filesystem->findShortestPathCode($link, $bin, false, true);
|
||||||
$binPathExported = var_export($binPath, true);
|
$autoloadPathCode = $streamProxyCode = $streamHint = '';
|
||||||
|
// Don't expose autoload path when vendor dir was not set in custom installers
|
||||||
return $proxyCode . "\n" . <<<PROXY
|
if ($this->vendorDir) {
|
||||||
<?php
|
$autoloadPathCode = '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $this->vendorDir . '/autoload.php', false, true).";\n";
|
||||||
|
}
|
||||||
/**
|
if (trim($match[0]) !== '<?php') {
|
||||||
* Proxy PHP file generated by Composer
|
$streamHint = ' using a stream wrapper to prevent the shebang from being output on PHP<8'."\n *";
|
||||||
*
|
$streamProxyCode = <<<STREAMPROXY
|
||||||
* 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;
|
|
||||||
|
|
||||||
if (PHP_VERSION_ID < 80000) {
|
if (PHP_VERSION_ID < 80000) {
|
||||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
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;
|
include \$binPath;
|
||||||
|
|
||||||
PROXY;
|
PROXY;
|
||||||
|
|
|
@ -63,7 +63,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
|
||||||
|
|
||||||
$this->filesystem = $filesystem ?: new Filesystem();
|
$this->filesystem = $filesystem ?: new Filesystem();
|
||||||
$this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/');
|
$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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue