Browse Source

Add a Composer\Versions class which is available in all projects at runtime to query installed packages/versions

Jordi Boggiano 4 years ago
parent
commit
0d1922dc27
29 changed files with 774 additions and 82 deletions
  1. 12 68
      src/Composer/Autoload/AutoloadGenerator.php
  2. 2 0
      src/Composer/Compiler.php
  3. 6 5
      src/Composer/Factory.php
  4. 186 0
      src/Composer/InstalledVersions.php
  5. 5 0
      src/Composer/Installer/InstallationManager.php
  6. 89 1
      src/Composer/Repository/FilesystemRepository.php
  7. 58 1
      src/Composer/Util/Filesystem.php
  8. 21 5
      tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
  9. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php
  10. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php
  11. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php
  12. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php
  13. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php
  14. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php
  15. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php
  16. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php
  17. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php
  18. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php
  19. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php
  20. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php
  21. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php
  22. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php
  23. 5 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php
  24. 1 0
      tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php
  25. 211 0
      tests/Composer/Test/InstalledVersionsTest.php
  26. 2 1
      tests/Composer/Test/Mock/FactoryMock.php
  27. 45 0
      tests/Composer/Test/Repository/FilesystemRepositoryTest.php
  28. 68 0
      tests/Composer/Test/Repository/Fixtures/installed.php
  29. 29 1
      tests/Composer/Test/TestCase.php

+ 12 - 68
src/Composer/Autoload/AutoloadGenerator.php

@@ -277,6 +277,7 @@ EOF;
             );
         }
 
+        $classMap['Composer\\InstalledVersions'] = "\$vendorDir . '/composer/InstalledVersions.php',\n";
         ksort($classMap);
         foreach ($classMap as $class => $code) {
             $classmapFile .= '    '.var_export($class, true).' => '.$code;
@@ -296,33 +297,33 @@ EOF;
             }
         }
 
-        $this->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile);
-        $this->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File);
-        $this->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile);
+        $filesystem->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile);
+        $filesystem->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File);
+        $filesystem->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile);
         $includePathFilePath = $targetDir.'/include_paths.php';
         if ($includePathFileContents = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) {
-            $this->filePutContentsIfModified($includePathFilePath, $includePathFileContents);
+            $filesystem->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)) {
-            $this->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents);
+            $filesystem->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents);
         } elseif (file_exists($includeFilesFilePath)) {
             unlink($includeFilesFilePath);
         }
-        $this->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion));
+        $filesystem->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion));
         $checkPlatform = $config->get('platform-check');
         if ($checkPlatform) {
-            $this->filePutContentsIfModified($targetDir.'/platform_check.php', $this->getPlatformCheck($packageMap));
+            $filesystem->filePutContentsIfModified($targetDir.'/platform_check.php', $this->getPlatformCheck($packageMap));
         } elseif (file_exists($targetDir.'/platform_check.php')) {
             unlink($targetDir.'/platform_check.php');
         }
-        $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, $checkPlatform));
+        $filesystem->filePutContentsIfModified($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix));
+        $filesystem->filePutContentsIfModified($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion, $checkPlatform));
 
-        $this->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php');
-        $this->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE');
+        $filesystem->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php');
+        $filesystem->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE');
 
         if ($this->runScripts) {
             $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array(
@@ -333,16 +334,6 @@ 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, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles)
     {
         foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) {
@@ -1133,51 +1124,4 @@ INITIALIZER;
 
         return $sortedPackageMap;
     }
-
-    /**
-     * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463
-     *
-     * @param string $source
-     * @param string $target
-     */
-    protected function safeCopy($source, $target)
-    {
-        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);
-
-        return $result;
-    }
 }

+ 2 - 0
src/Composer/Compiler.php

@@ -139,6 +139,8 @@ class Compiler
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_files.php'));
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_real.php'));
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_static.php'));
+        $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/installed.php'));
+        $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/InstalledVersions.php'));
         if (file_exists(__DIR__.'/../../vendor/composer/platform_check.php')) {
             $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/platform_check.php'));
         }

+ 6 - 5
src/Composer/Factory.php

@@ -17,6 +17,7 @@ use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
 use Composer\Package\Archiver;
 use Composer\Package\Version\VersionGuesser;
+use Composer\Package\RootPackageInterface;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\RepositoryFactory;
 use Composer\Repository\WritableRepositoryInterface;
@@ -344,9 +345,6 @@ class Factory
         $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
         $composer->setRepositoryManager($rm);
 
