diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e86da5cd..37f9048e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,6 +30,9 @@ public function __construct(array $urlParams = []) { #[\Override] public function register(IRegistrationContext $context): void { include_once __DIR__ . '/../../vendor/autoload.php'; + // Make sure rubix's free functions and constants are defined regardless of + // Composer's "files" autoloader dedupe (see the file for details). + require_once __DIR__ . '/../RubixBootstrap.php'; $context->registerEventListener(SuspiciousLoginEvent::class, LoginNotificationListener::class); $context->registerEventListener(SuspiciousLoginEvent::class, LoginMailListener::class); diff --git a/lib/RubixBootstrap.php b/lib/RubixBootstrap.php new file mode 100644 index 00000000..a72644d0 --- /dev/null +++ b/lib/RubixBootstrap.php @@ -0,0 +1,44 @@ +:"), + * stored process-wide in $GLOBALS['__composer_autoload_files']. Several apps + * bundle rubix and register the very same identifier, so only the first app to + * boot actually loads its copy; every other app's files loop is skipped. The + * catch is that those copies are not interchangeable: + * + * - an app that php-scopes rubix (e.g. recognize) only defines prefixed + * symbols such as OCA\Recognize\Vendor\Rubix\ML\sigmoid, so when it wins the + * race our un-scoped Rubix\ML\sigmoid is never defined and inference crashes + * with "Tensor\Matrix::map(): Argument #1 ($callback) must be of type + * callable, string given" (issue #1113); + * - an app that ships rubix un-scoped (e.g. mail) defines the very same + * Rubix\ML\* symbols we do, from a different path. + * + * So we must make sure our symbols exist, but only load our copy when they are + * actually missing: blindly requiring the files would fatal with "Cannot + * redeclare function Rubix\ML\argmin()" whenever another un-scoped copy already + * loaded them. require_once is not enough here - it dedupes by realpath, not by + * symbol, so it does not protect against another app's copy. Guard on a + * representative symbol from each file instead. + */ +if (!function_exists('Rubix\ML\sigmoid')) { + require_once __DIR__ . '/../vendor/rubix/ml/src/functions.php'; +} +if (!defined('Rubix\ML\HALF_PI')) { + require_once __DIR__ . '/../vendor/rubix/ml/src/constants.php'; +} +if (!defined('Tensor\EPSILON')) { + require_once __DIR__ . '/../vendor/rubix/tensor/src/constants.php'; +} diff --git a/psalm.xml b/psalm.xml index f33e5d8c..76ffb877 100644 --- a/psalm.xml +++ b/psalm.xml @@ -21,6 +21,10 @@ + + + diff --git a/tests/Unit/AppInfo/ApplicationTest.php b/tests/Unit/AppInfo/ApplicationTest.php new file mode 100644 index 00000000..a10166f3 --- /dev/null +++ b/tests/Unit/AppInfo/ApplicationTest.php @@ -0,0 +1,52 @@ +&1', $output, $exitCode); + + $message = implode("\n", $output); + self::assertNotSame(2, $exitCode, "Collision could not be reproduced, the test is stale:\n$message"); + self::assertSame(0, $exitCode, "Sigmoid activation failed despite RubixBootstrap.php:\n$message"); + } + + /** + * Counterpart to the test above: when another app ships rubix un-scoped and + * has already defined the Rubix\ML\* symbols, RubixBootstrap.php must not + * reload its own copy - that would fatal with "Cannot redeclare function + * Rubix\ML\argmin()". Reproduced in a subprocess for a clean symbol table. + */ + public function testRubixBootstrapDoesNotRedeclareExistingSymbols(): void { + $script = __DIR__ . '/rubix-redeclare-repro.php'; + + $output = []; + $exitCode = -1; + exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($script) . ' 2>&1', $output, $exitCode); + + self::assertSame(0, $exitCode, "RubixBootstrap.php redeclared rubix symbols that already existed:\n" . implode("\n", $output)); + } +} diff --git a/tests/Unit/AppInfo/rubix-collision-repro.php b/tests/Unit/AppInfo/rubix-collision-repro.php new file mode 100644 index 00000000..048e5d0d --- /dev/null +++ b/tests/Unit/AppInfo/rubix-collision-repro.php @@ -0,0 +1,52 @@ +:") and are therefore identical in every install, +// including the scoped copy shipped by other apps. +$GLOBALS['__composer_autoload_files']['0315e8fd3e479309d097647b8ef2920b'] = true; // rubix/ml src/functions.php +$GLOBALS['__composer_autoload_files']['702239352e6628be5dc71b6fd029e72e'] = true; // rubix/ml src/constants.php +$GLOBALS['__composer_autoload_files']['8f758069bf9eb3411d096c10be343745'] = true; // rubix/tensor src/constants.php + +require $appRoot . '/vendor/autoload.php'; + +// Guard: with the flags pre-set, our files loop must have skipped functions.php. +// If the function is already defined here the identifiers drifted and this test +// no longer reproduces the bug, so fail loudly instead of passing silently. +if (function_exists('Rubix\ML\sigmoid')) { + fwrite(STDERR, "collision not reproduced: Rubix\\ML\\sigmoid already defined\n"); + exit(2); +} + +// The fix: explicitly load our own copies regardless of the dedupe flag. +require_once $appRoot . '/lib/RubixBootstrap.php'; + +// Exercise the exact path from the crash report. +$result = (new Rubix\ML\NeuralNet\ActivationFunctions\Sigmoid()) + ->activate(Tensor\Matrix::quick([[0.5, -1.0], [2.0, 0.0]])); + +exit($result instanceof Tensor\Matrix ? 0 : 3); diff --git a/tests/Unit/AppInfo/rubix-redeclare-repro.php b/tests/Unit/AppInfo/rubix-redeclare-repro.php new file mode 100644 index 00000000..f5b6def1 --- /dev/null +++ b/tests/Unit/AppInfo/rubix-redeclare-repro.php @@ -0,0 +1,42 @@ +