Browse Source

Merge branch 'master' into 2.0

Jordi Boggiano 5 years ago
parent
commit
8584d15e8c

+ 27 - 0
CHANGELOG.md

@@ -1,3 +1,28 @@
+### [1.10.0] 2020-01-XX
+
+  * Breaking: `composer global exec ...` now executes the process in the current working directory instead of executing it in the global directory.
+  * Warning: Added a warning when class names are being loaded by a PSR-4 or PSR-0 rule only due to classmap optimization, but would not otherwise be autoloadable. The next minor version will stop autoloading these classes so make sure you fix your autoload configs.
+  * Added support for configuring suggestions using config command, e.g. `composer config suggest.foo/bar some text`
+  * Added support for configuring fine-grained preferred-install using config command, e.g. `composer config preferred-install.foo/* dist`
+  * Added `@putenv` script handler to set environment variables from composer.json for following scripts
+  * Added `lock` option that can be set to false, in which case no composer.lock file will be generated
+  * Added support for IPv6 addresses in NO_PROXY
+  * Added package homepage display in the show command
+  * Added debug info about HTTP authentications
+  * Added Symfony 5 compatibility
+  * Added --fixed flag to require command to make it use a fixed constraint instead of a ^x.y constraint when adding the requirement
+  * Fixed GitHub deprecation of access_token query parameter, now using Authorization header
+  * Fixed archive command to persist file permissions inside the zip files
+  * Fixed init/require command to avoid suggesting packages which are already selected in the search results
+  * Fixed create-project UX issues
+
+### [1.9.2] 2020-01-14
+
+  * Fixed minor git driver bugs
+  * Fixed schema validation for version field to allow dev-* versions too
+  * Fixed external processes' output being formatted even though it should not
+  * Fixed issue with path repositories when trying to install feature branches
+
 ### [1.9.1] 2019-11-01
 
   * Fixed various credential handling issues with gitlab and github
@@ -776,6 +801,8 @@
 
   * Initial release
 
+[1.10.0]: https://github.com/composer/composer/compare/1.9.2...1.10.0
+[1.9.2]: https://github.com/composer/composer/compare/1.9.1...1.9.2
 [1.9.1]: https://github.com/composer/composer/compare/1.9.0...1.9.1
 [1.9.0]: https://github.com/composer/composer/compare/1.8.6...1.9.0
 [1.8.6]: https://github.com/composer/composer/compare/1.8.5...1.8.6

+ 54 - 14
src/Composer/Autoload/AutoloadGenerator.php

@@ -285,24 +285,24 @@ EOF;
             }
         }
 
-        file_put_contents($targetDir.'/autoload_namespaces.php', $namespacesFile);
-        file_put_contents($targetDir.'/autoload_psr4.php', $psr4File);
-        file_put_contents($targetDir.'/autoload_classmap.php', $classmapFile);
+        $this->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile);
+        $this->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File);
+        $this->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile);
         $includePathFilePath = $targetDir.'/include_paths.php';
         if ($includePathFileContents = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) {
-            file_put_contents($includePathFilePath, $includePathFileContents);
+            $this->filePutContentsIfModified($includePathFilePath, $includePathFileContents);
         } elseif (file_exists($includePathFilePath)) {
             unlink($includePathFilePath);
         }
         $includeFilesFilePath = $targetDir.'/autoload_files.php';
         if ($includeFilesFileContents = $this->getIncludeFilesFile($autoloads['files'], $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) {
-            file_put_contents($includeFilesFilePath, $includeFilesFileContents);
+            $this->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents);
         } elseif (file_exists($includeFilesFilePath)) {
             unlink($includeFilesFilePath);
         }
-        file_put_contents($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion));
-        file_put_contents($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix));
-        file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion));
+        $this->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion));
+        $this->filePutContentsIfModified($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix));
+        $this->filePutContentsIfModified($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion));
 
         $this->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php');
         $this->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE');
@@ -316,6 +316,16 @@ EOF;
         return count($classMap);
     }
 