-        // load local repository
-        $this->addLocalRepository($io, $rm, $vendorDir);
-
         // force-set the version of the global package if not defined as
         // guessing it adds no value and only takes time
         if (!$fullLoad && !isset($localConfig['version'])) {
@@ -360,6 +358,9 @@ class Factory
         $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd);
         $composer->setPackage($package);
 
+        // load local repository
+        $this->addLocalRepository($io, $rm, $vendorDir, $package);
+
         // initialize installation manager
         $im = $this->createInstallationManager($loop, $io, $dispatcher);
         $composer->setInstallationManager($im);
@@ -431,9 +432,9 @@ class Factory
      * @param Repository\RepositoryManager $rm
      * @param string                       $vendorDir
      */
-    protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir)
+    protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage)
     {
-        $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io)));
+        $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io), true, $rootPackage));
     }
 
     /**

+ 186 - 0
src/Composer/InstalledVersions.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace Composer;
+
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * To require it's presence, you can require `composer-runtime-api ^2.0`
+ */
+class InstalledVersions
+{
+    private static $installed;
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        return array_keys(self::$installed['versions']);
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @return bool
+     */
+    public static function isInstalled($packageName)
+    {
+        return isset(self::$installed['versions'][$packageName]);
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param string        $packageName
+     * @param ?string       $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     *
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints($constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        if (!isset(self::$installed['versions'][$packageName])) {
+            throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+        }
+
+        $ranges = array();
+        if (isset(self::$installed['versions'][$packageName]['pretty_version'])) {
+            $ranges[] = self::$installed['versions'][$packageName]['pretty_version'];
+        }
+        if (array_key_exists('aliases', self::$installed['versions'][$packageName])) {
+            $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['aliases']);
+        }
+        if (array_key_exists('replaced', self::$installed['versions'][$packageName])) {
+            $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['replaced']);
+        }
+        if (array_key_exists('provided', self::$installed['versions'][$packageName])) {
+            $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['provided']);
+        }
+
+        return implode(' || ', $ranges);
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        if (!isset(self::$installed['versions'][$packageName])) {
+            throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+        }
+
+        if (!isset(self::$installed['versions'][$packageName]['version'])) {
+            return null;
+        }
+
+        return self::$installed['versions'][$packageName]['version'];
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        if (!isset(self::$installed['versions'][$packageName])) {
+            throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+        }
+
+        if (!isset(self::$installed['versions'][$packageName]['pretty_version'])) {
+            return null;
+        }
+
+        return self::$installed['versions'][$packageName]['pretty_version'];
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        if (!isset(self::$installed['versions'][$packageName])) {
+            throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+        }
+
+        if (!isset(self::$installed['versions'][$packageName]['reference'])) {
+            return null;
+        }
+
+        return self::$installed['versions'][$packageName]['reference'];
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}
+     */
+    public static function getRootPackage()
+    {
+        return self::$installed['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @return array[]
+     * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}, versions: list<string, array{pretty_version: ?string, version: ?string, aliases: ?string[], reference: ?string, replaced: ?string[], provided: ?string[]}>}
+     */
+    public static function getRawData()
+    {
+        return self::$installed;
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}, versions: list<string, array{pretty_version: ?string, version: ?string, aliases: ?string[], reference: ?string, replaced: ?string[], provided: ?string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+    }
+}

+ 5 - 0
src/Composer/Installer/InstallationManager.php

@@ -284,6 +284,11 @@ class InstallationManager
 
             throw $e;
         }
+
+        // do a last write so that we write the repository even if nothing changed
+        // as that can trigger an update of some files like InstalledVersions.php if
+        // running a new composer version
+        $repo->write($devMode, $this);
     }
 
     /**

+ 89 - 1
src/Composer/Repository/FilesystemRepository.php

@@ -14,6 +14,8 @@ namespace Composer\Repository;
 
 use Composer\Json\JsonFile;
 use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\RootPackageInterface;
+use Composer\Package\AliasPackage;
 use Composer\Package\Dumper\ArrayDumper;
 use Composer\Installer\InstallationManager;
 use Composer\Util\Filesystem;
@@ -27,16 +29,25 @@ use Composer\Util\Filesystem;
 class FilesystemRepository extends WritableArrayRepository
 {
     private $file;
+    private $dumpVersions;
+    private $rootPackage;
 
     /**
      * Initializes filesystem repository.
      *
      * @param JsonFile $repositoryFile repository json file
+     * @param bool $dumpVersions
+     * @param ?RootPackageInterface $rootPackage Must be provided if $dumpVersions is true
      */
