Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/api-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ jobs:
database-password: ${{ env.POSTGRES_PASSWORD }}

- name: Prime app build
run: make composer
run: make php-test-dependencies-appstore

- name: Configure server with app
uses: SMillerDev/nextcloud-actions/setup-nextcloud-app@b13a04eb6a4e48972d31fa07559619843fcc2102 # main
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Changed

### Fixed
- Fixed News not starting since 28.5.0 due to php class loading issues (#3780)

Check failure on line 13 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'php'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'php'?", "location": {"path": "CHANGELOG.md", "range": {"start": {"line": 13, "column": 47}}}, "severity": "ERROR"}

# Releases
## [28.5.0] - 2026-06-03
Expand Down
33 changes: 31 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ appstore_sign_dir=$(appstore_build_directory)/sign
cert_dir=$(HOME)/.nextcloud/certificates
npm:=$(shell which npm 2> /dev/null)
composer:=$(shell which composer 2> /dev/null)
phpunit:=$(shell which phpunit 2> /dev/null)
ifeq (,$(composer))
composer:=php "$(build_tools_directory)/composer.phar"
endif
ifeq (,$(phpunit))
phpunit:=./vendor/phpunit/phpunit/phpunit
endif

#Support xDebug 3.0+
export XDEBUG_MODE=coverage
Expand Down Expand Up @@ -99,12 +103,12 @@ clean:

# Reports PHP codestyle violations
.PHONY: phpcs
phpcs:
phpcs: php-dev-dependencies-if-needed
./vendor/bin/phpcs --standard=PSR2 --ignore=lib/Migration/Version*.php,lib/Vendor/* lib

# Reports PHP static violations
.PHONY: phpstan
phpstan:
phpstan: php-dev-dependencies-if-needed
./vendor/bin/phpstan analyse --level=1 lib

# Same as clean but also removes dependencies installed by composer and
Expand Down Expand Up @@ -207,13 +211,34 @@ php-test-dependencies:
$(composer) install --prefer-dist
$(composer) scope-dependencies

.PHONY: php-test-dependencies-appstore
php-test-dependencies-appstore:
COMPOSER_NO_PLUGINS=1 $(composer) install --prefer-dist --no-dev --no-scripts
COMPOSER_NO_PLUGINS=1 $(composer) scope-dependencies
php ./bin/tools/fix_scoped_autoload.php
find "vendor" -mindepth 1 -maxdepth 1 ! -name 'composer' ! -name 'autoload.php' -exec rm -rf {} +

.PHONY: php-test-dependencies-if-needed
php-test-dependencies-if-needed:
@if [ ! -x "./vendor/phpunit/phpunit/phpunit" ]; then \
echo "PHP test dependencies not found. Installing dev dependencies..."; \
$(MAKE) php-test-dependencies; \
fi

.PHONY: php-dev-dependencies-if-needed
php-dev-dependencies-if-needed:
@if [ ! -x "./vendor/bin/phpstan" ]; then \
echo "Dev dependencies not found. Installing dev dependencies..."; \
$(MAKE) php-test-dependencies; \
fi

.PHONY: php-test-dependencies-if-needed-appstore
php-test-dependencies-if-needed-appstore:
@if [ ! -f "vendor/autoload.php" ] || [ ! -d "lib/Vendor" ]; then \
echo "Appstore-style PHP test dependencies not found. Installing scoped runtime dependencies..."; \
$(MAKE) php-test-dependencies-appstore; \
fi

.PHONY: scope-if-needed
scope-if-needed:
@if [ ! -d "lib/Vendor" ]; then \
Expand All @@ -225,6 +250,10 @@ scope-if-needed:
unit-test: php-test-dependencies-if-needed scope-if-needed
./vendor/phpunit/phpunit/phpunit -c phpunit.xml --coverage-clover build/php-unit.clover

.PHONY: unit-test-appstore
unit-test-appstore: php-test-dependencies-if-needed-appstore scope-if-needed
$(phpunit) -c phpunit.xml --coverage-clover build/php-unit.clover

# Command for running JS and PHP tests. Works for package.json files in the js/
# and root directory. If phpunit is not installed systemwide, a copy is fetched
# from the internet
Expand Down
95 changes: 95 additions & 0 deletions bin/tools/fix_scoped_autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

$projectRoot = dirname(__DIR__, 2);
$vendorDir = $projectRoot . '/vendor';
$scopedDir = $projectRoot . '/lib/Vendor';
$autoloadFilesPath = $vendorDir . '/composer/autoload_files.php';
$autoloadStaticPath = $vendorDir . '/composer/autoload_static.php';

if (!is_file($autoloadFilesPath) || !is_file($autoloadStaticPath) || !is_dir($scopedDir)) {
exit(0);
}

$autoloadFilesContent = file_get_contents($autoloadFilesPath);
$autoloadStaticContent = file_get_contents($autoloadStaticPath);
if ($autoloadFilesContent === false || $autoloadStaticContent === false) {
fwrite(STDERR, 'Unable to read Composer autoload files' . PHP_EOL);
exit(1);
}

preg_match_all('/\\$vendorDir \. \'\/([^\']+)\'/', $autoloadFilesContent, $matches);
$fileEntries = $matches[1] ?? [];

$pathMap = [];
foreach ($fileEntries as $relativeVendorPath) {
$candidate = mapVendorPathToScopedPath($relativeVendorPath, $scopedDir);
if ($candidate === null) {
// Fallback: some packages have their files placed at the root of lib/Vendor
// (e.g. symfony/deprecation-contracts → function.php, ralouphie/getallheaders
// → getallheaders.php). Check for a root-level match only — never match files
// in subdirectories, to avoid cross-package collisions such as phpstan's
// bootstrap.php matching a polyfill bootstrap.php deeper in the tree.
$basename = basename($relativeVendorPath);
if (is_file($scopedDir . '/' . $basename)) {
$candidate = $basename;
}
}

if ($candidate === null) {
continue;
}

$pathMap[$relativeVendorPath] = $candidate;
}

if ($pathMap === []) {
exit(0);
}

foreach ($pathMap as $vendorRelative => $scopedRelative) {
$autoloadFilesContent = str_replace(
"\$vendorDir . '/" . $vendorRelative . "'",
"\$baseDir . '/lib/Vendor/" . $scopedRelative . "'",
$autoloadFilesContent
);
$autoloadStaticContent = str_replace(
"__DIR__ . '/..' . '/" . $vendorRelative . "'",
"__DIR__ . '/../..' . '/lib/Vendor/" . $scopedRelative . "'",
$autoloadStaticContent
);
}

if (file_put_contents($autoloadFilesPath, $autoloadFilesContent) === false
|| file_put_contents($autoloadStaticPath, $autoloadStaticContent) === false) {
fwrite(STDERR, 'Unable to update Composer autoload files' . PHP_EOL);
exit(1);
}

function mapVendorPathToScopedPath(string $relativeVendorPath, string $scopedDir): ?string
{
$parts = explode('/', $relativeVendorPath);
if (count($parts) < 3) {
return null;
}

$vendor = normalizePackageSegment($parts[0]);
$packageParts = array_map('normalizePackageSegment', explode('-', $parts[1]));
$tail = array_slice($parts, 2);

$candidate = $vendor . '/' . implode('/', $packageParts);
if ($tail !== []) {
$candidate .= '/' . implode('/', $tail);
}

return is_file($scopedDir . '/' . $candidate) ? $candidate : null;
}

function normalizePackageSegment(string $segment): string
{
$words = preg_split('/[^a-zA-Z0-9]+/', $segment) ?: [];
$words = array_filter($words, static fn (string $word): bool => $word !== '');
return implode('', array_map(static fn (string $word): string => ucfirst(strtolower($word)), $words));
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@
"rm -Rf lib/Vendor",
"@php lib-vendor-organizer.php build/ lib/Vendor/ \"OCA\\\\News\\\\Vendor\"",
"rm -Rf build",
"composer dump-autoload"
"composer dump-autoload",
"@php bin/tools/fix_scoped_autoload.php"
]
}
}
Loading