Skip to content

fix(docker): install the composer dependencies of cloned shipped apps - #1064

Merged
susnux merged 2 commits into
nextcloud-libraries:mainfrom
luflow:fix/1059/composer-install-for-shipped-apps
Aug 3, 2026
Merged

fix(docker): install the composer dependencies of cloned shipped apps#1064
susnux merged 2 commits into
nextcloud-libraries:mainfrom
luflow:fix/1059/composer-install-for-shipped-apps

Conversation

@luflow

@luflow luflow commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #1063, so the first commit belongs to that PR. Please merge #1063 first, without it configureNextcloud aborts 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 spot in lib/docker.ts as 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/notifications no longer bundles them, but still ships a composer/autoload.php doing require_once __DIR__ . '/../vendor/autoload.php', and vendor/ is git ignored. OC_App::registerAutoloading() requires that file, so every request and every occ call dies:

Error: Failed opening required '/var/www/html/apps/notifications/composer/../vendor/autoload.php'
  … in /var/www/html/apps/notifications/composer/autoload.php:9
#0 /var/www/html/lib/private/legacy/OC_App.php(117): require_once()
#1 /var/www/html/lib/private/AppFramework/Bootstrap/Coordinator.php(78): OC_App::registerAutoloading()

On stable33 the composer/ directory did not exist yet, which is why this only surfaces when a suite moves to stable34. 0.4.0 and 0.5.0 are affected alike, the clone logic is unchanged between them.

Fix

If a cloned app has a composer.json, run composer install --no-dev for it. The server image ships PHP but no composer, so composer.phar is downloaded into the container once. NEXTCLOUD_E2E_COMPOSER_VERSION pins the version for consumers who want that, the default is latest-stable.

Scripts are run on purpose. Apps like notifications only assemble the prefixed copies of their dependencies (lib/Vendor/, via bamarni/composer-bin-plugin plus coenjacobs/mozart in post-install-cmd) there, so skipping scripts would leave the app loadable but still broken for web push.

A failing install fails the setup, and the public API is unchanged.

An earlier revision of this PR probed the app's composer autoloader and, when the install failed, degraded to removing the cloned composer/ directory. Both are gone after @susnux's review, see the discussion below.