-    public function __construct(JsonFile $repositoryFile)
+    public function __construct(JsonFile $repositoryFile, $dumpVersions = false, RootPackageInterface $rootPackage = null)
     {
         parent::__construct();
         $this->file = $repositoryFile;
+        $this->dumpVersions = $dumpVersions;
+        $this->rootPackage = $rootPackage;
+        if ($dumpVersions && !$rootPackage) {
+            throw new \InvalidArgumentException('Expected a root package instance if $dumpVersions is true');
+        }
     }
 
     /**
@@ -105,5 +116,82 @@ class FilesystemRepository extends WritableArrayRepository
         });
 
         $this->file->write($data);
+
+        if ($this->dumpVersions) {
+            $versions = array('versions' => array());
+            $packages = $this->getPackages();
+            $packages[] = $rootPackage = $this->rootPackage;
+            while ($rootPackage instanceof AliasPackage) {
+                $rootPackage = $rootPackage->getAliasOf();
+                $packages[] = $rootPackage;
+            }
+
+            // add real installed packages
+            foreach ($packages as $package) {
+                if ($package instanceof AliasPackage) {
+                    continue;
+                }
+
+                $reference = null;
+                if ($package->getInstallationSource()) {
+                    $reference = $package->getInstallationSource() === 'source' ? $package->getSourceReference() : $package->getDistReference();
+                }
+                if (null === $reference) {
+                    $reference = ($package->getSourceReference() ?: $package->getDistReference()) ?: null;
+                }
+
+                $versions['versions'][$package->getName()] = array(
+                    'pretty_version' => $package->getPrettyVersion(),
+                    'version' => $package->getVersion(),
+                    'aliases' => array(),
+                    'reference' => $reference,
+                );
+                if ($package instanceof RootPackageInterface) {
+                    $versions['root'] = $versions['versions'][$package->getName()];
+                    $versions['root']['name'] = $package->getName();
+                }
+            }
+
+            // add provided/replaced packages
+            foreach ($packages as $package) {
+                foreach ($package->getReplaces() as $replace) {
+                    $replaced = $replace->getPrettyConstraint();
+                    if ($replaced === 'self.version') {
+                        $replaced = $package->getPrettyVersion();
+                    }
+                    if (!isset($versions['versions'][$replace->getTarget()]['replaced']) || !in_array($replaced, $versions['versions'][$replace->getTarget()]['replaced'], true)) {
+                        $versions['versions'][$replace->getTarget()]['replaced'][] = $replaced;
+                    }
+                }
+                foreach ($package->getProvides() as $provide) {
+                    $provided = $provide->getPrettyConstraint();
+                    if ($provided === 'self.version') {
+                        $provided = $package->getPrettyVersion();
+                    }
+                    if (!isset($versions['versions'][$provide->getTarget()]['provided']) || !in_array($provided, $versions['versions'][$provide->getTarget()]['provided'], true)) {
+                        $versions['versions'][$provide->getTarget()]['provided'][] = $provided;
+                    }
+                }
+            }
+
+            // add aliases
+            foreach ($packages as $package) {
+                if (!$package instanceof AliasPackage) {
+                    continue;
+                }
+                $versions['versions'][$package->getName()]['aliases'][] = $package->getPrettyVersion();
+                if ($package instanceof RootPackageInterface) {
+                    $versions['root']['aliases'][] = $package->getPrettyVersion();
+                }
+            }
+
+            ksort($versions['versions']);
+            ksort($versions);
+
+            $fs->filePutContentsIfModified($repoDir.'/installed.php', '<?php return '.var_export($versions, true).';'."\n");
+            $installedVersionsClass = file_get_contents(__DIR__.'/../InstalledVersions.php');
+            $installedVersionsClass = str_replace('private static $installed;', 'private static $installed = '.var_export($versions, true).';', $installedVersionsClass);
+            $fs->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass);
+        }
     }
 }

+ 58 - 1
src/Composer/Util/Filesystem.php

@@ -313,7 +313,7 @@ class Filesystem
 
         if (!function_exists('proc_open')) {
             $this->copyThenRemove($source, $target);
-            
+
             return;
         }
 
@@ -721,4 +721,61 @@ class Filesystem
 
         return $this->rmdir($junction);
     }
+
+    public function filePutContentsIfModified($path, $content)
+    {
+        $currentContent = @file_get_contents($path);
+        if (!$currentContent || ($currentContent != $content)) {
+            return file_put_contents($path, $content);
+        }
+
+        return 0;
+    }
+
+    /**
+     * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463
+     *
+     * @param string $source
+     * @param string $target
+     */
+    public function safeCopy($source, $target)
+    {
+        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);
+
+        return $result;
+    }
 }

