diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index dbf41ceb9..28ea81028 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -294,6 +294,16 @@ EOT $output->writeln('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); $output->writeln('names : ' . implode(', ', $package->getNames())); + if ($package->isAbandoned()) { + $replacement = ($package->getReplacementPackage() !== null) + ? ' The author suggests using the ' . $package->getReplacementPackage(). ' package instead.' + : null; + + $output->writeln( + sprintf('Attention: This package is abandoned and no longer maintained.%s', $replacement) + ); + } + if ($package->getSupport()) { $output->writeln("\nsupport"); foreach ($package->getSupport() as $type => $value) { diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 294ab0a0a..b76155a5a 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -31,6 +31,7 @@ use Composer\Installer\NoopInstaller; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\AliasPackage; +use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Locker; @@ -240,6 +241,25 @@ class Installer } } + # Find abandoned packages and warn user + foreach ($localRepo->getPackages() as $package) { + if (!$package instanceof CompletePackage || !$package->isAbandoned()) { + continue; + } + + $replacement = (is_string($package->getReplacementPackage())) + ? 'Use ' . $package->getReplacementPackage() . ' instead' + : 'No replacement was suggested'; + + $this->io->write( + sprintf( + "Package %s is abandoned, you should avoid using it. %s.", + $package->getPrettyName(), + $replacement + ) + ); + } + if (!$this->dryRun) { // write lock if ($this->update || !$this->locker->isLocked()) { diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 183f2c740..3ab9c944d 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -333,6 +333,14 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getArchiveExcludes(); } + public function isAbandoned() + { + return $this->aliasOf->isAbandoned(); + } + public function getReplacementPackage() + { + return $this->aliasOf->getReplacementPackage(); + } public function __toString() { return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')'; diff --git a/src/Composer/Package/CompletePackage.php b/src/Composer/Package/CompletePackage.php index a884174af..27c9abeca 100644 --- a/src/Composer/Package/CompletePackage.php +++ b/src/Composer/Package/CompletePackage.php @@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface protected $homepage; protected $scripts = array(); protected $support = array(); + protected $abandoned = false; /** * @param array $scripts @@ -169,4 +170,30 @@ class CompletePackage extends Package implements CompletePackageInterface { return $this->support; } + + /** + * @return boolean + */ + public function isAbandoned() + { + return (boolean) $this->abandoned; + } + + /** + * @param boolean|string $abandoned + */ + public function setAbandoned($abandoned) + { + $this->abandoned = $abandoned; + } + + /** + * If the package is abandoned and has a suggested replacement, this method returns it + * + * @return string|null + */ + public function getReplacementPackage() + { + return is_string($this->abandoned)? $this->abandoned : null; + } } diff --git a/src/Composer/Package/CompletePackageInterface.php b/src/Composer/Package/CompletePackageInterface.php index a341766d3..8263a6535 100644 --- a/src/Composer/Package/CompletePackageInterface.php +++ b/src/Composer/Package/CompletePackageInterface.php @@ -78,4 +78,18 @@ interface CompletePackageInterface extends PackageInterface * @return array */ public function getSupport(); + + /** + * Returns if the package is abandoned or not + * + * @return boolean + */ + public function isAbandoned(); + + /** + * If the package is abandoned and has a suggested replacement, this method returns it + * + * @return string + */ + public function getReplacementPackage(); } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 67318c04a..714c5183b 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -105,6 +105,10 @@ class ArrayDumper if (isset($data['keywords']) && is_array($data['keywords'])) { sort($data['keywords']); } + + if ($package->isAbandoned()) { + $data['abandoned'] = $package->getReplacementPackage() ?: true; + } } if ($package instanceof RootPackageInterface) { diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index d25ea5546..243b7b574 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -195,6 +195,10 @@ class ArrayLoader implements LoaderInterface if (isset($config['support'])) { $package->setSupport($config['support']); } + + if (isset($config['abandoned'])) { + $package->setAbandoned($config['abandoned']); + } } if ($aliasNormalized = $this->getBranchAlias($config)) { diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test new file mode 100644 index 000000000..18f47732e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -0,0 +1,36 @@ +--TEST-- +Abandoned packages are flagged +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "abandoned": true } + ] + }, + { + "type": "package", + "package": [ + { "name": "c/c", "version": "1.0.0", "abandoned": "b/b" } + ] + } + ], + "require": { + "a/a": "1.0.0", + "c/c": "1.0.0" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Installing dependencies (including require-dev) +Package a/a is abandoned, you should avoid using it. No replacement was suggested. +Package c/c is abandoned, you should avoid using it. Use b/b instead. +Writing lock file +Generating autoload files + +--EXPECT-- +Installing a/a (1.0.0) +Installing c/c (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index 0fc5fe301..bfe0bb8a8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -70,4 +70,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master 1234) to a/a (dev-master master) \ No newline at end of file +Updating a/a (dev-master 1234) to a/a (dev-master master) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 03cc10628..40dd03313 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -306,7 +306,7 @@ class InstallerTest extends TestCase die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file))); } - $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode); + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode); } return $tests; diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php index 5576e3d05..f1889a1ce 100644 --- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php +++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php @@ -62,12 +62,33 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase $this->assertSame('dev', $config['minimum-stability']); } + public function testDumpAbandoned() + { + $this->packageExpects('isAbandoned', true); + $this->packageExpects('getReplacementPackage', true); + + $config = $this->dumper->dump($this->package); + + $this->assertSame(true, $config['abandoned']); + } + + public function testDumpAbandonedReplacement() + { + $this->packageExpects('isAbandoned', true); + $this->packageExpects('getReplacementPackage', 'foo/bar'); + + $config = $this->dumper->dump($this->package); + + $this->assertSame('foo/bar', $config['abandoned']); + } + /** * @dataProvider getKeys */ public function testKeys($key, $value, $method = null, $expectedValue = null) { $this->packageExpects('get'.ucfirst($method ?: $key), $value); + $this->packageExpects('isAbandoned', $value); $config = $this->dumper->dump($this->package); diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php index 04a537abd..6e4e2f5ee 100644 --- a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php @@ -117,7 +117,8 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase 'archive' => array( 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), ), - 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')) + 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')), + 'abandoned' => 'foo/bar' ); $package = $this->loader->load($config); @@ -138,4 +139,28 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('Composer\Package\AliasPackage', $package); $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); } + + public function testAbandoned() + { + $config = array( + 'name' => 'A', + 'version' => '1.2.3.4', + 'abandoned' => 'foo/bar' + ); + + $package = $this->loader->load($config); + $this->assertTrue($package->isAbandoned()); + $this->assertEquals('foo/bar', $package->getReplacementPackage()); + } + + public function testNotAbandoned() + { + $config = array( + 'name' => 'A', + 'version' => '1.2.3.4' + ); + + $package = $this->loader->load($config); + $this->assertFalse($package->isAbandoned()); + } }