From 3307d4957925df319df2b84cfc3e6f1680d0632a Mon Sep 17 00:00:00 2001 From: jeroendedauw Date: Fri, 20 Dec 2013 23:34:49 +0100 Subject: [PATCH] Make it possible for extensions to specify which version of MediaWiki they support via Composer. This change allows extensions to specify they depend on a specific version or version range of MediaWiki. This is done by adding the package mediawiki/mediawiki in their composer.json require section. As MediaWiki itself is not a Composer package and is quite far away from becoming one, a workaround was needed, which is provided by this commit. It works as follows. When "composer install" or "composer update" is run, a Composer hook is invoked. This hook programmatically indicates the root package provides MediaWiki, as it indeed does when extensions are installed into MediaWiki. The package link of type "provides" includes the MediaWiki version, which is read from DefaultSettings.php. This functionality has been tested and confirmed to work. One needs a recent Composer version for it to have an effect. The upcoming Composer alpha8 release will suffice. See https://github.com/composer/composer/issues/2520 Tests are included. Composer independent tests will run always, while the Composer specific ones are skipped when Composer is not installed. People that already have a composer.json file in their MediaWiki root directory will need to make the same additions there as this commit makes to composer-json.example. If this is not done, the new behaviour will not work for them (though no existing behaviour will break). The change to the json file has been made in such a way to minimize the likelihood that any future modifications there will be needed. Thanks go to @beausimensen (Sculpin) and @seldaek (Composer) for their support. Change-Id: I8df66a92971146ab79cd4fcbd181e559115ca240 --- composer-example.json | 9 + includes/AutoLoader.php | 5 + includes/MediaWikiVersionFetcher.php | 31 ++++ includes/composer/ComposerHookHandler.php | 37 +++++ includes/composer/ComposerPackageModifier.php | 57 +++++++ .../composer/ComposerVersionNormalizer.php | 67 ++++++++ .../includes/MediaWikiVersionFetcherTest.php | 21 +++ .../ComposerVersionNormalizerTest.php | 156 ++++++++++++++++++ 8 files changed, 383 insertions(+) create mode 100644 includes/MediaWikiVersionFetcher.php create mode 100644 includes/composer/ComposerHookHandler.php create mode 100644 includes/composer/ComposerPackageModifier.php create mode 100644 includes/composer/ComposerVersionNormalizer.php create mode 100644 tests/phpunit/includes/MediaWikiVersionFetcherTest.php create mode 100644 tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php diff --git a/composer-example.json b/composer-example.json index cf63678c1c..85304c1fa5 100644 --- a/composer-example.json +++ b/composer-example.json @@ -7,5 +7,14 @@ "ext-mbstring": "Faster unicode handling", "ext-wikidiff2": "Faster diff generation", "ext-apc": "Speed up MediaWiki with opcode caching (before PHP 5.5)" + }, + "autoload": { + "psr-0": { + "ComposerHookHandler": "includes/composer" + } + }, + "scripts": { + "pre-update-cmd": "ComposerHookHandler::onPreUpdate", + "pre-install-cmd": "ComposerHookHandler::onPreInstall" } } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index c511b4b2b1..2a5e857267 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -141,6 +141,7 @@ $wgAutoloadLocalClasses = array( 'MailAddress' => 'includes/UserMailer.php', 'MediaWiki' => 'includes/Wiki.php', 'MediaWiki_I18N' => 'includes/SkinTemplate.php', + 'MediaWikiVersionFetcher' => 'includes/MediaWikiVersionFetcher.php', 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', 'MimeMagic' => 'includes/MimeMagic.php', @@ -397,6 +398,10 @@ $wgAutoloadLocalClasses = array( 'RedisConnectionPool' => 'includes/clientpool/RedisConnectionPool.php', 'RedisConnRef' => 'includes/clientpool/RedisConnectionPool.php', + # includes/composer + 'ComposerPackageModifier' => 'includes/composer/ComposerPackageModifier.php', + 'ComposerVersionNormalizer' => 'includes/composer/ComposerVersionNormalizer.php', + # includes/config 'Config' => 'includes/config/Config.php', 'GlobalConfig' => 'includes/config/GlobalConfig.php', diff --git a/includes/MediaWikiVersionFetcher.php b/includes/MediaWikiVersionFetcher.php new file mode 100644 index 0000000000..1d59ec3097 --- /dev/null +++ b/includes/MediaWikiVersionFetcher.php @@ -0,0 +1,31 @@ + + */ +class MediaWikiVersionFetcher { + + /** + * Returns the MediaWiki version, in the format used by MediaWiki's wgVersion global. + * + * @return string + * @throws RuntimeException + */ + public function fetchVersion() { + $defaultSettings = file_get_contents( __DIR__ . '/DefaultSettings.php' ); + + $matches = array(); + preg_match( "/wgVersion = '([0-9a-zA-Z\.]+)';/", $defaultSettings, $matches ); + + if ( count( $matches ) !== 2 ) { + throw new RuntimeException( 'Could not extract the MediaWiki version from DefaultSettings.php' ); + } + + return $matches[1]; + } + +} \ No newline at end of file diff --git a/includes/composer/ComposerHookHandler.php b/includes/composer/ComposerHookHandler.php new file mode 100644 index 0000000000..5cf8a9b797 --- /dev/null +++ b/includes/composer/ComposerHookHandler.php @@ -0,0 +1,37 @@ + + */ +class ComposerHookHandler { + + public static function onPreUpdate( Event $event ) { + self::handleChangeEvent( $event ); + } + + public static function onPreInstall( Event $event ) { + self::handleChangeEvent( $event ); + } + + private static function handleChangeEvent( Event $event ) { + $package = $event->getComposer()->getPackage(); + + if ( $package instanceof Package ) { + $packageModifier = new ComposerPackageModifier( + $package, + new ComposerVersionNormalizer(), + new MediaWikiVersionFetcher() + ); + + $packageModifier->setProvidesMediaWiki(); + } + } + +} diff --git a/includes/composer/ComposerPackageModifier.php b/includes/composer/ComposerPackageModifier.php new file mode 100644 index 0000000000..ae8baf2b4b --- /dev/null +++ b/includes/composer/ComposerPackageModifier.php @@ -0,0 +1,57 @@ + + */ +class ComposerPackageModifier { + + const MEDIAWIKI_PACKAGE_NAME = 'mediawiki/mediawiki'; + + protected $package; + protected $versionNormalizer; + protected $versionFetcher; + + public function __construct( Package $package, ComposerVersionNormalizer $versionNormalizer, MediaWikiVersionFetcher $versionFetcher ) { + $this->package = $package; + $this->versionNormalizer = $versionNormalizer; + $this->versionFetcher = $versionFetcher; + } + + public function setProvidesMediaWiki() { + $this->setLinkAsProvides( $this->newMediaWikiLink() ); + } + + private function setLinkAsProvides( Link $link ) { + $this->package->setProvides( array( $link ) ); + } + + private function newMediaWikiLink() { + $version = $this->getMediaWikiVersionConstraint(); + + $link = new Link( + '__root__', + self::MEDIAWIKI_PACKAGE_NAME, + $version, + 'provides', + $version->getPrettyString() + ); + + return $link; + } + + private function getMediaWikiVersionConstraint() { + $mvVersion = $this->versionFetcher->fetchVersion(); + $mvVersion = $this->versionNormalizer->normalizeSuffix( $mvVersion ); + + $version = new VersionConstraint( '==', $this->versionNormalizer->normalizeLevelCount( $mvVersion ) ); + $version->setPrettyString( $mvVersion ); + + return $version; + } + +} diff --git a/includes/composer/ComposerVersionNormalizer.php b/includes/composer/ComposerVersionNormalizer.php new file mode 100644 index 0000000000..727e1423f7 --- /dev/null +++ b/includes/composer/ComposerVersionNormalizer.php @@ -0,0 +1,67 @@ + + */ +class ComposerVersionNormalizer { + + /** + * Ensures there is a dash in between the version and the stability suffix. + * + * Examples: + * - 1.23RC => 1.23-RC + * - 1.23alpha => 1.23-alpha + * - 1.23alpha3 => 1.23-alpha3 + * - 1.23-beta => 1.23-beta + * + * @param string $version + * + * @return string + * @throws InvalidArgumentException + */ + public function normalizeSuffix( $version ) { + if ( !is_string( $version ) ) { + throw new InvalidArgumentException( '$version must be a string' ); + } + + return preg_replace( '/^(\d[\d\.]*)([a-zA-Z]+)(\d*)$/', '$1-$2$3', $version, 1 ); + } + + /** + * Ensures the version has four levels. + * Version suffixes are supported, as long as they start with a dash. + * + * Examples: + * - 1.19 => 1.19.0.0 + * - 1.19.2.3 => 1.19.2.3 + * - 1.19-alpha => 1.19.0.0-alpha + * - 1337 => 1337.0.0.0 + * + * @param string $version + * + * @return string + * @throws InvalidArgumentException + */ + public function normalizeLevelCount( $version ) { + if ( !is_string( $version ) ) { + throw new InvalidArgumentException( '$version must be a string' ); + } + + $dashPosition = strpos( $version, '-' ); + + if ( $dashPosition !== false ) { + $suffix = substr( $version, $dashPosition ); + $version = substr( $version, 0, $dashPosition ); + } + + $version = implode( '.', array_pad( explode( '.', $version ), 4, '0' ) ); + + if ( $dashPosition !== false ) { + $version .= $suffix; + } + + return $version; + } + +} \ No newline at end of file diff --git a/tests/phpunit/includes/MediaWikiVersionFetcherTest.php b/tests/phpunit/includes/MediaWikiVersionFetcherTest.php new file mode 100644 index 0000000000..bbb83da6c1 --- /dev/null +++ b/tests/phpunit/includes/MediaWikiVersionFetcherTest.php @@ -0,0 +1,21 @@ + + */ +class MediaWikiVersionFetcherTest extends PHPUnit_Framework_TestCase { + + public function testReturnsResult() { + $versionFetcher = new MediaWikiVersionFetcher(); + $this->assertInternalType( 'string', $versionFetcher->fetchVersion() ); + } + +} \ No newline at end of file diff --git a/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php b/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php new file mode 100644 index 0000000000..8de8be16df --- /dev/null +++ b/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php @@ -0,0 +1,156 @@ + + */ +class ComposerVersionNormalizerTest extends PHPUnit_Framework_TestCase { + + /** + * @dataProvider nonStringProvider + */ + public function testGivenNonString_normalizeThrowsInvalidArgumentException( $nonString ) { + $normalizer = new ComposerVersionNormalizer(); + + $this->setExpectedException( 'InvalidArgumentException' ); + $normalizer->normalizeSuffix( $nonString ); + } + + public function nonStringProvider() { + return array( + array( null ), + array( 42 ), + array( array() ), + array( new stdClass() ), + array( true ), + ); + } + + /** + * @dataProvider simpleVersionProvider + */ + public function testGivenSimpleVersion_normalizeSuffixReturnsAsIs( $simpleVersion ) { + $this->assertRemainsUnchanged( $simpleVersion ); + } + + protected function assertRemainsUnchanged( $version ) { + $normalizer = new ComposerVersionNormalizer(); + + $this->assertEquals( + $version, + $normalizer->normalizeSuffix( $version ) + ); + } + + public function simpleVersionProvider() { + return array( + array( '1.22.0' ), + array( '1.19.2' ), + array( '1.19.2.0' ), + array( '1.9' ), + array( '123.321.456.654' ), + ); + } + + /** + * @dataProvider complexVersionProvider + */ + public function testGivenComplexVersionWithoutDash_normalizeSuffixAddsDash( $withoutDash, $withDash ) { + $normalizer = new ComposerVersionNormalizer(); + + $this->assertEquals( + $withDash, + $normalizer->normalizeSuffix( $withoutDash ) + ); + } + + public function complexVersionProvider() { + return array( + array( '1.22.0alpha', '1.22.0-alpha' ), + array( '1.22.0RC', '1.22.0-RC' ), + array( '1.19beta', '1.19-beta' ), + array( '1.9RC4', '1.9-RC4' ), + array( '1.9.1.2RC4', '1.9.1.2-RC4' ), + array( '1.9.1.2RC', '1.9.1.2-RC' ), + array( '123.321.456.654RC9001', '123.321.456.654-RC9001' ), + ); + } + + /** + * @dataProvider complexVersionProvider + */ + public function testGivenComplexVersionWithDash_normalizeSuffixReturnsAsIs( $withoutDash, $withDash ) { + $this->assertRemainsUnchanged( $withDash ); + } + + /** + * @dataProvider fourLevelVersionsProvider + */ + public function testGivenFourLevels_levelCountNormalizationDoesNothing( $version ) { + $normalizer = new ComposerVersionNormalizer(); + + $this->assertEquals( + $version, + $normalizer->normalizeLevelCount( $version ) + ); + } + + public function fourLevelVersionsProvider() { + return array( + array( '1.22.0.0' ), + array( '1.19.2.4' ), + array( '1.19.2.0' ), + array( '1.9.0.1' ), + array( '123.321.456.654' ), + array( '123.321.456.654RC4' ), + array( '123.321.456.654-RC4' ), + ); + } + + /** + * @dataProvider levelNormalizationProvider + */ + public function testGivenFewerLevels_levelCountNormalizationEnsuresFourLevels( $expected, $version ) { + $normalizer = new ComposerVersionNormalizer(); + + $this->assertEquals( + $expected, + $normalizer->normalizeLevelCount( $version ) + ); + } + + public function levelNormalizationProvider() { + return array( + array( '1.22.0.0', '1.22' ), + array( '1.22.0.0', '1.22.0' ), + array( '1.19.2.0', '1.19.2' ), + array( '12345.0.0.0', '12345' ), + array( '12345.0.0.0-RC4', '12345-RC4' ), + array( '12345.0.0.0-alpha', '12345-alpha' ), + ); + } + + /** + * @dataProvider invalidVersionProvider + */ + public function testGivenInvalidVersion_normalizeSuffixReturnsAsIs( $invalidVersion ) { + $this->assertRemainsUnchanged( $invalidVersion ); + } + + public function invalidVersionProvider() { + return array( + array( '1.221-a' ), + array( '1.221-' ), + array( '1.22rc4a' ), + array( 'a1.22rc' ), + array( '.1.22rc' ), + array( 'a' ), + array( 'alpha42' ), + ); + } + +} \ No newline at end of file -- 2.20.1