lorisleiva / laravel-docker

🐳 Generic docker image for Laravel Applications
MIT License
934 stars 314 forks source link

Install php extension with `docker-php-extension-installer` + get rid of already installed extension #95

Closed lukasleitsch closed 2 years ago

lukasleitsch commented 2 years ago

This PR installs all required dependencies with the docker-php-extension-installer.

The docker-php-extension-installer shown that some extensions already installed. Therefore, I cleaned up the already installed extensions.

Closes #94

lorisleiva commented 2 years ago

Thanks! That looks good to me. Did you manage to test this image on a project? I just want to make this is not going to suddenly break everybody's pipeline. 😅

ksassnowski commented 2 years ago

For what it's worth, I applied this patch to our fork of this repo (which adds the ldap extension) and our pipelines are running fine :)

lorisleiva commented 2 years ago

That's good enough for me. Thank you so much for giving it a test! 🍺

jesperbjerke commented 2 years ago

@lorisleiva This breaks our pipelines because the docker image is now missing git. We use Gitlab CI/CD and we host some private composer and npm packages and use git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "git@gitlab.com:" to allow for easy authentication. Since git is no longer installed, this is not possible.

Rubenpannen commented 2 years ago

@jesperbjerke @lorisleiva Same issue here... image

ibluecode commented 2 years ago

Phpunit not working, and it was yesterday.

image

stevegrunwell commented 2 years ago

I've also run into a strange issue, as nunomaduro/larastan requires composer/composer; when I reach the deployment stage and re-run Composer minus development dependencies, it ends up cannibalizing itself:

+ which composer
./vendor/bin/composer
+ composer --version
Composer 2.1.14 2021-11-30 10:51:43
+ composer install --no-dev --optimize-autoloader --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
Installing dependencies from lock file
Verifying lock file contents can be installed on current platform.
Package operations: 0 installs, 0 updates, 63 removals
  - Removing theseer/tokenizer (1.2.1)
  - Removing symfony/yaml (v5.4.0)
  - Removing symfony/stopwatch (v5.4.0)
  - Removing symfony/options-resolver (v5.4.0)
  - Removing symfony/filesystem (v5.4.0)
  - Removing squizlabs/php_codesniffer (3.6.1)
  - Removing seld/phar-utils (1.1.2)
  - Removing seld/jsonlint (1.8.3)
  - Removing sebastian/version (3.0.2)
  - Removing sebastian/type (2.3.4)
  - Removing sebastian/resource-operations (3.0.3)
  - Removing sebastian/recursion-context (4.0.4)
  - Removing sebastian/object-reflector (2.0.4)
  - Removing sebastian/object-enumerator (4.0.4)
  - Removing sebastian/lines-of-code (1.0.3)
  - Removing sebastian/global-state (5.0.3)
  - Removing sebastian/exporter (4.0.4)
  - Removing sebastian/environment (5.1.3)
  - Removing sebastian/diff (4.0.4)
  - Removing sebastian/complexity (2.0.2)
  - Removing sebastian/comparator (4.0.6)
  - Removing sebastian/code-unit-reverse-lookup (2.0.3)
  - Removing sebastian/code-unit (1.0.8)
  - Removing sebastian/cli-parser (1.0.1)
  - Removing react/promise (v2.8.0)
  - Removing psr/cache (3.0.0)
  - Removing phpunit/phpunit (9.5.10)
  - Removing phpunit/php-timer (5.0.3)
  - Removing phpunit/php-text-template (2.0.4)
  - Removing phpunit/php-invoker (3.1.1)
  - Removing phpunit/php-file-iterator (3.0.6)
  - Removing phpunit/php-code-coverage (9.2.10)
  - Removing phpstan/phpstan (1.2.0)
  - Removing phpspec/prophecy (v1.15.0)
  - Removing phpdocumentor/type-resolver (1.5.1)
  - Removing phpdocumentor/reflection-docblock (5.3.0)
  - Removing phpdocumentor/reflection-common (2.2.0)
  - Removing php-cs-fixer/diff (v2.0.2)
  - Removing phar-io/version (3.1.0)
  - Removing phar-io/manifest (2.0.3)
  - Removing nunomaduro/larastan (1.0.2)
  - Removing nunomaduro/collision (v5.10.0)
  - Removing myclabs/deep-copy (1.10.2)
  - Removing mockery/mockery (1.4.4)
  - Removing laravel/sail (v1.12.10)
  - Removing laravel/homestead (v13.0.0)
  - Removing justinrainbow/json-schema (5.2.11)
  - Removing hamcrest/hamcrest-php (v2.0.1)
  - Removing friendsofphp/php-cs-fixer (v3.3.2)
  - Removing filp/whoops (2.14.4)
  - Removing fakerphp/faker (v1.17.0)
  - Removing facade/ignition-contracts (1.0.2)
  - Removing facade/ignition (2.17.2)
  - Removing facade/flare-client-php (1.9.1)
  - Removing doctrine/instantiator (1.4.0)
  - Removing doctrine/annotations (1.13.2)
  - Removing composer/xdebug-handler (2.0.3)
  - Removing composer/spdx-licenses (1.5.6)
  - Removing composer/semver (3.2.6)
  - Removing composer/pcre (1.0.0)
  - Removing composer/metadata-minifier (1.0.0)
  - Removing composer/composer (2.1.14)
  - Removing composer/ca-bundle (1.3.1)
