diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 0607535d7..eda335453 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -71,12 +71,43 @@ indirectly) back on the root package itself, issues can occur in two cases: but some CIs do shallow clones so that process can fail when testing pull requests and feature branches. In these cases the branch alias may then not be recognized. The best solution is to define the version you are on via an environment variable - called COMPOSER_ROOT_VERSION. You set it to `dev-main` for example to define + called `COMPOSER_ROOT_VERSION`. You set it to `dev-main` for example to define the root package's version as `dev-main`. Use for example: `COMPOSER_ROOT_VERSION=dev-main composer install` to export the variable only for the call to composer, or you can define it globally in the CI env vars. +## Root package version detection + +Composer relies on knowing the version of the root package to resolve +dependencies effectively. The version of the root package is determined +using a hierarchical approach: + +1. **composer.json Version Field**: Firstly, Composer looks for a `version` + field in the project's root `composer.json` file. If present, this field + specifies the version of the root package directly. This is generally not + recommended as it needs to be constantly updated, but it is an option. + +2. **Environment Variable**: Composer then checks for the `COMPOSER_ROOT_VERSION` + environment variable. This variable can be explicitly set by the user to + define the version of the root package, providing a straightforward way to + inform Composer of the exact version, especially in CI/CD environments or + when the VCS method is not applicable. + +3. **Version Control System (VCS) Inspection**: Composer then attempts to guess + the version by interfacing with the version control system of the project. For + instance, in projects versioned with Git, Composer executes specific Git + commands to deduce the project's current version based on tags, branches, and + commit history. If a `.git` directory is missing or the history is incomplete + because CI is using a shallow clone for example, this detection may fail to find + the correct version. + +4. **Fallback**: If all else fails, Composer uses `1.0.0` as default version. + +Note that relying on the default/fallback version might potentially lead to dependency +resolution issues, especially when the root package depends on a package which ends up +depending (directly or indirectly) +[back on the root package itself](#dependencies-on-the-root-package). ## Network timeout issues, curl error diff --git a/src/Composer/Config.php b/src/Composer/Config.php index f9da7d304..8d2885a3c 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -554,6 +554,8 @@ class Config * This should be used to read COMPOSER_ environment variables * that overload config values. * + * @param non-empty-string $var + * * @return string|false */ private function getComposerEnv(string $var) diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 7e9d386c2..9796bb3c9 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -49,6 +49,11 @@ class RootPackageLoader extends ArrayLoader */ private $versionGuesser; + /** + * @var IOInterface|null + */ + private $io; + public function __construct(RepositoryManager $manager, Config $config, ?VersionParser $parser = null, ?VersionGuesser $versionGuesser = null, ?IOInterface $io = null) { parent::__construct($parser); @@ -56,6 +61,7 @@ class RootPackageLoader extends ArrayLoader $this->manager = $manager; $this->config = $config; $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser); + $this->io = $io; } /** @@ -93,6 +99,14 @@ class RootPackageLoader extends ArrayLoader } if (!isset($config['version'])) { + if ($this->io !== null && $config['name'] !== '__root__') { + $this->io->warning( + sprintf( + "Composer could not detect the root package (%s) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version", + $config['name'] + ) + ); + } $config['version'] = '1.0.0'; $autoVersioned = true; } diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 3c971d105..33ff62056 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -55,6 +55,8 @@ class Platform /** * getenv() equivalent but reads from the runtime global variables first * + * @param non-empty-string $name + * * @return string|false */ public static function getEnv(string $name) @@ -99,6 +101,7 @@ class Platform return Preg::replaceCallback('#^(\$|(?P%))(?P\w++)(?(percent)%)(?P.*)#', static function ($matches): string { assert(is_string($matches['var'])); + assert('' !== $matches['var']); // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons if (Platform::isWindows() && $matches['var'] === 'HOME') { diff --git a/tests/Composer/Test/Command/LicensesCommandTest.php b/tests/Composer/Test/Command/LicensesCommandTest.php index fe394febc..b48e625f3 100644 --- a/tests/Composer/Test/Command/LicensesCommandTest.php +++ b/tests/Composer/Test/Command/LicensesCommandTest.php @@ -23,6 +23,7 @@ class LicensesCommandTest extends TestCase $this->initTempComposer([ 'name' => 'test/pkg', + 'version' => '1.2.3', 'license' => 'MIT', 'require' => [ 'first/pkg' => '^2.0', @@ -57,7 +58,7 @@ class LicensesCommandTest extends TestCase $expected = [ ["Name:", "test/pkg"], - ["Version:", "1.0.0+no-version-set"], + ["Version:", "1.2.3"], ["Licenses:", "MIT"], ["Dependencies:"], [], @@ -88,7 +89,7 @@ class LicensesCommandTest extends TestCase $expected = [ ["Name:", "test/pkg"], - ["Version:", "1.0.0+no-version-set"], + ["Version:", "1.2.3"], ["Licenses:", "MIT"], ["Dependencies:"], [], @@ -118,7 +119,7 @@ class LicensesCommandTest extends TestCase $expected = [ "name" => "test/pkg", - "version" => "1.0.0+no-version-set", + "version" => "1.2.3", "license" => ["MIT"], "dependencies" => [ "dev/pkg" => [ diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index acab3ccc9..331ae0017 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -573,7 +573,7 @@ OUTPUT; public function testSelfAndNameOnly(): void { - $this->initTempComposer(['name' => 'vendor/package']); + $this->initTempComposer(['name' => 'vendor/package', 'version' => '1.2.3']); $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'show', '--self' => true, '--name-only' => true]); @@ -591,7 +591,7 @@ OUTPUT; public function testSelf(): void { - $this->initTempComposer(['name' => 'vendor/package', 'time' => date('Y-m-d')]); + $this->initTempComposer(['name' => 'vendor/package', 'version' => '1.2.3', 'time' => date('Y-m-d')]); $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'show', '--self' => true]); @@ -599,7 +599,7 @@ OUTPUT; 'name' => 'vendor/package', 'descrip.' => '', 'keywords' => '', - 'versions' => '* 1.0.0+no-version-set', + 'versions' => '* 1.2.3', 'released' => date('Y-m-d'). ', today', 'type' => 'library', 'homepage' => '', diff --git a/tests/Composer/Test/Command/ValidateCommandTest.php b/tests/Composer/Test/Command/ValidateCommandTest.php index 58d204364..363cd0391 100644 --- a/tests/Composer/Test/Command/ValidateCommandTest.php +++ b/tests/Composer/Test/Command/ValidateCommandTest.php @@ -33,7 +33,7 @@ class ValidateCommandTest extends TestCase $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); } - public function testValidateOnFileIssues(): void + public function testValidateOnFileIssues(): void { $directory = $this->initTempComposer(self::MINIMAL_VALID_CONFIGURATION); unlink($directory.'/composer.json'); @@ -45,7 +45,7 @@ class ValidateCommandTest extends TestCase $this->assertSame($expected, trim($appTester->getDisplay(true))); } - public function testWithComposerLock(): void + public function testWithComposerLock(): void { $this->initTempComposer(self::MINIMAL_VALID_CONFIGURATION); $this->createComposerLock(); @@ -53,7 +53,9 @@ class ValidateCommandTest extends TestCase $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'validate']); $expected = <<Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version +Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version +./composer.json is valid but your composer.lock has some errors # Lock file errors - Required package "root/req" is not present in the lock file. This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. @@ -64,12 +66,12 @@ OUTPUT; $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); } - public function testUnaccessibleFile(): void + public function testUnaccessibleFile(): void { if (Platform::isWindows()) { $this->markTestSkipped('Does not run on windows'); } - + $directory = $this->initTempComposer(self::MINIMAL_VALID_CONFIGURATION); chmod($directory.'/composer.json', 0200); @@ -105,11 +107,15 @@ OUTPUT; public static function provideValidateTests(): \Generator { - + yield 'validation passing' => [ self::MINIMAL_VALID_CONFIGURATION, [], - './composer.json is valid', + <<Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version +Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version +./composer.json is valid +OUTPUT ]; $publishDataStripped= array_diff_key(