+ 21 - 5
tests/Composer/Test/Autoload/AutoloadGeneratorTest.php

@@ -452,6 +452,7 @@ class AutoloadGeneratorTest extends TestCase
         $this->assertEquals(
             array(
                 'B\\C\\C' => $this->vendorDir.'/b/b/src/C/C.php',
+                'Composer\\InstalledVersions' => $this->vendorDir . '/composer/InstalledVersions.php',
             ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
@@ -599,7 +600,9 @@ class AutoloadGeneratorTest extends TestCase
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_8');
         $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated.");
         $this->assertEquals(
-            array(),
+            array(
+                'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php',
+            ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
     }
@@ -636,6 +639,7 @@ class AutoloadGeneratorTest extends TestCase
 \$baseDir = dirname(\$vendorDir);
 
 return array(
+    'Composer\\\\InstalledVersions' => \$vendorDir . '/composer/InstalledVersions.php',
     'psr0_match' => \$baseDir . '/psr0/psr0/match.php',
     'psr4\\\\match' => \$baseDir . '/psr4/match.php',
 );
@@ -677,6 +681,7 @@ EOF;
                 'ClassMapBar' => $this->vendorDir.'/b/b/src/b.php',
                 'ClassMapBaz' => $this->vendorDir.'/b/b/lib/c.php',
                 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php',
+                'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php',
             ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
@@ -717,6 +722,7 @@ EOF;
                 'ClassMapBar' => $this->vendorDir.'/a/a/target/lib/b.php',
                 'ClassMapBaz' => $this->vendorDir.'/b/b/src/c.php',
                 'ClassMapFoo' => $this->vendorDir.'/a/a/target/src/a.php',
+                'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php',
             ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
@@ -758,6 +764,7 @@ EOF;
                 'ClassMapBar' => $this->vendorDir.'/b/b/test.php',
                 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php',
                 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php',
+                'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php',
             ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
@@ -805,6 +812,7 @@ EOF;
                 'ClassMapBar' => $this->vendorDir.'/b/b/ClassMapBar.php',
                 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/ClassMapBaz.php',
                 'ClassMapFoo' => $this->vendorDir.'/a/a/src/ClassMapFoo.php',
+                'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php',
             ),
             include $this->vendorDir.'/composer/autoload_classmap.php'
         );
@@ -852,7 +860,8 @@ EOF;
         $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions.php', $this->vendorDir.'/composer/autoload_static.php');
         $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php');
 
-        include $this->vendorDir . '/autoload.php';
+        $loader = require $this->vendorDir . '/autoload.php';
+        $loader->unregister();
         $this->assertTrue(function_exists('testFilesAutoloadGeneration1'));
         $this->assertTrue(function_exists('testFilesAutoloadGeneration2'));
         $this->assertTrue(function_exists('testFilesAutoloadGeneration3'));
@@ -988,7 +997,8 @@ EOF;
         $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_real_files_by_dependency.php', $this->vendorDir . '/composer/autoload_real.php');
         $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_static_files_by_dependency.php', $this->vendorDir . '/composer/autoload_static.php');
 
-        require $this->vendorDir . '/autoload.php';
+        $loader = require $this->vendorDir . '/autoload.php';
+        $loader->unregister();
 
         $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency1'));
         $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency2'));
@@ -1087,6 +1097,7 @@ EOF;
 
 return array(
     'A\\\\B\\\\C' => \$baseDir . '/lib/A/B/C.php',
+    'Composer\\\\InstalledVersions' => \$vendorDir . '/composer/InstalledVersions.php',
     'Foo\\\\Bar' => \$baseDir . '/src/classes.php',
 );
 
@@ -1155,7 +1166,8 @@ EOF;
 
         $oldIncludePath = get_include_path();
 
-        require $this->vendorDir."/autoload.php";
+        $loader = require $this->vendorDir."/autoload.php";
+        $loader->unregister();
 
         $this->assertEquals(
             $this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath,
@@ -1183,7 +1195,8 @@ EOF;
 
         $oldIncludePath = get_include_path();
 
-        require $this->vendorDir."/autoload.php";
+        $loader = require $this->vendorDir."/autoload.php";
+        $loader->unregister();
 
         $this->assertEquals(
             $this->workingDir."/lib".PATH_SEPARATOR.$this->workingDir."/src".PATH_SEPARATOR.$this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath,
@@ -1356,6 +1369,7 @@ $baseDir = dirname($vendorDir).'/working-dir';
 return array(
     'Bar\\Bar' => $vendorDir . '/b/b/classmaps/classes.php',
     'Bar\\Foo' => $vendorDir . '/b/b/lib/Bar/Foo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Foo\\Bar' => $baseDir . '/src/Foo/Bar.php',
     'Foo\\Foo' => $baseDir . '/classmap/classes.php',
 );
@@ -1434,6 +1448,7 @@ $vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir).'/working-dir';
 
 return array(
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Foo\\Bar' => $baseDir . '/../src/Foo/Bar.php',
     'Foo\\Foo' => $baseDir . '/../classmap/classes.php',
 );
@@ -1503,6 +1518,7 @@ $baseDir = dirname($vendorDir);
 
 return array(
     'Classmap\\Foo' => $baseDir . '/class.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Foo\\Bar' => $baseDir . '/Foo/Bar.php',
 );
 

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php

@@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
 return array(
     'Acme\\Cake\\ClassMapBar' => $baseDir . '/src-cake/ClassMapBar.php',
     'ClassMapFoo' => $baseDir . '/composersrc/foo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Lala\\ClassMapMain' => $baseDir . '/src/Lala/ClassMapMain.php',
     'Lala\\Test\\ClassMapMainTest' => $baseDir . '/src/Lala/Test/ClassMapMainTest.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php

@@ -7,4 +7,5 @@ $baseDir = dirname(dirname($vendorDir));
 
 return array(
     'ClassMapFoo' => $baseDir . '/composersrc/foo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php

@@ -7,5 +7,6 @@ $baseDir = $vendorDir;
 
 return array(
     'ClassMapFoo' => $vendorDir . '/composersrc/foo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Main\\Foo' => $vendorDir . '/src/Main/Foo.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php

@@ -9,4 +9,5 @@ return array(
     'ClassMapBar' => $vendorDir . '/b/b/src/b.php',
     'ClassMapBaz' => $vendorDir . '/b/b/lib/c.php',
     'ClassMapFoo' => $vendorDir . '/a/a/src/a.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php

@@ -9,4 +9,5 @@ return array(
     'ClassMapBar' => $vendorDir . '/b/b/test.php',
     'ClassMapBaz' => $vendorDir . '/c/c/foo/test.php',
     'ClassMapFoo' => $vendorDir . '/a/a/src/a.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php

@@ -8,4 +8,5 @@ $baseDir = dirname($vendorDir);
 return array(
     'ClassMapBar' => $baseDir . '/lib/rootbar.php',
     'ClassMapFoo' => $baseDir . '/src/rootfoo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php

@@ -6,5 +6,6 @@ $vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);
 
 return array(
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Main\\ClassMain' => $baseDir . '/src/Main/ClassMain.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php

@@ -9,4 +9,5 @@ return array(
     'ClassMapBar' => $vendorDir . '/b/b/ClassMapBar.php',
     'ClassMapBaz' => $vendorDir . '/c/c/foo/ClassMapBaz.php',
     'ClassMapFoo' => $vendorDir . '/a/a/src/ClassMapFoo.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
 );

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php

@@ -8,5 +8,6 @@ $baseDir = dirname($vendorDir);
 return array(
     'A' => $vendorDir . '/a/a/src/A.php',
     'C' => $vendorDir . '/c/c/src/C.php',
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'D' => $vendorDir . '/d/d/src/D.php',
 );

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php

@@ -75,12 +75,17 @@ class ComposerStaticInitPhar
         ),
     );
 
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
             $loader->prefixLengthsPsr4 = ComposerStaticInitPhar::$prefixLengthsPsr4;
             $loader->prefixDirsPsr4 = ComposerStaticInitPhar::$prefixDirsPsr4;
             $loader->prefixesPsr0 = ComposerStaticInitPhar::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitPhar::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php

@@ -15,9 +15,14 @@ class ComposerStaticInitFilesAutoloadOrder
         '334307692417e52db5a08c3271700a7e' => __DIR__ . '/../..' . '/root2.php',
     );
 
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
+            $loader->classMap = ComposerStaticInitFilesAutoloadOrder::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php

@@ -14,9 +14,14 @@ class ComposerStaticInitFilesAutoload
         '61b776fd0ee84fb7d7d958ae46118ded' => __DIR__ . '/../..' . '/root.php',
     );
 
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
+            $loader->classMap = ComposerStaticInitFilesAutoload::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php

@@ -14,9 +14,14 @@ class ComposerStaticInitFilesAutoload
         '61b776fd0ee84fb7d7d958ae46118ded' => __DIR__ . '/../..' . '/root.php',
     );
 
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
+            $loader->classMap = ComposerStaticInitFilesAutoload::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php

@@ -6,9 +6,14 @@ namespace Composer\Autoload;
 
 class ComposerStaticInitFilesAutoload
 {
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
+            $loader->classMap = ComposerStaticInitFilesAutoload::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php

@@ -20,10 +20,15 @@ class ComposerStaticInitIncludePath
         ),
     );
 
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
             $loader->prefixesPsr0 = ComposerStaticInitIncludePath::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitIncludePath::$classMap;
 
         }, null, ClassLoader::class);
     }

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php