+    private function filePutContentsIfModified($path, $content)
+    {
+        $currentContent = @file_get_contents($path);
+        if (!$currentContent || ($currentContent != $content)) {
+            return file_put_contents($path, $content);
+        }
+
+        return 0;
+    }
+
     private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist = null, $namespaceFilter = null, $autoloadType = null, array $classMap = array())
     {
         foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType) as $class => $path) {
@@ -986,7 +996,6 @@ INITIALIZER;
 
         $sortedPackages = PackageSorter::sortPackages($packages);
 
-
         $sortedPackageMap = array();
 
         foreach ($sortedPackages as $package) {
@@ -1005,11 +1014,42 @@ INITIALIZER;
      */
     protected function safeCopy($source, $target)
     {
-        $source = fopen($source, 'r');
-        $target = fopen($target, 'w+');
+        if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) {
+            $source = fopen($source, 'r');
+            $target = fopen($target, 'w+');
+
+            stream_copy_to_stream($source, $target);
+            fclose($source);
+            fclose($target);
+        }
+    }
+
+    /**
+     * compare 2 files
+     * https://stackoverflow.com/questions/3060125/can-i-use-file-get-contents-to-compare-two-files
+     */
+    private function filesAreEqual($a, $b)
+    {
+        // Check if filesize is different
+        if (filesize($a) !== filesize($b)) {
+            return false;
+        }
+
+        // Check if content is different
+        $ah = fopen($a, 'rb');
+        $bh = fopen($b, 'rb');
+
+        $result = true;
+        while (!feof($ah)) {
+            if (fread($ah, 8192) != fread($bh, 8192)) {
+                $result = false;
+                break;
+            }
+        }
+
+        fclose($ah);
+        fclose($bh);
 
-        stream_copy_to_stream($source, $target);
-        fclose($source);
-        fclose($target);
+        return $result;
     }
 }

+ 3 - 2
src/Composer/Command/InitCommand.php

@@ -15,6 +15,7 @@ namespace Composer\Command;
 use Composer\Factory;
 use Composer\Json\JsonFile;
 use Composer\Package\BasePackage;
+use Composer\Package\Package;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionSelector;
 use Composer\Repository\CompositeRepository;
@@ -702,13 +703,13 @@ EOT
     private function getMinimumStability(InputInterface $input)
     {
         if ($input->hasOption('stability')) {
-            return $input->getOption('stability') ?: 'stable';
+            return VersionParser::normalizeStability($input->getOption('stability') ?: 'stable');
         }
 
         $file = Factory::getComposerFile();
         if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) {
             if (!empty($composer['minimum-stability'])) {
-                return $composer['minimum-stability'];
+                return VersionParser::normalizeStability($composer['minimum-stability']);
             }
         }
 

+ 196 - 11
src/Composer/Command/ShowCommand.php

@@ -201,6 +201,9 @@ EOT
                 if (empty($package)) {
                     $options = $input->getOptions();
                     if (!isset($options['working-dir']) || !file_exists('composer.json')) {
+                        if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $input->getArgument('package')) && !$input->getOption('platform')) {
+                            throw new \InvalidArgumentException('Package ' . $packageFilter . ' not found, try using --platform (-p) to show platform packages.');
+                        }
                         throw new \InvalidArgumentException('Package ' . $packageFilter . ' not found');
                     }
 
@@ -235,18 +238,12 @@ EOT
 
                     return $exitCode;
                 }
-                $this->printMeta($package, $versions, $installedRepo, $latestPackage ?: null);
-                $this->printLinks($package, 'requires');
-                $this->printLinks($package, 'devRequires', 'requires (dev)');
-                if ($package->getSuggests()) {
-                    $io->write("\n<info>suggests</info>");
-                    foreach ($package->getSuggests() as $suggested => $reason) {
-                        $io->write($suggested . ' <comment>' . $reason . '</comment>');
-                    }
+
+                if ('json' === $format) {
+                    $this->printPackageInfoAsJson($package, $versions, $installedRepo, $latestPackage ?: null);
+                } else {
+                    $this->printPackageInfo($package, $versions, $installedRepo, $latestPackage ?: null);
                 }
-                $this->printLinks($package, 'provides');
-                $this->printLinks($package, 'conflicts');
-                $this->printLinks($package, 'replaces');
             }
 
             return $exitCode;
