diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..9e02e939 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,79 @@ +build: false +platform: + - x64 + - x86 +clone_depth: 1 +clone_folder: C:\projects\discovery + +environment: + matrix: + - dependencies: basic + PHP_VERSION: 7.2 + - dependencies: high + PHP_VERSION: 7.2 + - dependencies: lowest + PHP_VERSION: 7.2 + +matrix: + fast_finish: true + allow_failures: + - dependencies: lowest + PHP_VERSION: 7.2 + +services: + - memcached + +# cache is cleared when linked file is modified +cache: + # Cache chocolatey packages + - C:\ProgramData\chocolatey\bin -> .appveyor.yml + - C:\ProgramData\chocolatey\lib -> .appveyor.yml + # Cache php install + - C:\tools\php -> .appveyor.yml + # Cache composer + - C:\projects\discovery\vendor -> composer.json + - '%LOCALAPPDATA%\Composer\files -> composer.json' + +init: + - SET PHP=1 + - SET PATH=C:\Program Files\OpenSSL;C:\tools\php;%PATH% + - SET ANSICON=121x90 (121x90) + - SET COMPOSER_NO_INTERACTION=1 + - SET COMPOSER_UP=php composer.phar update --no-interaction --prefer-dist --no-progress --profile --no-suggest --ansi + - REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v DelayedExpansion /t REG_DWORD /d 1 /f + +install: + # If there is a newer build queued for the same PR, cancel this one. + # The AppVeyor 'rollout builds' option is supposed to serve the same + # purpose but it is problematic because it tends to cancel builds pushed + # directly to master instead of just PR builds (or the converse). + # credits: JuliaLang developers. + - ps: if ($Env:APPVEYOR_PULL_REQUEST_NUMBER -and $Env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` + https://ci.appveyor.com/api/projects/$Env:APPVEYOR_ACCOUNT_NAME/$Env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` + Where-Object pullRequestId -eq $Env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` + throw "There are newer queued builds for this pull request, failing early." } + # Install PHP + - IF EXIST C:\tools\php (SET PHP=0) + - IF %PHP%==1 appveyor DownloadFile "https://raw.githubusercontent.com/prisis/ps-install-php/master/Install-PHP.ps1" + - ps: if (-not (Test-Path C:\tools\php)) {.\Install-PHP.ps1 -InstallPath C:\tools\php -Version $Env:PHP_VERSION -Highest -Arch $Env:PLATFORM -Extensions mbstring,intl,openssl,memcache,fileinfo,pdo_sqlite,curl,sodium} + # PHP settings and extensions + - IF %PHP%==1 cd C:\tools\php + - IF %PHP%==1 echo zend_extension=php_opcache.dll >> php.ini + - IF %PHP%==1 echo max_execution_time=1200 >> php.ini + - IF %PHP%==1 echo date.timezone="Europe/Berlin" >> php.ini + - IF %PHP%==1 echo opcache.enable_cli=1 >> php.ini + - IF %PHP%==1 echo apc.enable_cli=1 >> php.ini + - IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev) + - cd C:\projects\discovery + - php -r "readfile('https://getcomposer.org/installer');" | php + # Matrix + - php composer.phar global require hirak/prestissimo + - IF %dependencies%==basic appveyor-retry %COMPOSER_UP% + - IF %dependencies%==lowest appveyor-retry %COMPOSER_UP% --prefer-lowest --prefer-stable + - IF %dependencies%==high appveyor-retry %COMPOSER_UP% + - cd build/appveyor + - appveyor DownloadFile https://cdn.rawgit.com/prisis/43a2a7b137998ac92e24ee4daaa8e296/raw/681b89b8e156750de46558ead661509c468fb9a2/try_catch.sh + +test_script: + - cd C:\projects\discovery + - sh ./build/appveyor/script.sh diff --git a/build/appveyor/script.sh b/build/appveyor/script.sh new file mode 100644 index 00000000..efe3ff1c --- /dev/null +++ b/build/appveyor/script.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +source ./build/appveyor/try_catch.sh + +for f in ./src/*; do + if [[ -d "$f" && ! -L "$f" ]]; then + TYPE="$(basename "$f")"; + + if [[ "$TYPE" = "Common" ]]; then + TESTSUITE="Narrowspark Discovery Common Test Suite"; + elif [[ "$TYPE" = "Discovery" ]]; then + TESTSUITE="Narrowspark Discovery Test Suite"; + fi + + echo ""; + echo -e "$TESTSUITE"; + echo ""; + + try + sh vendor/bin/phpunit --verbose -c ./phpunit.xml.dist --testsuite="$TESTSUITE"; + catch || { + exit 1 + } + fi +done diff --git a/src/Common/Configurator/AbstractConfigurator.php b/src/Common/Configurator/AbstractConfigurator.php index 2eeb2809..4be71fb5 100644 --- a/src/Common/Configurator/AbstractConfigurator.php +++ b/src/Common/Configurator/AbstractConfigurator.php @@ -110,7 +110,7 @@ protected function markData(string $packageName, string $data, int $spaceMultipl } /** - * Insert string at specified position. + * Insert string before specified position. * * @param string $string * @param string $insertStr diff --git a/src/Discovery/Discovery.php b/src/Discovery/Discovery.php index 5c263271..5be1f12b 100644 --- a/src/Discovery/Discovery.php +++ b/src/Discovery/Discovery.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace Narrowspark\Discovery; +use Closure; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -22,7 +23,9 @@ use Composer\Plugin\PluginInterface; use Composer\Plugin\PreFileDownloadEvent; use Composer\Repository\ComposerRepository as BaseComposerRepository; +use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositoryManager; use Composer\Script\Event; use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; @@ -34,6 +37,7 @@ use Narrowspark\Discovery\Installer\QuestionInstallationManager; use Narrowspark\Discovery\Prefetcher\ParallelDownloader; use Narrowspark\Discovery\Prefetcher\Prefetcher; +use Narrowspark\Discovery\Prefetcher\TruncatedComposerRepository; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; @@ -253,9 +257,26 @@ public function activate(Composer $composer, IOInterface $io): void $this->rfs = new ParallelDownloader($this->io, $composerConfig, $rfs->getOptions(), $rfs->isTlsDisabled()); $this->prefetcher = new Prefetcher($this->composer, $this->io, $this->input, $this->rfs); - $this->prefetcher->prefetchComposerRepositories($rfs); + $manager = RepositoryFactory::manager($this->io, $composerConfig, $composer->getEventDispatcher(), $this->rfs); + $setRepositories = Closure::bind(function (RepositoryManager $manager) { + $manager->repositoryClasses = $this->repositoryClasses; + $manager->setRepositoryClass('composer', TruncatedComposerRepository::class); + $manager->repositories = $this->repositories; + + $i = 0; + + foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) { + $manager->repositories[$i++] = $repo; + } + + $manager->setLocalRepository($this->getLocalRepository()); + }, $composer->getRepositoryManager(), RepositoryManager::class); + + $setRepositories($manager); + $composer->setRepositoryManager($manager); + $this->lock->add('@readme', [ 'This file locks the discovery information of your project to a known state', 'This file is @generated automatically', @@ -794,9 +815,9 @@ private function getErrorMessage(): ?string return 'You must enable the openssl extension in your "php.ini" file.'; } - \preg_match_all('/\d+.\d+.\d+/m', Composer::VERSION, $matches, \PREG_SET_ORDER, 0); + \preg_match('/\d+.\d+.\d+/m', Composer::VERSION, $matches); - if ($matches !== null && \version_compare('1.6.0', $matches[0], '<=')) { + if ($matches !== null && \version_compare($matches[0], '1.6.0') === -1) { return \sprintf('Your version "%s" of Composer is too old; Please upgrade.', Composer::VERSION); } diff --git a/src/Discovery/Prefetcher/Cache.php b/src/Discovery/Prefetcher/Cache.php new file mode 100644 index 00000000..927a2db1 --- /dev/null +++ b/src/Discovery/Prefetcher/Cache.php @@ -0,0 +1,126 @@ + + */ +class Cache extends BaseComposerCache +{ + /** + * @var array + */ + private static $lowestTags = [ + 'symfony/symfony' => [ + 'version' => 'v3.4.0', + 'replaces' => [ + 'symfony/asset', + 'symfony/browser-kit', + 'symfony/cache', + 'symfony/config', + 'symfony/console', + 'symfony/css-selector', + 'symfony/dependency-injection', + 'symfony/debug', + 'symfony/debug-bundle', + 'symfony/doctrine-bridge', + 'symfony/dom-crawler', + 'symfony/dotenv', + 'symfony/event-dispatcher', + 'symfony/expression-language', + 'symfony/filesystem', + 'symfony/finder', + 'symfony/form', + 'symfony/framework-bundle', + 'symfony/http-foundation', + 'symfony/http-kernel', + 'symfony/inflector', + 'symfony/intl', + 'symfony/ldap', + 'symfony/lock', + 'symfony/messenger', + 'symfony/monolog-bridge', + 'symfony/options-resolver', + 'symfony/process', + 'symfony/property-access', + 'symfony/property-info', + 'symfony/proxy-manager-bridge', + 'symfony/routing', + 'symfony/security', + 'symfony/security-core', + 'symfony/security-csrf', + 'symfony/security-guard', + 'symfony/security-http', + 'symfony/security-bundle', + 'symfony/serializer', + 'symfony/stopwatch', + 'symfony/templating', + 'symfony/translation', + 'symfony/twig-bridge', + 'symfony/twig-bundle', + 'symfony/validator', + 'symfony/var-dumper', + 'symfony/web-link', + 'symfony/web-profiler-bundle', + 'symfony/web-server-bundle', + 'symfony/workflow', + 'symfony/yaml', + ], + ], + ]; + + /** + * @param string $file + * + * @return bool|string + */ + public function read($file) + { + $content = parent::read($file); + + if (0 === \mb_strpos($file, 'provider-symfony$')) { + $content = \json_encode($this->removeLegacyTags(\json_decode($content, true))); + } + + return $content; + } + + /** + * @param array $data + * + * @return array + */ + public function removeLegacyTags(array $data): array + { + foreach (self::$lowestTags as $lowestPackage => $settings) { + $lowestVersion = $settings['version']; + $replacedPackages = $settings['replaces']; + + if (! isset($data['packages'][$lowestPackage][$lowestVersion])) { + continue; + } + + foreach ($data['packages'] as $package => $versions) { + if ($package !== $lowestPackage && ! \in_array($package, $replacedPackages, true)) { + continue; + } + + foreach ($versions as $version => $composerJson) { + if (\version_compare($version, $lowestVersion, '<')) { + unset($data['packages'][$package][$version]); + } + } + } + + break; + } + + return $data; + } +} diff --git a/src/Discovery/Prefetcher/TruncatedComposerRepository.php b/src/Discovery/Prefetcher/TruncatedComposerRepository.php new file mode 100644 index 00000000..6be17178 --- /dev/null +++ b/src/Discovery/Prefetcher/TruncatedComposerRepository.php @@ -0,0 +1,32 @@ + + */ +class TruncatedComposerRepository extends BaseComposerRepository +{ + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + { + parent::__construct($repoConfig, $io, $config, $eventDispatcher, $rfs); + $this->cache = new Cache($io, $config->get('cache-repo-dir') . '/' . \preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); + } + + protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false) + { + $data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime); + + return $this->cache->removeLegacyTags($data); + } +} diff --git a/tests/Common/Installer/InstallationManagerTest.php b/tests/Common/Installer/InstallationManagerTest.php index a6b14080..2d12d7d0 100644 --- a/tests/Common/Installer/InstallationManagerTest.php +++ b/tests/Common/Installer/InstallationManagerTest.php @@ -1,6 +1,6 @@ once() ->andReturn($pluginManagerMock); + $this->composerMock->shouldReceive('getEventDispatcher') + ->once() + ->andReturn($this->mock(EventDispatcher::class)); + + $this->composerMock->shouldReceive('setRepositoryManager') + ->with(\Mockery::type(RepositoryManager::class)) + ->once(); + $this->ioMock->shouldReceive('writeError') ->once() ->with('Composer >=1.7 not found, downloads will happen in sequence', true, IOInterface::DEBUG); @@ -182,6 +191,13 @@ public function testOnCommand(): void $commandEventMock->shouldReceive('getInput->setOption') ->once() ->with('no-suggest', true); + $commandEventMock->shouldReceive('getInput->hasOption') + ->once() + ->with('remove-vcs') + ->andReturn(true); + $commandEventMock->shouldReceive('getInput->setOption') + ->once() + ->with('remove-vcs', true); $this->discovery->onCommand($commandEventMock); } @@ -305,6 +321,9 @@ private function arrangeDiscoveryConfig(): void ->once() ->with('cache-files-dir') ->andReturn(__DIR__); + $this->configMock->shouldReceive('get') + ->with('cache-repo-dir') + ->andReturn('repo'); $this->composerMock->shouldReceive('getConfig') ->andReturn($this->configMock); } diff --git a/tests/Discovery/Installer/QuestionInstallationManagerTest.php b/tests/Discovery/Installer/QuestionInstallationManagerTest.php index 26329912..83c32693 100644 --- a/tests/Discovery/Installer/QuestionInstallationManagerTest.php +++ b/tests/Discovery/Installer/QuestionInstallationManagerTest.php @@ -867,9 +867,9 @@ private function arrangeSimpleRootPackage(string $stability = 'dev') /** * @param string $name * - * @return \Narrowspark\Discovery\Package + * @return \Narrowspark\Discovery\Common\Package */ - private function arrangeInstallPackage(string $name): \Narrowspark\Discovery\Package + private function arrangeInstallPackage(string $name): Package { return new Package( $name,