@@ -28,6 +28,7 @@ class ComposerStaticInitTargetDir
     public static $classMap = array (
         'ClassMapBar' => __DIR__ . '/../..' . '/lib/rootbar.php',
         'ClassMapFoo' => __DIR__ . '/../..' . '/src/rootfoo.php',
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
     );
 
     public static function getInitializer(ClassLoader $loader)

+ 211 - 0
tests/Composer/Test/InstalledVersionsTest.php

@@ -0,0 +1,211 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test;
+
+use Composer\Test\TestCase;
+use Composer\InstalledVersions;
+use Composer\Semver\VersionParser;
+
+class InstalledVersionsTest extends TestCase
+{
+    public function setUp()
+    {
+        InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php');
+    }
+
+    public function testGetInstalledPackages()
+    {
+        $names = array(
+            '__root__',
+            'a/provider',
+            'a/provider2',
+            'b/replacer',
+            'c/c',
+            'foo/impl',
+            'foo/impl2',
+            'foo/replaced',
+        );
+        $this->assertSame($names, InstalledVersions::getInstalledPackages());
+    }
+
+    /**
+     * @dataProvider isInstalledProvider
+     */
+    public function testIsInstalled($expected, $name, $constraint = null)
+    {
+        $this->assertSame($expected, InstalledVersions::isInstalled($name));
+    }
+
+    public static function isInstalledProvider()
+    {
+        return array(
+            array(true,  'foo/impl'),
+            array(true,  'foo/replaced'),
+            array(true,  'c/c'),
+            array(true,  '__root__'),
+            array(true,  'b/replacer'),
+            array(false, 'not/there'),
+            array(false, 'not/there', '^1.0'),
+        );
+    }
+
+    /**
+     * @dataProvider satisfiesProvider
+     */
+    public function testSatisfies($expected, $name, $constraint)
+    {
+        $this->assertSame($expected, InstalledVersions::satisfies(new VersionParser, $name, $constraint));
+    }
+
+    public static function satisfiesProvider()
+    {
+        return array(
+            array(true,  'foo/impl', '1.5'),
+            array(true,  'foo/impl', '1.2'),
+            array(true,  'foo/impl', '^1.0'),
+            array(true,  'foo/impl', '^3 || ^2'),
+            array(false, 'foo/impl', '^3'),
+
+            array(true,  'foo/replaced', '3.5'),
+            array(true,  'foo/replaced', '^3.2'),
+            array(false,  'foo/replaced', '4.0'),
+
+            array(true,  'c/c', '3.0.0'),
+            array(true,  'c/c', '^3'),
+            array(false, 'c/c', '^3.1'),
+
+            array(true,  '__root__', 'dev-master'),
+            array(true,  '__root__', '^1.10'),
+            array(false, '__root__', '^2'),
+
+            array(true,  'b/replacer', '^2.1'),
+            array(false, 'b/replacer', '^2.3'),
+
+            array(true,  'a/provider2', '^1.2'),
+            array(true,  'a/provider2', '^1.4'),
+            array(false, 'a/provider2', '^1.5'),
+        );
+    }
+
+    /**
+     * @dataProvider getVersionRangesProvider
+     */
+    public function testGetVersionRanges($expected, $name)
+    {
+        $this->assertSame($expected, InstalledVersions::getVersionRanges($name));
+    }
+
+    public static function getVersionRangesProvider()
+    {
+        return array(
+            array('dev-master || 1.10.x-dev',   '__root__'),
+            array('^1.1 || 1.2 || 1.4 || 2.0',  'foo/impl'),
+            array('2.2 || 2.0',                 'foo/impl2'),
+            array('^3.0',                       'foo/replaced'),
+            array('1.1',                        'a/provider'),
+            array('1.2 || 1.4',                 'a/provider2'),
+            array('2.2',                        'b/replacer'),
+            array('3.0',                        'c/c'),
+        );
+    }
+
+    /**
+     * @dataProvider getVersionProvider
+     */
+    public function testGetVersion($expected, $name)
+    {
+        $this->assertSame($expected, InstalledVersions::getVersion($name));
+    }
+
+    public static function getVersionProvider()
+    {
+        return array(
+            array('dev-master',  '__root__'),
+            array(null, 'foo/impl'),
+            array(null, 'foo/impl2'),
+            array(null, 'foo/replaced'),
+            array('1.1.0.0', 'a/provider'),
+            array('1.2.0.0', 'a/provider2'),
+            array('2.2.0.0', 'b/replacer'),
+            array('3.0.0.0', 'c/c'),
+        );
+    }
+
+    /**
+     * @dataProvider getPrettyVersionProvider
+     */
+    public function testGetPrettyVersion($expected, $name)
+    {
+        $this->assertSame($expected, InstalledVersions::getPrettyVersion($name));
+    }
+
+    public static function getPrettyVersionProvider()
+    {
+        return array(
+            array('dev-master',  '__root__'),
+            array(null, 'foo/impl'),
+            array(null, 'foo/impl2'),
+            array(null, 'foo/replaced'),
+            array('1.1', 'a/provider'),
+            array('1.2', 'a/provider2'),
+            array('2.2', 'b/replacer'),
+            array('3.0', 'c/c'),
+        );
+    }
+
+    public function testGetVersionOutOfBounds()
+    {
+        $this->setExpectedException('OutOfBoundsException');
+        InstalledVersions::getVersion('not/installed');
+    }
+
+    public function testGetRootPackage()
+    {
+        $this->assertSame(array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'aliases' => array(
+                '1.10.x-dev',
+            ),
+            'reference' => 'sourceref-by-default',
+            'name' => '__root__',
+        ), InstalledVersions::getRootPackage());
+    }
+
+    public function testGetRawData()
+    {
+        $this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData());
+    }
+
+    /**
+     * @dataProvider getReferenceProvider
+     */
+    public function testGetReference($expected, $name)
+    {
+        $this->assertSame($expected, InstalledVersions::getReference($name));
+    }
+
+    public static function getReferenceProvider()
+    {
+        return array(
+            array('sourceref-by-default',  '__root__'),
+            array(null, 'foo/impl'),
+            array(null, 'foo/impl2'),
+            array(null, 'foo/replaced'),
+            array('distref-as-no-source', 'a/provider'),
+            array('distref-as-installed-from-dist', 'a/provider2'),
+            array(null, 'b/replacer'),
+            array(null, 'c/c'),
+        );
+    }
+}