@@ -570,12 +567,41 @@ EOT
         return array($matchedPackage, $versions);
     }
 
+    /**
+     * Prints package info.
+     *
+     * @param CompletePackageInterface $package
+     * @param array                    $versions
+     * @param RepositoryInterface      $installedRepo
+     * @param PackageInterface|null    $latestPackage
+     */
+    protected function printPackageInfo(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
+    {
+        $io = $this->getIO();
+
+        $this->printMeta($package, $versions, $installedRepo, $latestPackage ?: null);
+        $this->printLinks($package, 'requires');
+        $this->printLinks($package, 'devRequires', 'requires (dev)');
+
+        if ($package->getSuggests()) {
+            $io->write("\n<info>suggests</info>");
+            foreach ($package->getSuggests() as $suggested => $reason) {
+                $io->write($suggested . ' <comment>' . $reason . '</comment>');
+            }
+        }
+
+        $this->printLinks($package, 'provides');
+        $this->printLinks($package, 'conflicts');
+        $this->printLinks($package, 'replaces');
+    }
+
     /**
      * Prints package metadata.
      *
      * @param CompletePackageInterface $package
      * @param array                    $versions
      * @param RepositoryInterface      $installedRepo
+     * @param PackageInterface|null    $latestPackage
      */
     protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
     {
@@ -717,6 +743,165 @@ EOT
         }
     }
 
