Skip to content

Commit

Permalink
Expose path to autoload in a global var for binaries (#10137)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
helhum and Seldaek committed Nov 25, 2021
1 parent dc526d3 commit f12a5b8
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 75 deletions.
4 changes: 2 additions & 2 deletions doc/04-schema.md
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions doc/06-config.md
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions doc/07-runtime.md
Expand Up @@ -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;
31 changes: 26 additions & 5 deletions doc/articles/vendor-binaries.md
Expand Up @@ -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:

Expand Down Expand Up @@ -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?

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions res/composer-schema.json
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Composer.php
Expand Up @@ -65,7 +65,7 @@ class Composer
*
* @var string
*/
const RUNTIME_API_VERSION = '2.1.0';
const RUNTIME_API_VERSION = '2.2.0';

/**
* @return string
Expand Down
8 changes: 6 additions & 2 deletions src/Composer/Config.php
Expand Up @@ -367,12 +367,16 @@ public function get($key, $flags = 0)
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':
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Factory.php
Expand Up @@ -594,7 +594,7 @@ public function createInstallationManager(Loop $loop, IOInterface $io, EventDisp
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));
Expand Down
127 changes: 68 additions & 59 deletions src/Composer/Installer/BinaryInstaller.php
Expand Up @@ -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;
}

/**
Expand All @@ -72,38 +76,37 @@ public function installBinaries(PackageInterface $package, $installPath, $warnOn
$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());
}
Expand All @@ -122,10 +125,10 @@ public function removeBinaries(PackageInterface $package)
}
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');
}
}
Expand Down Expand Up @@ -188,19 +191,6 @@ protected function installFullBinaries($binPath, $link, $bin, PackageInterface $
}
}

/**
* @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
Expand Down Expand Up @@ -233,6 +223,16 @@ protected function generateWindowsProxyCode($bin, $link)
$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".
Expand All @@ -258,25 +258,15 @@ protected function generateUnixyProxyCode($bin, $link)
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')) {
/**
Expand Down Expand Up @@ -357,6 +347,25 @@ public function stream_set_option(\$option, \$arg1, \$arg2)
}
}
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;
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Installer/LibraryInstaller.php
Expand Up @@ -63,7 +63,7 @@ public function __construct(IOInterface $io, Composer $composer, $type = 'librar

$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);
}

/**
Expand Down

0 comments on commit f12a5b8

Please sign in to comment.