+ 2 - 1
tests/Composer/Test/Mock/FactoryMock.php

@@ -17,6 +17,7 @@ use Composer\Config;
 use Composer\Factory;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\WritableRepositoryInterface;
+use Composer\Package\RootPackageInterface;
 use Composer\Installer;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\IO\IOInterface;
@@ -37,7 +38,7 @@ class FactoryMock extends Factory
         return $config;
     }
 
-    protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir)
+    protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage)
     {
     }
 

+ 45 - 0
tests/Composer/Test/Repository/FilesystemRepositoryTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test\Repository;
 
 use Composer\Repository\FilesystemRepository;
 use Composer\Test\TestCase;
+use Composer\Json\JsonFile;
 
 class FilesystemRepositoryTest extends TestCase
 {
@@ -113,6 +114,50 @@ class FilesystemRepositoryTest extends TestCase
         $repository->write(true, $im);
     }
 
+    public function testRepositoryWritesInstalledPhp()
+    {
+        $dir = $this->getUniqueTmpDirectory();
+        $json = new JsonFile($dir.'/installed.json');
+
+        $rootPackage = $this->getPackage('__root__', 'dev-master', 'Composer\Package\RootPackage');
+        $rootPackage->setSourceReference('sourceref-by-default');
+        $rootPackage->setDistReference('distref');
+        $this->configureLinks($rootPackage, array('provide' => array('foo/impl' => '2.0')));
+        $rootPackage = $this->getAliasPackage($rootPackage, '1.10.x-dev');
+
+        $repository = new FilesystemRepository($json, true, $rootPackage);
+        $pkg = $this->getPackage('a/provider', '1.1');
+        $this->configureLinks($pkg, array('provide' => array('foo/impl' => '^1.1', 'foo/impl2' => '2.0')));
+        $pkg->setDistReference('distref-as-no-source');
+        $repository->addPackage($pkg);
+
+        $pkg = $this->getPackage('a/provider2', '1.2');
+        $this->configureLinks($pkg, array('provide' => array('foo/impl' => 'self.version', 'foo/impl2' => '2.0')));
+        $pkg->setSourceReference('sourceref');
+        $pkg->setDistReference('distref-as-installed-from-dist');
+        $pkg->setInstallationSource('dist');
+        $repository->addPackage($pkg);
+
+        $repository->addPackage($this->getAliasPackage($pkg, '1.4'));
+
+        $pkg = $this->getPackage('b/replacer', '2.2');
+        $this->configureLinks($pkg, array('replace' => array('foo/impl2' => 'self.version', 'foo/replaced' => '^3.0')));
+        $repository->addPackage($pkg);
+
+        $pkg = $this->getPackage('c/c', '3.0');
+        $repository->addPackage($pkg);
+
+        $im = $this->getMockBuilder('Composer\Installer\InstallationManager')
+            ->disableOriginalConstructor()
+            ->getMock();
+        $im->expects($this->any())
+            ->method('getInstallPath')
+            ->will($this->returnValue('/foo/bar/vendor/woop/woop'));
+
+        $repository->write(true, $im);
+        $this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php');
+    }
+
     private function createJsonFileMock()
     {
         return $this->getMockBuilder('Composer\Json\JsonFile')

+ 68 - 0
tests/Composer/Test/Repository/Fixtures/installed.php

@@ -0,0 +1,68 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'aliases' => array(
+            '1.10.x-dev',
+        ),
+        'reference' => 'sourceref-by-default',
+        'name' => '__root__',
+    ),
+    'versions' => array(
+        '__root__' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'aliases' => array(
+                '1.10.x-dev',
+            ),
+            'reference' => 'sourceref-by-default',
+        ),
+        'a/provider' => array(
+            'pretty_version' => '1.1',
+            'version' => '1.1.0.0',
+            'aliases' => array(),
+            'reference' => 'distref-as-no-source',
+        ),
+        'a/provider2' => array(
+            'pretty_version' => '1.2',
+            'version' => '1.2.0.0',
+            'aliases' => array(
+              '1.4',
+            ),
+            'reference' => 'distref-as-installed-from-dist',
+        ),
+        'b/replacer' => array(
+            'pretty_version' => '2.2',
+            'version' => '2.2.0.0',
+            'aliases' => array(),
+            'reference' => NULL,
+        ),
+        'c/c' => array(
+            'pretty_version' => '3.0',
+            'version' => '3.0.0.0',
+            'aliases' => array(),
+            'reference' => NULL,
+        ),
+        'foo/impl' => array(
+            'provided' => array(
+                '^1.1',
+                '1.2',
+                '1.4',
+                '2.0',
+            )
+        ),
+        'foo/impl2' => array(
+            'provided' => array(
+                '2.0',
+            ),
+            'replaced' => array(
+                '2.2',
+            ),
+        ),
+        'foo/replaced' => array(
+            'replaced' => array(
+                '^3.0',
+            ),
+        ),
+    ),
+);