Fatal error: Uncaught Error: Class "React\Promise\RejectedPromise" not found in /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php:30
Stack trace:
#0 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(134): React\Promise\FulfilledPromise->then(Object(Closure), Object(Closure))
#1 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#2 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#3 /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php(42): React\Promise\Promise::React\Promise\{closure}(NULL)
#4 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(135): React\Promise\FulfilledPromise->done(Object(Closure), Object(Closure), Object(Closure))
#5 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#6 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#7 /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php(42): React\Promise\Promise::React\Promise\{closure}(NULL)
#8 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(135): React\Promise\FulfilledPromise->done(Object(Closure), Object(Closure), Object(Closure))
#9 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#10 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#11 /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php(42): React\Promise\Promise::React\Promise\{closure}(NULL)
#12 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(135): React\Promise\FulfilledPromise->done(Object(Closure), Object(Closure), Object(Closure))
#13 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#14 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#15 /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php(42): React\Promise\Promise::React\Promise\{closure}(true)
#16 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(135): React\Promise\FulfilledPromise->done(Object(Closure), Object(Closure), Object(Closure))
#17 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#18 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#19 /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php(42): React\Promise\Promise::React\Promise\{closure}(Object(Symfony\Component\Process\Process))
#20 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(135): React\Promise\FulfilledPromise->done(Object(Closure), Object(Closure), Object(Closure))
#21 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(168): React\Promise\Promise::React\Promise\{closure}(Object(React\Promise\FulfilledPromise))
#22 /builds/my-org/my-app/vendor/react/promise/src/Promise.php(231): React\Promise\Promise->settle(Object(React\Promise\FulfilledPromise))
#23 [internal function]: React\Promise\Promise::React\Promise\{closure}(Object(Symfony\Component\Process\Process))
#24 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Util/ProcessExecutor.php(342): call_user_func(Object(Closure), Object(Symfony\Component\Process\Process))
#25 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Util/Loop.php(98): Composer\Util\ProcessExecutor->countActiveJobs()
#26 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer/InstallationManager.php(497): Composer\Util\Loop->wait(Array, NULL)
#27 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer/InstallationManager.php(470): Composer\Installer\InstallationManager->waitOnPromises(Array)
#28 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer/InstallationManager.php(390): Composer\Installer\InstallationManager->executeBatch(Object(Composer\Repository\InstalledFilesystemRepository), Array, Array, false, true, Array)
#29 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer/InstallationManager.php(282): Composer\Installer\InstallationManager->downloadAndExecuteBatch(Object(Composer\Repository\InstalledFilesystemRepository), Array, Array, false, true, Array)
#30 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer.php(742): Composer\Installer\InstallationManager->execute(Object(Composer\Repository\InstalledFilesystemRepository), Array, false, true)
#31 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Installer.php(273): Composer\Installer->doInstall(Object(Composer\Repository\InstalledFilesystemRepository))
#32 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Command/InstallCommand.php(140): Composer\Installer->run()
#33 /builds/my-org/my-app/vendor/symfony/console/Command/Command.php(298): Composer\Command\InstallCommand->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#34 /builds/my-org/my-app/vendor/symfony/console/Application.php(1005): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#35 /builds/my-org/my-app/vendor/symfony/console/Application.php(299): Symfony\Component\Console\Application->doRunCommand(Object(Composer\Command\InstallCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#36 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Console/Application.php(327): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#37 /builds/my-org/my-app/vendor/symfony/console/Application.php(171): Composer\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#38 /builds/my-org/my-app/vendor/composer/composer/src/Composer/Console/Application.php(128): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#39 /builds/my-org/my-app/vendor/composer/composer/bin/composer(74): Composer\Console\Application->run()
#40 {main}
  thrown in /builds/my-org/my-app/vendor/react/promise/src/FulfilledPromise.php on line 30
