Problem/Motivation
Permissions issues with scaffold files, which are made unmodifiable by Drupal's permissions hardening code, is the most commonly reported problem with the Core Scaffolding Plugin. Presently, every scaffold operation rewrites every scaffold file, regardless of whether it has changed or not. This can cause spurious failures, e.g. if an unmodified scaffold file is not writable. The scaffold files protected by permissions hardening are usually not modified by users, so avoiding rewrites of these files will reduce occurrences of this problem substantially, with most users only affected when an example file in sites/default
is updated by a Drupal upgrade.
The re-scaffolding also produces a lot of extraneous output to the console, as the Scaffold Plugin reports on every file that it writes. In contrast, other Composer commands, e.g. `composer install`, only print output when they are actually doing something, and remain silent in other cases, e.g. if a package has already been downloaded.
Proposed resolution
Do not scaffold a file unless the on-disk version differs from what would be written. Show the scaffold information only for files that are actually written.
Remaining tasks
Commit
User interface changes
Users no longer see excessive information about scaffold files being re-copied at the end of every composer command (install, update, etc).
API changes
None. Only internal to composer plugins (which aren't API). See #3103918: [policy + docs] Decide on backwards compatibility policy for Composer plugins in Drupal 8.
Data model changes
None.
https://www.drupal.org/project/drupal/issues/3091285
Release notes snippet
The output of the `composer scaffold` command is now much less verbose. Scaffold operations are only displayed on the first run, or when a file is changed (e.g. rewritten). This change also greatly reduces the number of times that scaffold files are rewritten, which avoids the common problem with permissions hardening that causes scaffolding to fail after the Drupal installer runs.
Comment | File | Size | Author |
---|---|---|---|
#51 | 3103090-8.9.x-51.patch | 29.91 KB | jungle |
#51 | raw-interdiff-49-51.txt | 1.73 KB | jungle |
#49 | 3103090-9.x-49.patch | 29.64 KB | greg.1.anderson |
#49 | 48-to-49-interdiff.txt | 1.1 KB | greg.1.anderson |
#48 | 3103090-9.0.x-48.patch | 29.64 KB | greg.1.anderson |
Comments
Comment #2
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedHere is a patch. This is a reduced version of the work from #3092563: Avoid overwriting .htaccess changes during scaffolding > security problem, so I have included an interdiff for the benefit of anyone coming here from that issue.
Comment #3
Mile23Cool. So we want to filter out Ops for files that we don't need to update by comparing their destination content before we do a file operation. That way we can avoid (some) errors for hardened files and the user doesn't see a wall of output per scaffolding run. We get that from ScaffoldFileCollection::checkUnchanged().
As for a review: Looks good. Mostly some minor stuff.
Needs a docblock.
Farewell, ConjunctionOp. However, there are still references to it in docblocks in ScaffoldFileCollection, maybe other places too.
Contents of the destination file, not contents of the scaffold file(s).
Comment #4
dww+1 to improving the UX here.
While we're nitpicking, a few more nits / questions / concerns:
s/consiteration/consideration/
s/rewritng/rewriting/
s/item/items/
This seems to be a local variable for this method only. Isn't our convention to use
$has_at_least_one_with_content
for these, instead?This seems like an important API change. Without looking more closely, not sure why we're changing the nature of the iterator for this operation and why this is safe/okay. Maybe some code comments would help?
Excellent comment, thanks! Do we want a @todo / @see for "we cannot tell the difference..." part?
can this be ===?
Seems good but not obvious why this is in scope with the current issue...
More good test cleanup that should maybe be a follow-up to make this smaller / easier to review?
Thanks!
-Derek
Comment #5
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commented#3.1: Added docblock.
#3.2: The code that combined operations into conjunction operations is still present and only slightly changed by this patch; that's why references to 'conjunction' still existed. I renamed these methods for more consistency and clarity with the new algorithm.
#3.3: Improved docblock comment.
#4.1, #4.2: Fixed typos.
#4.3: Fixed Drupal coding convention.
#4.4: This question also came up in #3092563-51. My answer from last time:
ArrayIterator seems like a good choice for a method called getIterator. I'm not sure why the old code was using a recursive array iterator, but seeing as the method was unused, perhaps it does not matter.
#4.5: Added a reference to the issue that provides more code to allow us to distinguish between these two cases.
#4.6: Used `===`; probably does not matter, as contents should never be any type other than a string.
#4.7, #4.8: These changes are for another patch. They are simplifications to the tests, not necessary here, so I backed them out.
Comment #6
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedBack to 'needs review' to run tests.
Comment #7
Mile23I really wish we could get over the use of the term 'nitpick.' For instance:
I mean this, which is still there:
It's confusing to read if there's no such thing as a ConjoinOp. The curse of good inline documentation is that you have to keep it updated...
Re: #4.4: I'd argue that there's no such thing as an API change in a Composer plugin. You can't subclass anything from a plugin package without also inheriting its events and behaviors. You pretty much have to fork a Composer plugin if you want to change it.
In fact, we might consider marking our plugins @internal just to be explicit. That would be a follow-up.
Also #4.8: Those tests are from before build tests were available. We might consider figuring out if we should convert them to build tests for a little less complexity, or if they'd get more complex in the process...
Generally +1 on everything else.
Comment #8
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedDarn it, I thought I searched and found all of the conjunction / conjoin references. I'll go back and clobber the rest.
Comment #9
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedAlso, that was an explanation, not an excuse. :) The feedback was valid and appropriate. These should all be gone now.
Comment #10
Mile23Ah, mixup on my part. I meant to direct the 'nitpick' gripe more towards @dww, which I really shouldn't have done anyway, but now I messed it up and here we are.
+1 on docs change, and thanks.
Comment #11
dwwRe: "nitpicking" in #4: Sorry that came across as a criticism of the review in #3! Totally unintentional. What I really meant is:
"While this needs work and will be re-rolled, here's a mostly nitpick-ful review for other minor things to fix about this otherwise nice-looking patch."
@Mile23 sorry that you took offense. Definitely agree that the points in #3 (while mostly cosmetic) are totally valid.
@greg.1.anderson thanks for re-rolling to address all the feedback!
One very minor remaining thing I see:
UberNit: Core's standards for doc blocks are action verbs that end in 's'. E.g. s/Return/Returns/ and s/Filter/Filters/.
Otherwise, I'd RTBC, except I can't figure out how to actually test this patch. ;) If I start from a git checkout of 8.9.x branch, the patch applies, but the composer.json isn't setup as a real composer-based site, scaffolding doesn't happen at all, and there's nothing to see/verify about this change.
If I start from
composer create-project drupal/recommended-project 8_8_composer --no-interaction
I get a test site that uses the scaffolding stuff, see the problem, but if I try to install this patch in the site, it doesn't apply at all. There is no
composer
subdir of either the8_8_composer
root dir,web
norweb/core
. It seems the relevant composer scaffolding stuff is now invendor/drupal/core-composer-scaffold
but with a different structure than what this patch expects.How does one actually test this on a working composer-based site? ;)
Thanks!
-Derek
Comment #12
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commented@dww The easiest way to set up to do ad-hoc testing on this site is to run the functional tests like this:
SCAFFOLD_FIXTURE_DIR=$HOME/tmp/scaffold-fixtures ./vendor/bin/phpunit -c core --group Scaffold
Then you can cd into the fixtures that are created inside
$HOME/tmp/scaffold-fixtures
, and runcomposer scaffold
et. al. on some of the fixtures you find there, like drupal-drupal or drupal-composer-drupal-project (n.b. these fixtures are not actually very similar to their namesakes).If you want to test this on an actual site you have set up locally, copy the path repository directives from those fixtures into your site's composer.json file.
Comment #13
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedHere's fix for the comment summary conventions.
Comment #14
dwwRe: #12: Perfect, thanks! That works great.
Before
After
Any ideas why with the patch applied, after removing only
index.php
, we're getting a notice about.htaccess
?Otherwise, this seems great and RTBC. I have no further concerns on the patch / docs. However, I'll hold off on the status change until confirmation around the
.htaccess
thing.Thanks again,
-Derek
Comment #15
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedThe .htaccess thing is by design. The message you are seeing is letting you know that .htaccess was NOT written, and the reason why it was not written.
Prior to this issue, all messages were printed all the time.
After this issue, we only print messages when the operation changes a file. But what about those notification messages, like the .htaccess override? They never change files. These messages, then, are printed any time any other file change happens as the result of any other operation from the same package changing the contents of a file. This means that the notifications are printed on the first run, when all messages are printed, and they are not printed on a run where no files are changed (so no output at all is produced).
Comment #16
dwwOkay, basically makes sense. However, in that case, why aren't we seeing the notice about other skipped files? E.g:
Comment #17
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedProbably because I still have one more bug to track down.
Comment #18
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commented#16 actually isn't a bug.
The robots.txt file still exists on disk, so the Scaffold plugin knows that it wasn't modified, and it filters it out. In the case of the .htaccess file, though, there is no file on disk. Therefore, the Scaffold plugin figures it had better write it back to disk again, but all it has is the "skip" message. The important difference here is that .htaccess is set to 'false', whereas the robots.txt file is appended, so there are contents for the plugin to compare against.
Comment #19
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedWe could improve this behavior if the Scaffold plugin recorded something about the state of the source file at the time it was written (e.g. its hash), but in this issue we do not support that.
Comment #20
dww@greg.1.anderson: Thanks for investigating and clarifying! All makes sense. I was about to RTBC, but I realized we should probably have a test that shows we don't try to scaffold or print out anything about an un-touched file. Sorry for not noticing this and mentioning it earlier. I looked at all the test changes in the patch and don't see anything like this. Would you be willing to add something?
Perhaps also attach a test-only patch that shows the test failure without the fixes applied, and the full patch with the fixes and test that should be green?
Thanks/sorry!
-Derek
Comment #21
dwwPasting Slack exchange here to clarify:
greg.1.anderson 10 minutes ago
@dww Sure, that sounds reasonable. To be clear, the only test you want is one that confirms nothing is printed on the second run when running composer scaffold (or equiv) twice, yes?
dww 9 minutes ago
Yeah, basically.
dww 9 minutes ago
Obv, that command always prints something. ;)
dww 7 minutes ago
Ideally, something more or less like the commands / output I ran as before/after in comment 14.... Do initial scaffolding, verify it worked. Run scaffold again, expect to see nothing. Remove a file. Run scaffold again, expect to see only the file I removed as re-written.
dww 6 minutes ago
I assume "Do initial scaffolding, verify it worked." already exists as a test somewhere... so this would be more actions + assertions at the end of the same test method...
Comment #22
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedIronically, #20 is related to #4.7 and #4.8. I had tests like this in the issue that I forked this from, but backed out too much when I ported over to here, and then removed the rest per #4. No worries, though; the suggested test is valuable. I should have noticed it was missing when I looked at #4. I'll add it back in.
Comment #23
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedHere are the moar tests, along with a failing test.
Oh, the failing test is also by coincidence the interdiff from the last patch.
Comment #25
dww@greg.1.anderson: Fantastic! That's (almost) ideal.
I was about to RTBC (again!), but then I looked at the test results for the green patch. Feels cruel to do this, but NW for the CS warnings:
5 coding standards messages
✗ 5 more than branch result
/var/lib/drupalci/workspace/jenkins-drupal_patches-23927/source/composer/Plugin/Scaffold/Operations/ScaffoldFileCollection.php ✗ 3 more
line 82 TRUE, FALSE and NULL must be uppercase; expected "FALSE" but found "false"
87 Expected 1 space after ELSEIF keyword; 0 found
90 TRUE, FALSE and NULL must be uppercase; expected "TRUE" but found "true"
/var/lib/drupalci/workspace/jenkins-drupal_patches-23927/source/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php ✗ 2 more
153 Expected 1 blank line after function; 2 found
156 The closing brace for the class must have an empty line before it
I'd just fix these myself, but then I couldn't officially RTBC.
Sorry!
-Derek
Comment #26
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentednp, thanks for the review. Here are the cs fixes.
Comment #27
dwwFantastic, thanks! 🙏
No moar complaints. ;) Removing tag. 👍
Ready To Be Committed (the original meaning of "RTBC") 🎉
Comment #28
dwwMinor cleanup:
- More descriptive title of what this patch actually does.
- Hide some stale files.
- Minor summary updates / clarifications.
Comment #29
Mile23Just a +1 that the test in #23 does in fact check that we don't re-scaffold files that are already present.
Then in #3092563: Avoid overwriting .htaccess changes during scaffolding > security problem we can add more complex UX type changes as to how to handle files that the user has altered but didn't set up their own scaffolding for.
Comment #30
larowlanfor API changes - can you point me to that policy - thanks.
we seem to have lost the trim in the new logic - does that matter?
is there something to prevent the prepend/append happening every time?
e.g. the file is originally made up of original contents, but then on the first pass we get prepend/original/append - what mechanism is there to prevent us getting prepend/prepend/original/append/append on subsequent attempts?
🧐 nit: projects
🧐 nit: we can typehint $files_to_filter as an array
🧐nit: should use the third argument on in_array because strings
🧐 nit: we can continue here and then make the elseif an if
🧐 nit: I think we can break here or
continue 2
- we've found one file with content for this project, so we can plough on?If we do that, we don't even need the local $has_at_least_one_with_content variable I think 🤔
😎
I think this answers my question above about 'what safeguards are there' to prevent subsequent prepending appending?
🤔 nit: I think we have one too many mustExecs here?
Comment #31
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commented1. Opened #3103918: [policy + docs] Decide on backwards compatibility policy for Composer plugins in Drupal 8 to track this. Note that "compatibility" for a Composer plugin has a different definition than for other sorts of APIs. In some ways a Composer plugin is more flexible to being changed without impacting any users. Other kinds of changes, though, can cause
composer update
to crash, as outlined in the referenced issues. These sorts of changes cannot simply be postponed until the next stable release, as they would also cause acomposer update
crash when moving, say, from 8.9.x to 9.0.x.I will make a test to guard against these sorts of changes so that we do not accidentally get ourselves into a position where it's hard for users to upgrade Drupal versions only due to Composer plugins.
2. The old logic used 'trim' when determining whether the append / prepend contents were empty. The new logic does not care whether the append / prepend contents are empty; it is now concerned with the final file contents. We could still trim the append / prepend file contents before adding it to the original scaffold file contents. That didn't seem necessary to me, but I'd be fine with doing that if anyone thought it was preferable. The current algorithm does not trim the contents before appending / prepending, though, only for comparing.
3. The answer in 10. is correct.
I'll update the patch to address the other review points. I prefer to avoid `break` and `continue` most of the time; I will either find another way to avoid the elseif, or just go ahead and address as suggested.
Setting to "needs work" for 1.
Comment #32
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedPostponed on #3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems .
Comment #33
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedFor what it's worth, I merged #3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems in here, and with slight modifications, the tests passed. (Slight modifications were needed because the tests as written in the other patch used output in stdout to determine that the scaffold plugin was working, but this patch optimized away that output because the scaffold operation wasn't necessary.)
I split out #3104922 because I feel that the Scaffold plugin shipped in 8.8.0 and 8.8.1 is a little dangerous, and I'd like to see that patch get into 8.8.2, so that we have the safer version available as soon as possible.
Comment #34
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedUn-postponed here now that #3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems is in. Will re-roll shortly.
Comment #35
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedI updated the Issue Summary to more correctly classify this issue as a bug, as the primary benefit of this issue is avoiding scaffold failures after permissions hardening.
30.1. I updated the issue summary in #3103918: [policy + docs] Decide on backwards compatibility policy for Composer plugins in Drupal 8 to contain proposed text clarifying this policy. #3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems has been committed, providing protection against changes to the Scaffold plugin which might cause update problems.
I still need to make minor changes to address #30 and re-roll.
Comment #36
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commented#30.1 - #30.3: Addressed above.
#30.4 - #30.6: Fixed minor defects as identified.
#30.7 & #30.8: Refactored for better code hygiene.
#30.11: Removed extraneous scaffold operation.
Could someone be so kind as to review here and set to POSTPONED (on #3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems ) if it's RTBC, or set it to NEEDS WORK if it's not?
Comment #37
Mile23#3104922: Guard against changes to Scaffold Plugin that might cause upgrade problems is in for 8.9x and 9.0.x, with a possible 8.8.x backport.
LGTM.
Comment #39
ravi.shankar CreditAttribution: ravi.shankar at OpenSense Labs commentedIt seems unrelated test failures, so back to RTBC as per comment #37.
Comment #40
alexpottWe need a roll here.
Comment #41
ravi.shankar CreditAttribution: ravi.shankar at OpenSense Labs commentedComment #42
ravi.shankar CreditAttribution: ravi.shankar at OpenSense Labs commentedHere I have added re-roll of patch #36.
Comment #43
Mile23Back to RTBC based on #37. Thanks, @ravi.shankar
Comment #45
alexpott9.x and 8.x have diverged for COmposerHookTest and the new test is using things deprecated in Drupal 9. So here's a 9.x patch - plus the old 8.x patch for good measure.
Comment #46
alexpottThis is looking pretty good. Whilst I was the originator of the conjoining approach I was always a little unsure of the approach - I felt it was the best we had at the time. This change (that removes it) I think is a move in a really good direction and the system is clearer for it. Nice work.
If we make this final then we enforce the relationship between this and generateContents() which seems a good idea to me. It feels like the point here is to not generate contents more than once AND which also sort of enforces the calling contents twice on the smae operation cannot return something different - which feels an important thing.
I think the
to be cached
is not necessary - it feels like an implementation detail.I think the purpose of the this method is get the contents of the file with the operation applied.
Can we typehint on SomeObject[]
Can we typehint to ScaffoldFileInfo[]?
Does this hint that we should have a $scaffold_file->hasChanges() method that does something like
Comment #47
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedI agree with all of the suggestions in #46. I'll roll a new patch as soon as I get a chance, if someone else doesn't pick it up first.
Comment #48
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedHere are fixes for #46 for the 9.0.x branch. Don't quite have time to do a quick back-port to 8.9.x at the moment.
Comment #49
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedCoding standards.
Comment #50
jungleOn rerolling a patch for 8.9.x
Comment #51
jungleassertStringContainsString()
andassertStringNotContainsString()
are not available in 8.x, but with the forwards-compatibility shim introduced in #3126797: [D8 only] Add forwards-compatibility shim for assertString(Not)ContainsString()replacements in phpunit 6&7, it's good to go.Comment #52
alexpottAll my feedback from #46 has been addressed thanks @greg.1.anderson.
Comment #53
alexpottCommitted and pushed c1548fce81 to 9.1.x and 4b53364f22 to 9.0.x. Thanks!
Committed 39ec77f and pushed to 8.9.x. Thanks!
I backported this to 8.9.x (would love to put it in 8.8.x too but let's not) - because this addresses a very annoying bug in scaffold and allows most users to also fix #3091285: Composer scaffolding fails when permissions on default.settings.yml or default.settings.php is not writable..
Comment #57
jungle@alexpott, thanks for committing!
Test ...
should beTests ...
, but the patches were committed, and found there are more occurences, so filed #3133162: Replace the start verb Test with Tests in method comments of testsComment #58
greg.1.anderson CreditAttribution: greg.1.anderson at Pantheon commentedYay! Thanks for committing! This should help the experience of scaffolding a whole bunch.