+ 29 - 1
tests/Composer/Test/TestCase.php

@@ -14,11 +14,15 @@ namespace Composer\Test;
 
 use Composer\Semver\VersionParser;
 use Composer\Package\AliasPackage;
+use Composer\Package\RootPackageInterface;
+use Composer\Package\PackageInterface;
 use Composer\Semver\Constraint\Constraint;
 use Composer\Util\Filesystem;
 use Composer\Util\Silencer;
 use PHPUnit\Framework\TestCase as BaseTestCase;
 use Symfony\Component\Process\ExecutableFinder;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\BasePackage;
 
 abstract class TestCase extends BaseTestCase
 {
@@ -73,7 +77,31 @@ abstract class TestCase extends BaseTestCase
     {
         $normVersion = self::getVersionParser()->normalize($version);
 
-        return new AliasPackage($package, $normVersion, $version);
+        $class = 'Composer\Package\AliasPackage';
+        if ($package instanceof RootPackageInterface) {
+            $class = 'Composer\Package\RootAliasPackage';
+        }
+
+        return new $class($package, $normVersion, $version);
+    }
+
+    protected function configureLinks(PackageInterface $package, array $config)
+    {
+        $arrayLoader = new ArrayLoader();
+
+        foreach (BasePackage::$supportedLinkTypes as $type => $opts) {
+            if (isset($config[$type])) {
+                $method = 'set'.ucfirst($opts['method']);
+                $package->{$method}(
+                    $arrayLoader->parseLinks(
+                        $package->getName(),
+                        $package->getPrettyVersion(),
+                        $opts['description'],
+                        $config[$type]
+                    )
+                );
+            }
+        }
     }
 
     protected static function ensureDirectoryExistsAndClear($directory)