Problem/Motivation
Right now, we have a method in ComposerUtility called getInstalledPackages(), which uses Composer's API to return a keyed array of PackageInterface objects.
Proposed resolution
-
Replace that with two classes: InstalledPackage, which is a simple read-only value object representing a single installed package, including the information that can be gleaned from installed.php. There would also be an InstalledPackagesList object, which would include most of the functionality currently in ComposerUtility, but its getInstalledPackages() method (or some other name) would return InstalledPackage objects, not PackageInterface. This class would rely on using the ComposerInspector server to run
composer show, and never try to load Composer's API.Unfortunately
composer showwhen used as a list will not return back thetype😤. So to get around this we loadcomposer.lockto determine the type. (We can fix that when https://github.com/composer/composer/pull/11340 is merged and released in Composer itself.) - Calling
composer showinstead of using the Composer PHP will be slightly slower than our current use of Composer's PHP API. So we will cache the package lists if the composer.lock file has not changed (based on its SHA-256 hash).In unit and kernel tests, the hope is that we will able to completely avoid calling
composer show, instead just creating InstalledPackageList objects directly and mocking the Composer inspector to return them. For validators which need to analyze installed packages, this should generally allow us to move away from fixture manipulation. - In subsequent issues, we'll change our validators to use this new API instead of ComposerUtility. It's too much work and complexity to do it all in this one issue.
Issue fork automatic_updates-3334994
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
tedbowI started to look at this I don't think using
installed.phpwill be as simple as taking our use of it inComposerUtilityand copying it into a new class.I was looking at my local
vendor/composer/installed.phpwhich I have in my core clone.There are packages in there that do show up when I run `composer show` from my command line.
for instance installed.php has
but if I run
composer show --path --format=jsonit will not be in that listAlso true for
drupal/core-serializationI am sure we could filter the list in
installed.phpin. some way to get us what we want but we don't actually want to useinstalled.phplong term and I doubt we would get into core if we were scanning this file to determine which packages are installed.I think instead we should do something like #3316668: ComposerSettingsValidator should run `composer config` to determine if HTTPS is enabled does and just the json directly from invoking the composer runner with
--format=jsonComment #4
wim leersActually, it looks like this is a duplicate of #3307365: Use Composer's internal API to access ::getInstalledPackages()…
Comment #5
tedbowI closed #3307365: Use Composer's internal API to access ::getInstalledPackages()
I should have postponed this issue on #3335908: The 'fake_site' fixture cannot be using with `composer show` because the packages are not installed but too late now because it is fixed. This is ready to be worked on again
Comment #6
wim leers@tedbow: I think this is
core-mvpbecause it blocks #3316368: Remove our runtime dependency on composer/composer: remove ComposerUtility? Please confirm 😊Comment #7
tedbowcore-mvp, also critical because #3316368: Remove our runtime dependency on composer/composer: remove ComposerUtility is critical
Comment #10
wim leersRegarding
As surfaced at https://drupal.slack.com/archives/C7QJNEY3E/p1675885515350319?thread_ts=..., the current code:
… is wrong. It doesn't do what it documents nor what this issue claims.
That is not looking at installed packages. That is looking at packages available in local (aka path) repositories…
So any
FixtureManipulator::addPackage()calls that specified a version will not be returned. Because those packages do not exist in a local repository. Unlessinstall_pathis specified, in which case the fixture manipulator generates a local repository — meaning this AFAICT only works by accident?! 😳 I feel like I'm missing something still though…In any case, this is what I just spent a few hours pulling my hair out over. This is now for sure a hard blocker for #3340022: Tighten ComposerPluginsValidator: support only specified version constraint.
Comment #11
tedbowI thing I am starting to realize is that with this new `InstalledPackagesList` using `composer show` we need to have real packages installed for it to work.
So all our tests that use our FixtureManipulators are not going to work because they don't actually install packages.
We could update them to use real composer operations. This though would really slow down our tests. I notice our kernel tests already when from 1:30 to over 2 minutes. My suspicion is that it is because all of the new Composer calls for ComposerInspector.
If we had even more calls to install packages and many `composer show` calls I think this is going very slow.
We could put a caching layer in for the
InstalledPackagesList. Probably if the composer.lock file has not changes we could used a cached list.Another option to speed up the tests/mock versions of the InstalledPackagesList objects in most of our tests. This way we would not need to make
composer showcalls every where. We could add extensive test for InstalledPackagesList itself to ensure that returns back the correct results but then in most of our tests just mock the results from InstalledPackagesList.UPDATE: I think it is actually this issue that makes kernel test run much slower. I am actually only using the new InstalledPackagesList in 1 validator, ComposerPatchesValidator. but since it listens to 3 events and would called during most other kernel tests this could have a big affect. Will push up a commit with reverted to see.
Comment #12
tedbowComment #13
tedbowUnassigning adding 3) to proposed solution which is replace calls to certain methods in
ComposerUtilitywith calls toInstalledPackagesListclass.These replacements will not mostly not need test change because it is just the source of the list that is changing
Comment #14
omkar.podey commentedComment #15
omkar.podey commentedThe error i am running into is
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : The "package_manager.composer_inspector" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.i saw that we have
\Drupal\package_manager_bypass\TestComposerInspectorbut that doesn't help either. So i'm blocked .Comment #16
omkar.podey commentedComment #17
tedbowpackage_manager.composer_inspectorservice haspublic: falsewhich means it is only available via dependency injection so\Drupal\Tests\package_manager\Kernel\InstalledPackagesListTest::registerfor how to get around this in tests.But specifically where you were going us this in
\Drupal\Tests\package_manager\Kernel\FakeSiteFixtureTest::testExpectedPackagesI think this is test is outdated and we should have removed it in #3335908: The 'fake_site' fixture cannot be using with `composer show` because the packages are not installed because the purpose of this test was to make sure our installed.json and installed.php files were kept in sync because we just to make them manually now they we make them via the script\Drupal\automatic_updates\Development\ComposerFixtureCreator::createFixturewith an actual composer install I think we need this test.Comment #18
omkar.podey commented👍 working on it
Comment #19
omkar.podey commentedComment #20
tedbowWe can continue working on this but I think it should land in 3.x not 2.x see #3340892: Tag 8.x-2.7, and move development to new 3.x branch to focus on Core inclusion for reasons
Comment #21
phenaproximaComment #22
phenaproximaComment #23
phenaproximaBack to you, @omkar.podey!
As far as I know, what's still needed here is to replace all calls to ComposerUtility::getInstalledPackages() and ComposerUtility::getInstalledPackagesData() with calls to ComposerInspector::getInstalledPackagesList().
Feel free to use any PHP 8.1 features you've been itching to try ;)
Comment #24
omkar.podey commentedThe two places outside of
automatic_updates_extensionsthat i didn't updatesrc/Validator/ScaffoldFilePermissionsValidator.phpbecause // We don't have a getExtra() function yet .package_manager/src/Validator/ComposerPatchesValidator.phpneeds extra logic to switch between active and stage.Comment #25
omkar.podey commentedI think we need
getProjectForPackageand something similar togetPrettyVersioninpackage_manager/src/InstalledPackagesList.php, the commit i just pushed above should fail as we don't have the functions yet.Comment #26
phenaproximaYou don't need it :) You can call
$this->composerInspector->getConfig('extra')and parse the output as JSON.Right you are. I've added
InstalledPackage::getProjectName()for this purpose, andInstalledPackagesList::getPackageByProjectName()for the inverse. (Note that the latter now returns an InstalledPackage or null, but never a string.)Comment #27
phenaproximaWhoa. Didn't mean to turn this completely green, but hey...just goes to show that sometimes a few small changes have outsized effects.
I guess this is reviewable now...?
Comment #28
omkar.podey commentedComment #29
omkar.podey commentedSo #3342227: ScaffoldFilePermissionsValidator should use ComposerInspector instead of ComposerUtility is what we decided to go with i have pushed the change there which needs @travis.carden's input.
Comment #30
wim leersEpic progress here! 🤩 Left a range of suggestions & questions.
Comment #31
phenaproximaInteresting - I think the addition of
strict_types = 1has revealed an existing problem here!xdebug tells me that, in certain cases, the realpath being computed for a (fake) installed package is wildly wrong. Example: in CronUpdaterTest::testUpdaterCalled(), at some point InstalledPackage tries to get a realpath for
/private/tmp/package_manager_testing_roottest46267339/stage/gypHULuVrCAuBshO10VGa-1tjDkH1ZMWgWDzSQQzWYk/vendor/composer/../drupal/core-dev...but/private/tmp/package_manager_testing_roottest46267339/stagedoesn't even exist yet.I'm happy for anyone else to try and fix this, because I believe it's a pre-existing condition that our code simply wasn't strict enough to find until now.
Comment #32
phenaproximaFound the problem: FixtureManipulator::setPackage() is faulty. It's towards the end, when it rewrites installed.php.
Here's the thing: when Composer generates installed.php, it uses the __DIR__ constant quite liberally because it knows that the package paths are always relative to some vendor directory. But that's not what FixtureManipulator is doing. It's just stripping off the full path of the vendor directory, effectively converting the install path to an absolute path.
But that's no good, because a) it's not realistic, and b) if that rewritten installed.php is moved to some other place, the paths break because they may no longer be accurate. InstalledPackage.php's strict typing is detecting this because realpath() is producing a FALSE, rather than a string.
I think the right approach here is for FixtureManipulator to do what Composer would do and write __DIR__ into installed.php, keeping the paths relative. That's previously what I programmed it to do, and indeed that code is still there, at the end of setPackage():
But that's not having an effect anymore. I presume that's because of this, but the code is hard to understand, so I'm not sure. This feels really haphazard.
We should open another issue to fix that, since it may break some tests (which, it should be clear, are probably having erroneous expectations now). That issue will have to block this one, I'm afraid.
Comment #33
phenaproximaEDIT: This is gonna take some more involved work to get it to a sane place.
Comment #35
phenaproximaThis comment from Wim scared the hell out of me:
So I did what I always do: I got into a Zoom with Ted, who is basically my code therapist at this point, to talk about it.
What we concluded is that the scope of this issue is starting to balloon. We are constantly having to adjust fixtures, and the way we manipulate fixtures, as we change our validators to use this new API. I think the reason that is such a painful and omnipresent task is because, due to ComposerUtility and the way it works, we are tightly coupled to Composer's internals. (This has happened over time; it started as a good idea, but has mutated into something much more complicated and sinister.) This is the reason we're having to add things like TestComposerInspector -- because if we tried to always directly use
composer showto read fixtures, we might suffer from performance issues, and we'd need the fixtures to always be 100% complete and correct. (Although, to be clear, they are significantly better than they used to be -- but as #32 demonstrates, there are still problems.)The addition of InstalledPackage and InstalledPackagesList is really meant to help us move away, in many cases, from fixture manipulation, rather than further entrench the need for it. For example, if a validator's test coverage needs a list of installed packages, it should be possible for the test code to simply define an array of installed packages, rather than have to prepare a fixture in the exact right way. What's not totally clear to Ted and I is whether it's "better" to rely on fixture manipulation, or adopt the use of light mocking (i.e., by directly creating InstalledPackageList objects in test code, and telling ComposerInspector to return them). We need to discuss that with Wim.
But what is clear is that such a decision is only going to come into play when we actually start adopting InstalledPackageList in our validators. That, we think, should happen in separate issues, so that the decision can be made case-by-case whether to test any given validator (or, really, any given code path) by manipulating a fixture, or by mocking an InstalledPackageList.
But, either way, we need to add these new classes, with solid unit-level test coverage. So I've opened another MR to do just that. It only adds the stuff we need, and doesn't bother trying to convert any validators to use it. I think we can comfortably commit that -- as far as I know, all of Wim's feedback is addressed -- and then adopt the new system piecemeal in follow-ups.
Comment #36
phenaproximaComment #37
wim leersThis now also blocks #3337760: ComposerPatchesValidator should use ComposerInspector instead of ComposerUtility.
Comment #38
tedbowsee MR comments
Comment #39
phenaproximaComment #40
tedbowLooks good!
Comment #42
phenaproximaOnward and upward!
Comment #43
wim leers🥳 This unblocked #3340022: Tighten ComposerPluginsValidator: support only specified version constraint and #3342227: ScaffoldFilePermissionsValidator should use ComposerInspector instead of ComposerUtility!
Comment #44
wim leersThis did fail to update the documentation and it did not provide a change record. I'll take care of the documentation in #3318306: Define the Package Manager API (package_manager.api.php is outdated). Please ensure that every infrastructure change comes with explicit CRs + docs from now on.
Comment #45
phenaproximaWill do. Sorry about that. I feel appropriately chastised. 🥺
Here's the change record: https://www.drupal.org/node/3343718
Comment #46
wim leersNot the intent!
The intent is just to stop making the docs outdated, and then taking weeks/months to get them up-to-date. Now that we're close to core, we need to start treating the docs equally important as the code 😊
We just collectively need to change our habits!