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);
}
/**