+    /**
+     * Prints package info in JSON format.
+     *
+     * @param CompletePackageInterface $package
+     * @param array                    $versions
+     * @param RepositoryInterface      $installedRepo
+     * @param PackageInterface|null    $latestPackage
+     */
+    protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
+    {
+        $json = array(
+            'name' => $package->getPrettyName(),
+            'description' => $package->getDescription(),
+            'keywords' => $package->getKeywords() ?: array(),
+            'type' => $package->getType(),
+            'homepage' => $package->getHomepage(),
+            'names' => $package->getNames()
+        );
+
+        $json = $this->appendVersions($json, $versions);
+        $json = $this->appendLicenses($json, $package);
+
+        if ($latestPackage) {
+            $json['latest'] = $latestPackage->getPrettyVersion();
+        } else {
+            $latestPackage = $package;
+        }
+
+        if ($package->getSourceType()) {
+            $json['source'] = array(
+                'type' => $package->getSourceType(),
+                'url' => $package->getSourceUrl(),
+                'reference' => $package->getSourceReference()
+            );
+        }
+
+        if ($package->getDistType()) {
+            $json['dist'] = array(
+                'type' => $package->getDistType(),
+                'url' => $package->getDistUrl(),
+                'reference' => $package->getDistReference()
+            );
+        }
+
+        if ($installedRepo->hasPackage($package)) {
+            $json['path'] = realpath($this->getComposer()->getInstallationManager()->getInstallPath($package));
+            if ($json['path'] === false) {
+                unset($json['path']);
+            }
+        }
+
+        if ($latestPackage->isAbandoned()) {
+            $json['replacement'] = $latestPackage->getReplacementPackage();
+        }
+
+        if ($package->getSuggests()) {
+            $json['suggests'] = $package->getSuggests();
+        }
+
+        if ($package->getSupport()) {
+            $json['support'] = $package->getSupport();
+        }
+
+        $json = $this->appendAutoload($json, $package);
+
+        if ($package->getIncludePaths()) {
+            $json['include_path'] = $package->getIncludePaths();
+        }
+
+        $json = $this->appendLinks($json, $package);
+
+        $this->getIO()->write(JsonFile::encode($json));
+    }
+
+    private function appendVersions($json, array $versions)
+    {
+        uasort($versions, 'version_compare');
+        $versions = array_keys(array_reverse($versions));
+        $json['versions'] = $versions;
+
+        return $json;
+    }
+
+    private function appendLicenses($json, CompletePackageInterface $package)
+    {
+        if ($licenses = $package->getLicense()) {
+            $spdxLicenses = new SpdxLicenses();
+
+            $json['licenses'] = array_map(function ($licenseId) use ($spdxLicenses) {
+                $license = $spdxLicenses->getLicenseByIdentifier($licenseId); // keys: 0 fullname, 1 osi, 2 url
+
+                if (!$license) {
+                    return $licenseId;
+                }
+
+                return array(
+                    'name' => $license[0],
+                    'osi' => $licenseId,
+                    'url' => $license[2]
+                );
+            }, $licenses);
+        }
+
+        return $json;
+    }
+
+    private function appendAutoload($json, CompletePackageInterface $package)
+    {
+        if ($package->getAutoload()) {
+            $autoload = array();
+
+            foreach ($package->getAutoload() as $type => $autoloads) {
+                if ($type === 'psr-0' || $type === 'psr-4') {
+                    $psr = array();
+
+                    foreach ($autoloads as $name => $path) {
+                        if (!$path) {
+                            $path = '.';
+                        }
+
+                        $psr[$name ?: '*'] = $path;
+                    }
+
+                    $autoload[$type] = $psr;
+                } elseif ($type === 'classmap') {
+                    $autoload['classmap'] = $autoloads;
+                }
+            }
+
+            $json['autoload'] = $autoload;
+        }
+
+        return $json;
+    }
+
+    private function appendLinks($json, CompletePackageInterface $package)
+    {
+        foreach (array('requires', 'devRequires', 'provides', 'conflicts', 'replaces') as $linkType) {
+            $json = $this->appendLink($json, $package, $linkType);
+        }
+
+        return $json;
+    }
+
+    private function appendLink($json, CompletePackageInterface $package, $linkType)
+    {
+        $links = $package->{'get' . ucfirst($linkType)}();
+
+        if ($links) {
+            $json[$linkType] = array();
+
+            foreach ($links as $link) {
+                $json[$linkType][$link->getTarget()] = $link->getPrettyConstraint();
+            }
+        }
+
+        return $json;
+    }
+
     /**
      * Init styles for tree
      *

+ 21 - 15
src/Composer/EventDispatcher/EventDispatcher.php

@@ -143,27 +143,15 @@ class EventDispatcher
      */
     protected function doDispatch(Event $event)
     {
-        $pathStr = 'PATH';
-        if (!isset($_SERVER[$pathStr]) && isset($_SERVER['Path'])) {
-            $pathStr = 'Path';
-        }
-
-        // add the bin dir to the PATH to make local binaries of deps usable in scripts
-        $binDir = $this->composer->getConfig()->get('bin-dir');
-        if (is_dir($binDir)) {
-            $binDir = realpath($binDir);
-            if (isset($_SERVER[$pathStr]) && !preg_match('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $_SERVER[$pathStr])) {
-                $_SERVER[$pathStr] = $binDir.PATH_SEPARATOR.getenv($pathStr);
-                putenv($pathStr.'='.$_SERVER[$pathStr]);
-            }
-        }
-
         $listeners = $this->getListeners($event);
 
         $this->pushEvent($event);
 
         $return = 0;
         foreach ($listeners as $callable) {
+
+            $this->ensureBinDirIsInPath();
+
             if (!is_string($callable)) {
                 if (!is_callable($callable)) {
                     $className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0];
@@ -525,4 +513,22 @@ class EventDispatcher
     {
         return array_pop($this->eventStack);
     }
+
+    private function ensureBinDirIsInPath()
+    {
+        $pathStr = 'PATH';
+        if (!isset($_SERVER[$pathStr]) && isset($_SERVER['Path'])) {
+            $pathStr = 'Path';
+        }
+
+        // add the bin dir to the PATH to make local binaries of deps usable in scripts
+        $binDir = $this->composer->getConfig()->get('bin-dir');
+        if (is_dir($binDir)) {
+            $binDir = realpath($binDir);
+            if (isset($_SERVER[$pathStr]) && !preg_match('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $_SERVER[$pathStr])) {
+                $_SERVER[$pathStr] = $binDir.PATH_SEPARATOR.getenv($pathStr);
+                putenv($pathStr.'='.$_SERVER[$pathStr]);
+            }
+        }
+    }
 }

+ 10 - 7
src/Composer/Installer.php

@@ -302,13 +302,6 @@ class Installer
             foreach ($localRepo->getPackages() as $package) {
                 $this->installationManager->ensureBinariesPresence($package);
             }
-
-            $vendorDir = $this->config->get('vendor-dir');
-            if (is_dir($vendorDir)) {
-                // suppress errors as this fails sometimes on OSX for no apparent reason
-                // see https://github.com/composer/composer/issues/4070#issuecomment-129792748
-                @touch($vendorDir);
-            }
         }
 
         if ($this->runScripts) {
@@ -491,6 +484,16 @@ class Installer
             $this->io->writeError('<info>Writing lock file</info>');
         }
 
+        // see https://github.com/composer/composer/issues/2764
+        if ($this->executeOperations && count($lockTransaction->getOperations()) > 0) {
+            $vendorDir = $this->config->get('vendor-dir');
+            if (is_dir($vendorDir)) {
+                // suppress errors as this fails sometimes on OSX for no apparent reason
+                // see https://github.com/composer/composer/issues/4070#issuecomment-129792748
+                @touch($vendorDir);
+            }
+        }
+
         if ($doInstall) {
             // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false
             return $this->doInstall($localRepo, true);

+ 14 - 1
src/Composer/Json/JsonFile.php

@@ -129,7 +129,7 @@ class JsonFile
         $retries = 3;
         while ($retries--) {
             try {
-                file_put_contents($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : ''));
+                $this->filePutContentsIfModified($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : ''));
                 break;
             } catch (\Exception $e) {
                 if ($retries) {
@@ -142,6 +142,19 @@ class JsonFile
         }
     }
 
+    /**
+     * modify file properties only if content modified
+     */
+    private function filePutContentsIfModified($path, $content)
+    {
+        $currentContent = @file_get_contents($path);
+        if (!$currentContent || ($currentContent != $content)) {
+            return file_put_contents($path, $content);
+        }
+
+        return 0;
+    }
+
     /**
      * Validates the schema of the current json file according to composer-schema.json rules
      *

+ 54 - 0
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -280,6 +280,60 @@ class EventDispatcherTest extends TestCase
         $this->assertEquals($expected, $io->getOutput());
     }
 
+    public function testDispatcherAppendsDirBinOnPathForEveryListener()
+    {
+        $currentDirectoryBkp = getcwd();
+        $composerBinDirBkp   = getenv('COMPOSER_BIN_DIR');
+        chdir(__DIR__);
+        putenv('COMPOSER_BIN_DIR=' . __DIR__ . sprintf('%svendor%sbin', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR));
+
+        $process    = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->setConstructorArgs(array(
+                $this->createComposerInstance(),
+                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
+                $process,
+            ))->setMethods(array(
+                'getListeners',
+            ))->getMock();
+
+        $listeners = array(
+            'Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvDoesNotContainsBin',
+            'Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvContainsBin',
+        );
+
+        $dispatcher->expects($this->atLeastOnce())->method('getListeners')->will($this->returnValue($listeners));
+
+        $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
+        rmdir(__DIR__ . sprintf('%svendor%sbin', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR));
+        rmdir(__DIR__ . sprintf('%svendor', DIRECTORY_SEPARATOR));
+
+        chdir($currentDirectoryBkp);
+        putenv('COMPOSER_BIN_DIR=' . $composerBinDirBkp);
+    }
+
+    static public function createsVendorBinFolderChecksEnvDoesNotContainsBin()
+    {
+        mkdir(__DIR__ . sprintf('%svendor%sbin', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), 0700, true);
+        $val = getenv('PATH');
+
+        if ( ! $val ) {
+            $val = getenv('Path');
+        }
+
+        self::assertFalse(strpos($val, __DIR__ . sprintf('%svendor%sbin', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR)));
+    }
+
+    static public function createsVendorBinFolderChecksEnvContainsBin()
+    {
+        $val = getenv('PATH');
+
+        if ( ! $val ) {
+            $val = getenv('Path');
+        }
+
+        self::assertNotFalse(strpos($val, __DIR__ . sprintf('%svendor%sbin', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR)));
+    }
+
     static public function getTestEnv() {
         $val = getenv('ABC');
         if ($val !== '123') {