fix(docker): install the composer dependencies of cloned shipped apps - #1064
fix(docker): install the composer dependencies of cloned shipped apps#1064luflow wants to merge 2 commits into
Conversation
| */ | ||
| verbose: boolean | ||
| /** | ||
| * Working directory to run the command in. Defaults to the container's working directory. |
There was a problem hiding this comment.
| * Working directory to run the command in. Defaults to the container's working directory. | |
| * Working directory to run the command in. Defaults to the Nextcloud root. |
There was a problem hiding this comment.
(or similar - as otherwise you would need to know the container structure)
There was a problem hiding this comment.
Applied, thanks. It's also more accurate than what I had, since runExec(['cat', 'core/shipped.json']) and runOcc already rely on that default.
| async function completeClonedApp(app: string, container: Container) { | ||
| const appPath = `/var/www/html/apps-writable/${app}` | ||
| if (!await isComposerAutoloaderBroken(appPath, container)) { | ||
| return | ||
| } | ||
|
|
||
| console.log(`│ ├─ ${app} was cloned without the Composer dependencies its autoloader needs`) | ||
| if (await installComposerDependencies(app, appPath, container)) { | ||
| return | ||
| } | ||
|
|
||
| await runExec(['rm', '-rf', `${appPath}/composer`], { container }) | ||
| console.log(`│ ├─ Removed '${app}/composer' to make the app loadable without its Composer autoloader`) | ||
| console.log(`│ └─ ⚠️ ${app} is incomplete, code using its Composer dependencies (e.g. 'lib/Vendor') will fail`) | ||
| } |
There was a problem hiding this comment.
Why not simply:
IF composer.json exists
THEN composer install --no-dev
?
There was a problem hiding this comment.
That was my first attempt, but it breaks on the two different layouts these apps use.
viewer and text have a second composer project in composer/composer.json with "vendor-dir": ".", so composer/ is their vendor dir and it's committed (composer/composer/autoload_real.php is right there in the repo). Their root composer.json is just dev tooling: viewer has an empty require, text only requires php.
notifications is the newer layout. Root composer.json is the runtime project, the vendor dir is the default vendor and git ignored, and composer/autoload.php is a hand written shim that requires it.
So "root composer.json exists, run install" would install the dev tooling project for viewer and text, while their actual runtime autoloader in composer/ is already fine. Nothing gets fixed, it just costs time.
The bigger problem is the combination with the rm -rf composer fallback. If that pointless install fails for any reason, we'd delete a committed, working vendor dir and break an app that was healthy. Running the autoloader only touches apps that would really make the server fatal, and it costs two docker exec round trips per cloned app.
Happy to go your way if you still prefer it. I'd just keep the fallback behind the autoloader check so a working composer/ can never get removed.
|
|
||
| // The server image ships PHP but no Composer, so it is downloaded on demand. | ||
| // Pinned so that a Composer release cannot break every consumer's CI at once. | ||
| const COMPOSER_VERSION = '2.10.2' |
There was a problem hiding this comment.
I doubt this will get updated especially apps will then update this library.
I think the risk of a broken composer release (until its fixed) is lower than being pinned to a vulnerable version.
So I prefer using latest here (you might want to add an environment override option though).
There was a problem hiding this comment.
Or call self-update afterwards
There was a problem hiding this comment.
Fair enough, the vulnerable version argument beats mine. Back to latest-stable with an override:
const COMPOSER_VERSION = process.env.NEXTCLOUD_E2E_COMPOSER_VERSION || 'latest-stable'
@nickvergessen with latest-stable self-update doesn't do anything, so I left it out. Happy to add it if you'd rather pin and update.
I avoided the name COMPOSER_VERSION because a few PHP and composer Docker images set that one already, and picking it up by accident would be confusing. Can rename if you like.
Docker pre-creates `/var/www/html/apps-writable` as root when an app is bind mounted into it, so the `chown` handing the directory to `www-data` has to run as root as well. Without it `configureNextcloud` aborts with "chown: changing ownership of '/var/www/html/apps-writable': Operation not permitted". The app list also has to be built after `apps.config.php` registered `apps-writable` as an apps path, otherwise mounted apps are missing from it and are then installed from the app store, which fails with "<app> already installed". Signed-off-by: Florian Ludwig <florian@krautnerds.de>
Shipped apps that are missing from the server image are installed with a plain
`git clone`, which only works as long as the app commits its composer
dependencies. Since Nextcloud 34 `nextcloud/notifications` no longer does, but
still ships `composer/autoload.php` requiring the git ignored
`vendor/autoload.php`. `OC_App::registerAutoloading()` requires that file, so
every request and every `occ` call dies with:
Failed opening required
'/var/www/html/apps/notifications/composer/../vendor/autoload.php'
#0 lib/private/legacy/OC_App.php(117): require_once()
nextcloud-libraries#1 lib/private/AppFramework/Bootstrap/Coordinator.php(78)
After cloning, the app's composer autoloader is now executed the same way the
server does. If it cannot be loaded, composer is downloaded into the container
and `composer install` is run for the app. Scripts are run on purpose, apps like
notifications only assemble the prefixed copies of their dependencies in
`post-install-cmd`.
If that is not possible, the cloned `composer` directory is removed so the
server registers PSR-4 for the app's `lib` directory itself. The app then loads
but is incomplete, which is logged as such instead of failing hard.
Fixes nextcloud-libraries#1059
Signed-off-by: Florian Ludwig <florian@krautnerds.de>
97897f2 to
e25ec03
Compare
Note
Stacked on #1063 — the first commit belongs to that PR. Please merge #1063 first; without it
configureNextcloudaborts before it ever reaches this code path when an app is bind mounted.Fixes #1059. Companion to nextcloud/notifications#3206, where @nickvergessen pointed at this exact spot in
lib/docker.tsas the place to solve it.Problem
Shipped apps that are missing from the CI server image are installed with a plain
git clone, which only works as long as the app commits its composer dependencies.Since
stable34,nextcloud/notificationsno longer bundles them, but still ships acomposer/autoload.phpdoingrequire_once __DIR__ . '/../vendor/autoload.php'— andvendor/is git ignored.OC_App::registerAutoloading()finds that file, requires it, and everyocccall dies:On
stable33thecomposer/directory did not exist yet, which is why this only surfaces when a suite moves tostable34. 0.4.0 and 0.5.0 are affected alike — the clone logic is unchanged between them.Detection
Not a name list, and deliberately not the obvious heuristic either.
My first attempt was "
composer/autoload.phpexists butvendor/autoload.phpdoes not". That is wrong:viewerandtextcommit a self-containedcomposer/directory whose entrypoint loadscomposer/composer/autoload_real.php, not../vendor/autoload.php. They are complete, and the heuristic triggered an unnecessarycomposer installfor both (caught bynpm run test:node).So instead the app's autoloader is executed the same way the server does:
That detects exactly the apps that would break the server, with no assumption about how an app lays out its dependencies.
Chosen solution:
composer installinside the containerThe server image (
ghcr.io/nextcloud/continuous-integration-shallow-server) ships PHP, git, curl, unzip and thezip/pharextensions, but no composer, so a pinnedcomposer.pharis downloaded once per container.Scripts are run on purpose: apps like
notificationsonly assemble the prefixed copies of their dependencies (lib/Vendor/, viabamarni/composer-bin-plugin+coenjacobs/mozartinpost-install-cmd) there. Skipping scripts would leave the app loadable but still broken for web push.If the download or the install fails, the cloned
composerdirectory is removed instead. The server then registers PSR-4 for the app'slibdirectory itself — thestable33behaviour — so the app loads. It is incomplete, and that is logged loudly rather than passing silently:Apps that do bundle their dependencies are unaffected; the public API is unchanged.
Alternatives considered
composer installon the host, result shipped in viacontainer.putArchive(the workaround by @theCalcaholic in Missing bundled PHP dependencies (stable34) nextcloud/notifications#3206). Works, but adds a host dependency on PHP + composer. Given onubuntu-latest, not given for contributors running the suite locally. The in-container variant behaves identically on Linux runners and on macOS with Docker Desktop.composer/directory (my own workaround in Support installing composer packages for Nextcloud apps #1059). No toolchain needed at all, butlib/Vendor/stays absent, so anything touching web push logsClass "OCA\Notifications\Vendor\Minishlink\WebPush\VAPID" not found. Kept as the degraded fallback, not as the default.vendor/, and it would remove this whole code path. Rejected because it gives up matching the app to the server branch, which is the reason the clone exists.continuous-integration-shallow-server. Until then this keeps consumers working without an image change.composer.pharagainst its published checksum. Dropped: it is served from the same host over the same HTTPS connection, so it proves transfer integrity, not authenticity — while the code deliberately executes third-party scripts from a freshgit clonemoments later. The composer version is pinned instead, so a bad composer release cannot break every consumer's CI at once.Testing
Fork checked out at this branch:
npm ci,npx tsc --noEmit,npm run lint,npm run build— clean (lint: 0 errors; the 8 warnings are pre-existing inlib/commands/*)npm run test:node— 4/4 pass (47s).notificationsadded to the suite plus a new assertion that itsvendor/autoload.phpexists and the app is enabled.viewer/text/formsand not triggering an install — they keep the exact previous behaviour.End-to-end against a real
stable34server, usingnextcloud/attendance(the consumer from #1059) with this build linked in and its test server bumpedstable33→stable34:Completeness checked in the container, not just "it boots":
The degraded path got verified by accident and is worth reporting: an intermediate revision passed an invalid
--no-auditflag. The run then produced exactly the intended behaviour — loud🛑,composer/removed,notifications 8.0.0-dev.0 enabled— andnpm run test:nodewent red, withocc app:listemittingClass "OCA\Notifications\Vendor\…". So the fallback works, is visible, and the new test catches it as a regression. The flag is gone; everything above is from the final revision.Not verified
stable34(only the server setup was exercised; test failures there would be NC33→NC34 UI changes, unrelated to this).notificationshitting this — the detection is generic, butnotificationsis the only app I saw it trigger for.install. Same structure, but untested for the download.