Cleaning up project directory and file based variables
ERROR: Job failed: exit code 1

To get around this, I've had to update my deployment script to explicitly reference /usr/local/bin/composer.

lorisleiva commented 2 years ago

Strange, I've added /usr/local/bin to the PATH of the image recently.

stevegrunwell commented 2 years ago

I think it's more a matter of the composer binary being found in ./vendor/bin, which comes first in the $PATH.

From the snippet above:

+ which composer
./vendor/bin/composer

I think the trick here is to remove ./vendor/bin/ from the beginning of $PATH, as Composer will automatically prepend it when running a Composer script:

Note: Before executing scripts, Composer's bin-dir is temporarily pushed on top of the PATH environment variable so that binaries of dependencies are directly accessible. In this example no matter if the phpunit binary is actually in vendor/bin/phpunit or bin/phpunit it will be found and executed.

Without it, running which composer would produce a result of /usr/local/bin/composer and subsequent calls to Composer that affect the vendor/ directory would not impact the running instance.

This also has the benefit of using the Composer configuration to prepend the vendor and bin paths, as Composer permits users to configure these values.

lorisleiva commented 2 years ago

Would that be the case in the terminal as well though? It seems to me that composer only pushes the local vendor bin when running a script defined in the composer.json.

stevegrunwell commented 2 years ago

You're correct: Composer only pushes its bin directory to the front of the path when running Composer scripts (or composer exec).

Imagine we have the following paths:

Under the current $PATH settings, these commands would map like this:

$ which composer
/var/www/vendor/bin/composer

$ which phpcs
/var/www/vendor/bin/phpcs

This feels reversed; I think it's reasonable to expect that running composer --version would reference the globally-installed instance.

If we drop ./vendor/bin from $PATH:

$ which composer
/usr/local/bin/composer

$ which phpcs
/root/.composer/vendor/bin/phpcs

This would require pipelines that have been built around this unconventional $PATH to be updated, however; if a site was previously just calling phpcs (expecting to reference ./vendor/bin/phpcs), it would start using the global installation instead.

Perhaps more-impactful would be PHPUnit: since it's not installed globally (nor should it be, IMO), pipelines that were simply running phpunit to implicitly refer to ./vendor/bin/phpunit may encounter "command not found: phpunit" errors.

Fortunately, there are a couple easy ways to fix that within a pipeline:

  1. Explicitly declare the path to the script you want to run (e.g. ./vendor/bin/phpunit)
  2. Prefix the command with composer exec to let Composer prepend the vendor-bin directory to $PATH (e.g. composer exec phpunit)
  3. Define a Composer script (e.g. composer test) and call that from the pipeline:

    // composer.json
    {
        // ...
        "scripts": {
            "test": "phpunit --some-opts"
        }
    }
lorisleiva commented 2 years ago

I think it's reasonable to expect that running composer --version would reference the globally-installed instance.

I am afraid I disagree with that premise. If you want to access composer globally, you can use composer --global. I've never worked on a project that expect global binaries to be prioritised over local binaries.