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