diff --git a/.travis.yml b/.travis.yml
index 8ba4b4e7e..d78190a57 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -29,3 +29,4 @@ install:
script:
- ./vendor/bin/simple-phpunit
+ - find src/ -name '*.php' | xargs -n1 php -l
diff --git a/src/Cache.php b/src/Cache.php
index 7123d2cda..825011ee2 100644
--- a/src/Cache.php
+++ b/src/Cache.php
@@ -12,70 +12,27 @@
namespace Symfony\Flex;
use Composer\Cache as BaseCache;
+use Composer\IO\IOInterface;
+use Composer\Semver\Constraint\Constraint;
+use Composer\Semver\VersionParser;
/**
* @author Nicolas Grekas
*/
class Cache extends BaseCache
{
- 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',
- ],
- ],
- ];
+ private $versionParser;
+ private $symfonyRequire;
+ private $symfonyContraints;
+ private $io;
+
+ public function setSymfonyRequire(string $symfonyRequire, IOInterface $io)
+ {
+ $this->versionParser = new VersionParser();
+ $this->symfonyRequire = $symfonyRequire;
+ $this->symfonyContraints = $this->versionParser->parseConstraints($symfonyRequire);
+ $this->io = $io;
+ }
public function read($file)
{
@@ -90,23 +47,28 @@ public function read($file)
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)) {
+ if (!$this->symfonyContraints || !isset($data['packages']['symfony/symfony'])) {
+ return $data;
+ }
+ $symfonyVersions = $data['packages']['symfony/symfony'];
+
+ foreach ($data['packages'] as $name => $versions) {
+ foreach ($versions as $version => $package) {
+ if ('symfony/symfony' !== $name && 'self.version' !== ($symfonyVersions[preg_replace('/^(\d++\.\d++)\..*/', '$1.x-dev', $version)]['replace'][$name] ?? null)) {
continue;
}
- foreach ($versions as $version => $composerJson) {
- if (version_compare($version, $lowestVersion, '<')) {
- unset($data['packages'][$package][$version]);
+ $normalizedVersion = $package['extra']['branch-alias'][$version] ?? null;
+ $normalizedVersion = $normalizedVersion ? $this->versionParser->normalize($normalizedVersion) : $package['version_normalized'];
+ $provider = new Constraint('==', $normalizedVersion);
+
+ if (!$this->symfonyContraints->matches($provider)) {
+ if ($this->io) {
+ $this->io->writeError(sprintf('Restricting packages listed in "symfony/symfony" to "%s"', $this->symfonyRequire));
+ $this->io = null;
}
+ unset($data['packages'][$name][$version]);
}
}
- break;
}
return $data;
diff --git a/src/Downloader.php b/src/Downloader.php
index 753c7106e..cdb85275c 100644
--- a/src/Downloader.php
+++ b/src/Downloader.php
@@ -42,21 +42,21 @@ public function __construct(Composer $composer, IoInterface $io, ParallelDownloa
if (getenv('SYMFONY_CAFILE')) {
$this->caFile = getenv('SYMFONY_CAFILE');
}
- if (getenv('SYMFONY_ENDPOINT')) {
- $endpoint = getenv('SYMFONY_ENDPOINT');
- } else {
- $endpoint = $composer->getPackage()->getExtra()['symfony']['endpoint'] ?? self::$DEFAULT_ENDPOINT;
+
+ foreach ($composer->getPackage()->getRequires() as $link) {
+ // recipes apply only when symfony/flex is found in "require" in the root package
+ if ('symfony/flex' !== $link->getTarget()) {
+ continue;
+ }
+ $this->endpoint = rtrim(getenv('SYMFONY_ENDPOINT') ?: ($composer->getPackage()->getExtra()['symfony']['endpoint'] ?? self::$DEFAULT_ENDPOINT), '/');
+ break;
}
- $this->endpoint = rtrim($endpoint, '/');
+
$this->io = $io;
$config = $composer->getConfig();
$this->rfs = $rfs;
$this->cache = new ComposerCache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->endpoint));
$this->sess = bin2hex(random_bytes(16));
-
- if (self::$DEFAULT_ENDPOINT !== $endpoint) {
- $this->io->writeError('Warning: Using '.$endpoint.' as the Symfony endpoint');
- }
}
public function getSessionId(): string
@@ -69,6 +69,11 @@ public function setFlexId(string $id = null)
$this->flexId = $id;
}
+ public function getEndpoint()
+ {
+ return $this->endpoint;
+ }
+
/**
* Downloads recipes.
*
@@ -120,6 +125,10 @@ public function getRecipes(array $operations): array
$paths[] = ['/p/'.$chunk];
}
+ if (null !== $this->endpoint && self::$DEFAULT_ENDPOINT !== $this->endpoint) {
+ $this->io->writeError('Using "'.$this->endpoint.'" as the Symfony endpoint');
+ }
+
$bodies = [];
$this->rfs->download($paths, function ($path) use (&$bodies) {
if ($body = $this->get($path, [], false)->getBody()) {
@@ -151,6 +160,9 @@ public function getRecipes(array $operations): array
*/
public function get(string $path, array $headers = [], $cache = true): Response
{
+ if (null === $this->endpoint) {
+ return new Response([]);
+ }
$headers[] = 'Package-Session: '.$this->sess;
$url = $this->endpoint.'/'.ltrim($path, '/');
$cacheKey = $cache ? ltrim($path, '/') : '';
diff --git a/src/Flex.php b/src/Flex.php
index 75b779051..7828c038a 100644
--- a/src/Flex.php
+++ b/src/Flex.php
@@ -30,6 +30,7 @@
use Composer\IO\NullIO;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
+use Composer\Package\BasePackage;
use Composer\Package\Comparer\Comparer;
use Composer\Package\Locker;
use Composer\Package\PackageInterface;
@@ -105,14 +106,19 @@ public function activate(Composer $composer, IOInterface $io)
$rfs = Factory::createRemoteFilesystem($this->io, $this->config);
$this->rfs = new ParallelDownloader($this->io, $this->config, $rfs->getOptions(), $rfs->isTlsDisabled());
+ $symfonyRequire = getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? null);
+
$manager = RepositoryFactory::manager($this->io, $this->config, $composer->getEventDispatcher(), $this->rfs);
- $setRepositories = \Closure::bind(function (RepositoryManager $manager) {
+ $setRepositories = \Closure::bind(function (RepositoryManager $manager) use ($symfonyRequire) {
$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;
+ if ($repo instanceof TruncatedComposerRepository && $symfonyRequire) {
+ $repo->setSymfonyRequire($symfonyRequire, $this->io);
+ }
}
$manager->setLocalRepository($this->getLocalRepository());
}, $composer->getRepositoryManager(), RepositoryManager::class);
@@ -156,6 +162,8 @@ public function activate(Composer $composer, IOInterface $io)
continue;
}
+ // In Composer 1.0.*, $input knows about option and argument definitions
+ // Since Composer >=1.1, $input contains only raw values
$input = $trace['args'][0];
$app = $trace['object'];
@@ -195,12 +203,19 @@ public function activate(Composer $composer, IOInterface $io)
}
}
- if ($input->hasOption('no-progress')) {
- $this->progress = !$input->getOption('no-progress');
+ if ($input->hasParameterOption('--no-progress', true)) {
+ $this->progress = false;
+ }
+
+ if ($input->hasParameterOption('--dry-run', true)) {
+ $this->dryRun = true;
}
- if ($input->hasOption('dry-run')) {
- $this->dryRun = $input->getOption('dry-run');
+ if ($input->hasParameterOption('--prefer-lowest', true)) {
+ // When prefer-lowest is set and no stable version has been released,
+ // we consider "dev" more stable than "alpha", "beta" or "RC". This
+ // allows testing lowest versions with potential fixes applied.
+ BasePackage::$stabilities['dev'] = 1 + BasePackage::STABILITY_STABLE;
}
$composerFile = Factory::getComposerFile();
@@ -530,6 +545,10 @@ public function generateFlexId()
return;
}
+ if (null === $this->downloader->getEndpoint()) {
+ throw new \LogicException('Cannot generate project id when "symfony/flex" is not in the "require" section of the root composer.json.');
+ }
+
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
$manipulator->addSubNode('extra', 'symfony.id', $this->downloader->get('/ulid')->getBody()['ulid']);
@@ -540,6 +559,11 @@ public function generateFlexId()
private function fetchRecipes(): array
{
+ if (null === $this->downloader->getEndpoint()) {
+ $this->io->writeError('Symfony recipes are disabled: "symfony/flex" is not in the "require" section of the root composer.json');
+
+ return [[], []];
+ }
$devPackages = null;
$data = $this->downloader->getRecipes($this->operations);
$manifests = $data['manifests'] ?? [];
diff --git a/src/Lock.php b/src/Lock.php
index 0f320f9fb..70ae8efea 100644
--- a/src/Lock.php
+++ b/src/Lock.php
@@ -46,7 +46,11 @@ public function remove($name)
public function write()
{
- ksort($this->lock);
- $this->json->write($this->lock);
+ if ($this->lock) {
+ ksort($this->lock);
+ $this->json->write($this->lock);
+ } elseif ($this->json->exists()) {
+ @unlink($this->json->getPath());
+ }
}
}
diff --git a/src/ParallelDownloader.php b/src/ParallelDownloader.php
index 9d69eefde..8fe02dc56 100644
--- a/src/ParallelDownloader.php
+++ b/src/ParallelDownloader.php
@@ -83,8 +83,12 @@ public function download(array &$nextArgs, callable $nextCallback, bool $quiet =
}
try {
$this->getNext();
- if (!$this->quiet) {
+ if ($this->quiet) {
+ // no-op
+ } elseif ($this->progress) {
$this->io->overwriteError(' (100%)');
+ } else {
+ $this->io->writeError(' (100%)');
}
} finally {
if (!$this->quiet) {
diff --git a/src/Recipe.php b/src/Recipe.php
index 1c6b30107..2238b87a4 100644
--- a/src/Recipe.php
+++ b/src/Recipe.php
@@ -73,7 +73,7 @@ public function getURL(): string
// symfony/translation:3.3@github.com/symfony/recipes:master
if (!preg_match('/^([^\:]+?)\:([^\@]+)@([^\:]+)\:(.+)$/', $this->data['origin'], $matches)) {
- // that exclude auto-generated recipes, which is what we want
+ // that excludes auto-generated recipes, which is what we want
return '';
}
diff --git a/src/TruncatedComposerRepository.php b/src/TruncatedComposerRepository.php
index b49231e4d..69fb7d9b8 100644
--- a/src/TruncatedComposerRepository.php
+++ b/src/TruncatedComposerRepository.php
@@ -29,6 +29,11 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config,
$this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$');
}
+ public function setSymfonyRequire(string $symfonyRequire, IOInterface $io)
+ {
+ $this->cache->setSymfonyRequire($symfonyRequire, $io);
+ }
+
protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
{
$data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime);
diff --git a/tests/FlexTest.php b/tests/FlexTest.php
index 7c9fbe744..bc3437fd2 100644
--- a/tests/FlexTest.php
+++ b/tests/FlexTest.php
@@ -16,6 +16,7 @@
use Composer\Factory;
use Composer\Installer\PackageEvent;
use Composer\IO\BufferIO;
+use Composer\Package\Link;
use Composer\Package\Locker;
use Composer\Package\Package;
use Composer\Package\RootPackageInterface;
@@ -106,6 +107,7 @@ public function testPostInstall()
$downloader = $this->getMockBuilder(Downloader::class)->disableOriginalConstructor()->getMock();
$downloader->expects($this->once())->method('getRecipes')->willReturn($data);
+ $downloader->expects($this->once())->method('getEndpoint')->willReturn('dummy');
$io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE);
$locker = $this->getMockBuilder(Locker::class)->disableOriginalConstructor()->getMock();
@@ -167,6 +169,7 @@ public function testActivateLoadsClasses()
$composer->setConfig(Factory::createConfig($io));
$package = $this->getMockBuilder(RootPackageInterface::class)->disableOriginalConstructor()->getMock();
$package->method('getExtra')->will($this->returnValue(['symfony' => ['allow-contrib' => true]]));
+ $package->method('getRequires')->will($this->returnValue([new Link('dummy', 'symfony/flex')]));
$composer->setPackage($package);
$localRepo = $this->getMockBuilder(WritableRepositoryInterface::class)->disableOriginalConstructor()->getMock();
$manager = $this->getMockBuilder(RepositoryManager::class)->disableOriginalConstructor()->getMock();