Alternatives considered

  • composer install on the host, result shipped in via container.putArchive (the workaround by @theCalcaholic in Missing bundled PHP dependencies (stable34) nextcloud/notifications#3206). Works, but adds a host dependency on PHP and composer. Given on ubuntu-latest, not given for contributors running the suite locally. The in-container variant behaves the same on Linux runners and on macOS with Docker Desktop.
  • Only removing the cloned composer/ directory (my own workaround in Support installing composer packages for Nextcloud apps #1059). Needs no toolchain at all, but lib/Vendor/ stays absent, so anything touching web push logs Class "OCA\Notifications\Vendor\Minishlink\WebPush\VAPID" not found.
  • Installing such apps from the app store instead of cloning. App store tarballs do ship vendor/, and it would remove this code path entirely. Rejected because it gives up matching the app to the server branch, which is the reason the clone exists.
  • Shipping composer in the CI server image. Arguably the right layer, and I would happily drop the download if that lands in continuous-integration-shallow-server. Until then this keeps consumers working without an image change.
  • Verifying the downloaded composer.phar against its published checksum. Left out: it is served from the same host over the same HTTPS connection, so it proves transfer integrity rather than authenticity, while the code deliberately executes third-party scripts from a fresh git clone moments later.

Testing

  • npm ci, npx tsc --noEmit, npm run lint, npm run build clean (lint: 0 errors, the 8 warnings are pre-existing in lib/commands/*)
  • npm run test:node 4/4 pass, with notifications added to the suite plus an assertion that its vendor/autoload.php exists and that the app is enabled

Running the install for every cloned app is cheap, measured in the server image after a git clone --depth=1:

app composer install --no-dev autoloader afterwards committed composer/
viewer 0s, exit 0 loads untouched
text 0s, exit 0 loads untouched
notifications 22s, exit 0 loads (was fatal before) untouched

End to end against a real stable34 server, using nextcloud/attendance (the consumer from #1059) with this build linked in and its test server bumped stable33 to stable34:

├─ stderr: Cloning into 'apps-writable/notifications'...
│  ├─ Downloading Composer latest-stable into the container…
│  ├─ Running 'composer install' for notifications…
│  └─ Done
├─ stdout: notifications 7.0.0-dev.1 enabled
├─ stdout: attendance 1.42.0 enabled
└─ Nextcloud is now ready to use 🎉
✅ Snapshot "init" created successfully!

Completeness checked in the container, not just "it boots":

$ docker exec … ls /var/www/html/apps-writable/notifications/lib/Vendor | head -3
Base64Url
Brick
GuzzleHttp

$ docker exec -u www-data … php occ app:list | grep -E 'notifications|attendance'
  - attendance: 1.42.0
  - notifications: 7.0.0-dev.1

$ docker exec … sh -c "tail -c 4000 /var/www/html/data/nextcloud.log" | grep -ci vapid
0

Not verified

  • Linux runners, everything above ran on macOS with Docker Desktop.
  • The full Playwright suite of the consuming app against stable34, only the server setup was exercised. Failures there would be NC33 to NC34 UI changes, unrelated to this.
  • Apps other than notifications actually hitting the missing autoloader.

@nickvergessen
nickvergessen requested a review from susnux July 28, 2026 07:25
Comment thread lib/docker.ts Outdated
*/
verbose: boolean
/**
* Working directory to run the command in. Defaults to the container's working directory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or similar - as otherwise you would need to know the container structure)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, thanks. It's also more accurate than what I had, since runExec(['cat', 'core/shipped.json']) and runOcc already rely on that default.

Comment thread lib/docker.ts Outdated
Comment on lines +375 to +389
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`)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply:
IF composer.json exists
THEN composer install --no-dev

?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Why? I checked both cases and they do not have runtime dependencies.
So yes would waste time but a small amount only because with --no-dev nothing is installed.

The bigger problem is the combination with the rm -rf composer fallback.

Why would that be needed? I never had problem cloning any shipped app inside a Nextcloud checkout.
git clone + composer i --no-dev always works for all shipped apps I tried to clone (including text, notifications, activity, viewer, files_downloadlimit etc).

we'd delete a committed, working vendor dir

Which apps checks in vendor? This sound not really a good thing to do 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, switched to your version.

Also my wording was wrong: nothing checks in vendor. viewer and text commit composer/, which is the vendor dir of a separate composer project at composer/composer.json ("vendor-dir": "."). Their root vendor/ is git ignored like everywhere else.

I checked your point in the container, git clone + composer install --no-dev on viewer and text takes 0s, exits 0, and their committed composer/ autoloader still loads afterwards. So it is now simply: composer.json exists, run install. The autoloader probe and the fallback are gone, a failing install fails the setup.

Comment thread lib/docker.ts Outdated
@luflow
luflow force-pushed the fix/1059/composer-install-for-shipped-apps branch from 97897f2 to e25ec03 Compare July 28, 2026 16:02
luflow added 2 commits August 2, 2026 22:34
`app:list` was queried before `apps.config.php` registers `apps-writable` as an
apps path, so a bind mounted app is missing from the list. It is then not found
in `shipped.json` either and ends up in the app store branch, where
`app:install --force` exits non-zero 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)

Run `composer install --no-dev` for a cloned app that has a `composer.json`.
Composer itself is not part of the server image, so it is downloaded into the
container once, `NEXTCLOUD_E2E_COMPOSER_VERSION` pins the version if needed.
Scripts are run on purpose, apps like notifications only assemble the prefixed
copies of their dependencies in `post-install-cmd`.

Fixes nextcloud-libraries#1059

Signed-off-by: Florian Ludwig <florian@krautnerds.de>
@luflow
luflow force-pushed the fix/1059/composer-install-for-shipped-apps branch from 61a308a to f11eee4 Compare August 2, 2026 20:41
@luflow

luflow commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@susnux @nickvergessen updated the description here as well, it still described the previous revision.

The implementation now is just: cloned app has a composer.json, run composer install --no-dev. The autoloader probe and the rm -rf composer fallback are gone, a failing install fails the setup. Composer is back to latest-stable with NEXTCLOUD_E2E_COMPOSER_VERSION as an override, and the CHANGELOG change is dropped.

Since the approach changed rather than just got polished, the earlier approval probably does not carry over. Would appreciate another look.

@susnux
susnux merged commit 459d131 into nextcloud-libraries:main Aug 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support installing composer packages for Nextcloud apps

3 participants