diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 00000000000..b87d08926ea --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,90 @@ +name: Build docker image + +on: + push: + branches: + - add_docker_image + release: + types: + - published + +permissions: + contents: read + +jobs: + pre_job: + permissions: + actions: write + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5.3.1 + with: + concurrent_skipping: always + cancel_others: true + do_not_skip: '["release"]' + # list files that may affect or are included into the built phar + paths: '["bin/**", "assets/**", "build/**", "dictionaries/**", "src/**", "stubs/**", "psalm", "psalm-language-server", "psalm-plugin", "psalm-refactor", "psalm-review", "psalter", "box.json.dist", "composer.json", "config.xsd", "keys.asc.gpg", "scoper.inc.php"]' + + build-docker: + permissions: + packages: write + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + tools: composer:v2 + coverage: none + env: + fail-fast: true + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # required for composer to automatically detect root package version + + - name: Get Composer Cache Directories + id: composer-cache + run: | + echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT + + - name: Generate composer.lock + run: | + composer update --no-install + + - name: Cache composer cache + uses: actions/cache@v4 + with: + path: | + ${{ steps.composer-cache.outputs.files_cache }} + ${{ steps.composer-cache.outputs.vcs_cache }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Run composer install + run: composer install -o + # DO NOT set this, we need composer to figure out the version itself + # env: + # COMPOSER_ROOT_VERSION: dev-master + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Upload docker image + env: + EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + php bin/ci/build-docker.php diff --git a/bin/ci/build-docker.php b/bin/ci/build-docker.php new file mode 100755 index 00000000000..a76c0bf703b --- /dev/null +++ b/bin/ci/build-docker.php @@ -0,0 +1,58 @@ +write("> $cmd\n"); + $cmd = Process::start($cmd); + async(pipe(...), $cmd->getStdout(), getStdout())->ignore(); + async(pipe(...), $cmd->getStderr(), getStderr())->ignore(); + if ($exit = $cmd->join()) { + exit($exit); + } +} + +$composer_branch = $is_tag ? $ref : "dev-$ref"; +$dev = $is_tag ? '' : '~dev'; + +$cur = 0; +while (true) { + $json = json_decode(file_get_contents("https://repo.packagist.org/p2/vimeo/psalm$dev.json?v=$cur"), true)["packages"]["vimeo/psalm"]; + foreach ($json as $v) { + if ($v['version'] === $composer_branch) { + if ($v['source']['reference'] === $commit) { + break 2; + } + break; + } + } + sleep(1); + $cur++; +} + +$ref = escapeshellarg($ref); +$composer_branch = escapeshellarg($composer_branch); + +passthru("docker buildx build --push --platform linux/amd64,linux/arm64/v8 --cache-from ghcr.io/vimeo/psalm:$ref --cache-to type=inline . -t ghcr.io/vimeo/psalm:$ref --build-arg PSALM_REV=$composer_branch -f bin/docker/Dockerfile"); + +if ($is_tag) { + passthru("docker tag ghcr.io/vimeo/psalm:$ref ghcr.io/vimeo/psalm:latest"); + passthru("docker push ghcr.io/vimeo/psalm:latest"); +} diff --git a/bin/docker/Dockerfile b/bin/docker/Dockerfile new file mode 100644 index 00000000000..489416c4628 --- /dev/null +++ b/bin/docker/Dockerfile @@ -0,0 +1,262 @@ +# Not alpine, due to possible performance issues of MUSL malloc. +# +# In theory this should not be relevant because PHP uses its own allocator, +# but some one-time initialization logic inside PHP bypasses it, +# which means system malloc *is* used more often especially in cases like these. +# +# Copied from autogenerated dockerfile in https://github.com/docker-library/php/tree/master/8.4/bookworm/cli. +# Need to compile PHP from scratch in order to apply deepbind.patch and use jemalloc. + +FROM debian:bookworm-slim + +LABEL "repository"="http://github.com/vimeo/psalm" +LABEL "homepage"="http://psalm.dev" +LABEL "maintainer"="Daniil Gentili " + +LABEL "org.opencontainers.image.source"="http://github.com/vimeo/psalm" +LABEL "org.opencontainers.image.description"="A static analysis tool for finding errors in PHP applications " +LABEL "org.opencontainers.image.licenses"=MIT + +# prevent Debian's PHP packages from being installed +# https://github.com/docker-library/php/pull/542 +RUN set -eux; \ + { \ + echo 'Package: php*'; \ + echo 'Pin: release *'; \ + echo 'Pin-Priority: -1'; \ + } > /etc/apt/preferences.d/no-debian-php + +# dependencies required for running "phpize" +# (see persistent deps below) +ENV PHPIZE_DEPS \ + autoconf \ + dpkg-dev \ + file \ + g++ \ + gcc \ + libc-dev \ + make \ + pkg-config \ + re2c + +# persistent / runtime deps +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + $PHPIZE_DEPS \ + ca-certificates \ + curl \ + xz-utils \ + ; \ + rm -rf /var/lib/apt/lists/* + +ENV PHP_INI_DIR /usr/local/etc/php +RUN set -eux; \ + mkdir -p "$PHP_INI_DIR/conf.d"; \ +# allow running as an arbitrary user (https://github.com/docker-library/php/issues/743) + [ ! -d /var/www/html ]; \ + mkdir -p /var/www/html; \ + chown www-data:www-data /var/www/html; \ + chmod 1777 /var/www/html + +# Apply stack smash protection to functions using local buffers and alloca() +# Make PHP's main executable position-independent (improves ASLR security mechanism, and has no performance impact on x86_64) +# Enable optimization (-O2) +# Enable linker optimization (this sorts the hash buckets to improve cache locality, and is non-default) +# https://github.com/docker-library/php/issues/272 +# -D_LARGEFILE_SOURCE and -D_FILE_OFFSET_BITS=64 (https://www.php.net/manual/en/intro.filesystem.php) +ENV PHP_CFLAGS="-fstack-protector-strong -fpic -fpie -O3 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64" +ENV PHP_CPPFLAGS="$PHP_CFLAGS" +ENV PHP_LDFLAGS="-Wl,-O1 -pie" + +ENV GPG_KEYS AFD8691FDAEDF03BDF6E460563F15A9B715376CA 9D7F99A0CB8F05C8A6958D6256A97AF7600A39A6 0616E93D95AF471243E26761770426E17EBBB3DD + +ENV PHP_VERSION 8.4.4 +ENV PHP_URL="https://www.php.net/distributions/php-8.4.4.tar.xz" PHP_ASC_URL="https://www.php.net/distributions/php-8.4.4.tar.xz.asc" +ENV PHP_SHA256="05a6c9a2cc894dd8be719ecab221b311886d5e0c02cb6fac648dd9b3459681ac" + +ADD bin/docker/deepbind.patch / + +RUN set -eux; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends gnupg; \ + rm -rf /var/lib/apt/lists/*; \ + \ + mkdir -p /usr/src; \ + cd /usr/src; \ + \ + curl -fsSL -o php.tar.xz "$PHP_URL"; \ + \ + if [ -n "$PHP_SHA256" ]; then \ + echo "$PHP_SHA256 *php.tar.xz" | sha256sum -c -; \ + fi; \ + \ + if [ -n "$PHP_ASC_URL" ]; then \ + curl -fsSL -o php.tar.xz.asc "$PHP_ASC_URL"; \ + export GNUPGHOME="$(mktemp -d)"; \ + for key in $GPG_KEYS; do \ + gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"; \ + done; \ + gpg --batch --verify php.tar.xz.asc php.tar.xz; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + fi; \ + \ + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false + +COPY bin/docker/docker-php-source /usr/local/bin/ + +RUN set -eux; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libargon2-dev \ + libcurl4-openssl-dev \ + libonig-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + zlib1g-dev \ + libcapstone-dev \ + ; \ + \ + export \ + CFLAGS="$PHP_CFLAGS" \ + CPPFLAGS="$PHP_CPPFLAGS" \ + LDFLAGS="$PHP_LDFLAGS" \ +# https://github.com/php/php-src/blob/d6299206dd828382753453befd1b915491b741c6/configure.ac#L1496-L1511 + PHP_BUILD_PROVIDER='https://github.com/docker-library/php' \ + PHP_UNAME='Linux - Docker' \ + ; \ + docker-php-source extract; \ + cd /usr/src/php; \ + patch -p1 < /deepbind.patch; \ + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ +# https://bugs.php.net/bug.php?id=74125 + if [ ! -d /usr/include/curl ]; then \ + ln -sT "/usr/include/$debMultiarch/curl" /usr/local/include/curl; \ + fi; \ + ./configure \ + --build="$gnuArch" \ + --with-config-file-path="$PHP_INI_DIR" \ + --with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \ + \ +# make sure invalid --configure-flags are fatal errors instead of just warnings + --enable-option-checking=fatal \ + \ +# https://github.com/docker-library/php/issues/439 + --with-mhash \ + \ +# https://github.com/docker-library/php/issues/822 + --with-pic \ + \ +# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195) + --enable-mbstring \ +# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself) + --enable-mysqlnd \ +# https://wiki.php.net/rfc/argon2_password_hash + --with-password-argon2 \ +# https://wiki.php.net/rfc/libsodium + --with-sodium=shared \ +# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109) + --with-pdo-sqlite=/usr \ + --with-sqlite3=/usr \ + \ + --with-curl \ + --with-iconv \ + --with-openssl \ + --with-readline \ + --with-zlib \ + \ +# in PHP 7.4+, the pecl/pear installers are officially deprecated (requiring an explicit "--with-pear") + --with-pear \ + \ + --disable-cgi \ + --disable-phpdbg \ + --with-capstone \ + --with-libdir="lib/$debMultiarch" \ + \ + ; \ + make -j "$(nproc)"; \ + find -type f -name '*.a' -delete; \ + make install; \ + make clean; \ + \ +# https://github.com/docker-library/php/issues/692 (copy default example "php.ini" files somewhere easily discoverable) + cp -v php.ini-* "$PHP_INI_DIR/"; \ + \ + cd /; \ + docker-php-source delete; rm deepbind.patch; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \ + find /usr/local -type f -executable -exec ldd '{}' ';' \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \ + | sort -u \ + | xargs -r dpkg-query --search \ + | cut -d: -f1 \ + | sort -u \ + | xargs -r apt-mark manual \ + ; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ +# update pecl channel definitions https://github.com/docker-library/php/issues/443 + pecl update-channels; \ + rm -rf /tmp/pear ~/.pearrc; \ + \ +# smoke test + php --version + +COPY bin/docker/docker-php-ext-* /usr/local/bin/ + +# sodium was built as a shared module (so that it can be replaced later if so desired), so let's enable it too (https://github.com/docker-library/php/issues/598) +RUN docker-php-ext-enable sodium + +# This line invalidates cache when master branch changes +ADD https://github.com/vimeo/psalm/commits/master.atom /dev/null + +ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ + +RUN sed 's/-O2/-O3/g' -i /usr/local/bin/install-php-extensions && \ + chmod +x /usr/local/bin/install-php-extensions && \ + install-php-extensions pcntl mbstring xml dom igbinary opcache && \ + rm /usr/local/bin/install-php-extensions + +RUN apt-get update && apt-get -y --no-install-recommends install git unzip libjemalloc2 && apt-get clean && rm -rf /var/lib/apt/lists/* + +ADD bin/docker/php.ini /usr/local/lib/php.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +ARG PSALM_REV=dev-master +RUN COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_HOME="/composer" \ + composer global require vimeo/psalm:${PSALM_REV} --prefer-dist --no-progress --dev && \ + rm /usr/bin/composer + +# Add entrypoint script + +COPY ./bin/docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +RUN ln -s "$(dpkg -L libjemalloc2 | grep libjemalloc.so | head -1)" /usr/lib/libjemalloc.so + +ENV PATH=/composer/vendor/bin:${PATH} + +ENV USE_ZEND_ALLOC=0 +ENV LD_PRELOAD=/usr/lib/libjemalloc.so + +RUN php -v + +WORKDIR "/app" +CMD ["/entrypoint.sh"] \ No newline at end of file diff --git a/bin/docker/deepbind.patch b/bin/docker/deepbind.patch new file mode 100644 index 00000000000..981e1cceaa0 --- /dev/null +++ b/bin/docker/deepbind.patch @@ -0,0 +1,51 @@ +From e87144e1a87789dbb1b53957de1668086100e808 Mon Sep 17 00:00:00 2001 +From: Daniil Gentili +Date: Wed, 13 Nov 2024 12:24:29 +0000 +Subject: [PATCH] Add --enable-rtld-deepbind configure flag + +--- + Zend/zend_portability.h | 2 +- + configure.ac | 17 +++++++++++++++++ + 2 files changed, 18 insertions(+), 1 deletion(-) + +diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h +index 9ab46f9b32cfe..1efc15fb6386c 100644 +--- a/Zend/zend_portability.h ++++ b/Zend/zend_portability.h +@@ -164,7 +164,7 @@ + + # if defined(RTLD_GROUP) && defined(RTLD_WORLD) && defined(RTLD_PARENT) + # define DL_LOAD(libname) dlopen(libname, PHP_RTLD_MODE | RTLD_GLOBAL | RTLD_GROUP | RTLD_WORLD | RTLD_PARENT) +-# elif defined(RTLD_DEEPBIND) && !defined(__SANITIZE_ADDRESS__) && !__has_feature(memory_sanitizer) ++# elif defined(RTLD_DEEPBIND) && !defined(__SANITIZE_ADDRESS__) && !__has_feature(memory_sanitizer) && defined(PHP_USE_RTLD_DEEPBIND) + # define DL_LOAD(libname) dlopen(libname, PHP_RTLD_MODE | RTLD_GLOBAL | RTLD_DEEPBIND) + # else + # define DL_LOAD(libname) dlopen(libname, PHP_RTLD_MODE | RTLD_GLOBAL) +diff --git a/configure.ac b/configure.ac +index 01d9ded69b920..137f7dae8c3a9 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -868,6 +868,23 @@ AS_VAR_IF([PHP_RTLD_NOW], [yes], + [Define to 1 if 'dlopen()' uses the 'RTLD_NOW' mode flag instead of + 'RTLD_LAZY'.])]) + ++if test "$PHP_SAPI" = "apache2handler"; then ++ PHP_RTLD_DEEPBIND_DEFAULT=yes ++else ++ PHP_RTLD_DEEPBIND_DEFAULT=no ++fi ++ ++PHP_ARG_ENABLE([rtld-deepbind], ++ [whether to dlopen extensions with RTLD_DEEPBIND], ++ [AS_HELP_STRING([--enable-rtld-deepbind], ++ [Use dlopen with RTLD_DEEPBIND])], ++ [$PHP_RTLD_DEEPBIND_DEFAULT], ++ [$PHP_RTLD_DEEPBIND_DEFAULT]) ++ ++if test "$PHP_RTLD_DEEPBIND" = "yes"; then ++ AC_DEFINE(PHP_USE_RTLD_DEEPBIND, 1, [ Use dlopen with RTLD_DEEPBIND ]) ++fi ++ + PHP_ARG_WITH([layout], + [layout of installed files], + [AS_HELP_STRING([--with-layout=TYPE], diff --git a/bin/docker/docker-php-ext-configure b/bin/docker/docker-php-ext-configure new file mode 100755 index 00000000000..34fc1337d56 --- /dev/null +++ b/bin/docker/docker-php-ext-configure @@ -0,0 +1,69 @@ +#!/bin/sh +set -e + +# prefer user supplied CFLAGS, but default to our PHP_CFLAGS +: ${CFLAGS:=$PHP_CFLAGS} +: ${CPPFLAGS:=$PHP_CPPFLAGS} +: ${LDFLAGS:=$PHP_LDFLAGS} +export CFLAGS CPPFLAGS LDFLAGS + +srcExists= +if [ -d /usr/src/php ]; then + srcExists=1 +fi +docker-php-source extract +if [ -z "$srcExists" ]; then + touch /usr/src/php/.docker-delete-me +fi + +cd /usr/src/php/ext + +usage() { + echo "usage: $0 ext-name [configure flags]" + echo " ie: $0 gd --with-jpeg-dir=/usr/local/something" + echo + echo 'Possible values for ext-name:' + find . \ + -mindepth 2 \ + -maxdepth 2 \ + -type f \ + -name 'config.m4' \ + | xargs -n1 dirname \ + | xargs -n1 basename \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +ext="$1" +if [ -z "$ext" ] || [ ! -d "$ext" ]; then + usage >&2 + exit 1 +fi +shift + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +if [ "$pm" = 'apk' ]; then + if \ + [ -n "$PHPIZE_DEPS" ] \ + && ! apk info --installed .phpize-deps > /dev/null \ + && ! apk info --installed .phpize-deps-configure > /dev/null \ + ; then + apk add --no-cache --virtual .phpize-deps-configure $PHPIZE_DEPS + fi +fi + +if command -v dpkg-architecture > /dev/null; then + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" + set -- --build="$gnuArch" "$@" +fi + +cd "$ext" +phpize +./configure --enable-option-checking=fatal "$@" diff --git a/bin/docker/docker-php-ext-enable b/bin/docker/docker-php-ext-enable new file mode 100755 index 00000000000..41d20bbe3fb --- /dev/null +++ b/bin/docker/docker-php-ext-enable @@ -0,0 +1,121 @@ +#!/bin/sh +set -e + +extDir="$(php -d 'display_errors=stderr' -r 'echo ini_get("extension_dir");')" +cd "$extDir" + +usage() { + echo "usage: $0 [options] module-name [module-name ...]" + echo " ie: $0 gd mysqli" + echo " $0 pdo pdo_mysql" + echo " $0 --ini-name 0-apc.ini apcu apc" + echo + echo 'Possible values for module-name:' + find -maxdepth 1 \ + -type f \ + -name '*.so' \ + -exec basename '{}' ';' \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +opts="$(getopt -o 'h?' --long 'help,ini-name:' -- "$@" || { usage >&2 && false; })" +eval set -- "$opts" + +iniName= +while true; do + flag="$1" + shift + case "$flag" in + --help|-h|'-?') usage && exit 0 ;; + --ini-name) iniName="$1" && shift ;; + --) break ;; + *) + { + echo "error: unknown flag: $flag" + usage + } >&2 + exit 1 + ;; + esac +done + +modules= +for module; do + if [ -z "$module" ]; then + continue + fi + if ! [ -f "$module" ] && ! [ -f "$module.so" ]; then + echo >&2 "error: '$module' does not exist" + echo >&2 + usage >&2 + exit 1 + fi + modules="$modules $module" +done + +if [ -z "$modules" ]; then + usage >&2 + exit 1 +fi + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +apkDel= +if [ "$pm" = 'apk' ]; then + if \ + [ -n "$PHPIZE_DEPS" ] \ + && ! apk info --installed .phpize-deps > /dev/null \ + && ! apk info --installed .phpize-deps-configure > /dev/null \ + ; then + apk add --no-cache --virtual '.docker-php-ext-enable-deps' binutils + apkDel='.docker-php-ext-enable-deps' + fi +fi + +for module in $modules; do + moduleFile="$module" + if [ -f "$module.so" ] && ! [ -f "$module" ]; then + moduleFile="$module.so" + fi + if readelf --wide --syms "$moduleFile" | grep -q ' zend_extension_entry$'; then + # https://wiki.php.net/internals/extensions#loading_zend_extensions + line="zend_extension=$module" + else + line="extension=$module" + fi + + ext="$(basename "$module")" + ext="${ext%.*}" + if php -d 'display_errors=stderr' -r 'exit(extension_loaded("'"$ext"'") ? 0 : 1);'; then + # this isn't perfect, but it's better than nothing + # (for example, 'opcache.so' presents inside PHP as 'Zend OPcache', not 'opcache') + echo >&2 + echo >&2 "warning: $ext ($module) is already loaded!" + echo >&2 + continue + fi + + case "$iniName" in + /*) + # allow an absolute path + ini="$iniName" + ;; + *) + ini="$PHP_INI_DIR/conf.d/${iniName:-"docker-php-ext-$ext.ini"}" + ;; + esac + if ! grep -qFx -e "$line" -e "$line.so" "$ini" 2>/dev/null; then + echo "$line" >> "$ini" + fi +done + +if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then + apk del --no-network $apkDel +fi diff --git a/bin/docker/docker-php-ext-install b/bin/docker/docker-php-ext-install new file mode 100755 index 00000000000..aa0b96c5a3e --- /dev/null +++ b/bin/docker/docker-php-ext-install @@ -0,0 +1,143 @@ +#!/bin/sh +set -e + +# prefer user supplied CFLAGS, but default to our PHP_CFLAGS +: ${CFLAGS:=$PHP_CFLAGS} +: ${CPPFLAGS:=$PHP_CPPFLAGS} +: ${LDFLAGS:=$PHP_LDFLAGS} +export CFLAGS CPPFLAGS LDFLAGS + +srcExists= +if [ -d /usr/src/php ]; then + srcExists=1 +fi +docker-php-source extract +if [ -z "$srcExists" ]; then + touch /usr/src/php/.docker-delete-me +fi + +cd /usr/src/php/ext + +usage() { + echo "usage: $0 [-jN] [--ini-name file.ini] ext-name [ext-name ...]" + echo " ie: $0 gd mysqli" + echo " $0 pdo pdo_mysql" + echo " $0 -j5 gd mbstring mysqli pdo pdo_mysql shmop" + echo + echo 'if custom ./configure arguments are necessary, see docker-php-ext-configure' + echo + echo 'Possible values for ext-name:' + find . \ + -mindepth 2 \ + -maxdepth 2 \ + -type f \ + -name 'config.m4' \ + | xargs -n1 dirname \ + | xargs -n1 basename \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +opts="$(getopt -o 'h?j:' --long 'help,ini-name:,jobs:' -- "$@" || { usage >&2 && false; })" +eval set -- "$opts" + +j=1 +iniName= +while true; do + flag="$1" + shift + case "$flag" in + --help|-h|'-?') usage && exit 0 ;; + --ini-name) iniName="$1" && shift ;; + --jobs|-j) j="$1" && shift ;; + --) break ;; + *) + { + echo "error: unknown flag: $flag" + usage + } >&2 + exit 1 + ;; + esac +done + +exts= +for ext; do + if [ -z "$ext" ]; then + continue + fi + if [ ! -d "$ext" ]; then + echo >&2 "error: $PWD/$ext does not exist" + echo >&2 + usage >&2 + exit 1 + fi + exts="$exts $ext" +done + +if [ -z "$exts" ]; then + usage >&2 + exit 1 +fi + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +apkDel= +if [ "$pm" = 'apk' ]; then + if [ -n "$PHPIZE_DEPS" ]; then + if apk info --installed .phpize-deps-configure > /dev/null; then + apkDel='.phpize-deps-configure' + elif ! apk info --installed .phpize-deps > /dev/null; then + apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS + apkDel='.phpize-deps' + fi + fi +fi + +popDir="$PWD" +for ext in $exts; do + cd "$ext" + + [ -e Makefile ] || docker-php-ext-configure "$ext" + + make -j"$j" + + if ! php -n -d 'display_errors=stderr' -r 'exit(ZEND_DEBUG_BUILD ? 0 : 1);' > /dev/null; then + # only "strip" modules if we aren't using a debug build of PHP + # (none of our builds are debug builds, but PHP might be recompiled with "--enable-debug" configure option) + # https://github.com/docker-library/php/issues/1268 + + find modules \ + -maxdepth 1 \ + -name '*.so' \ + -exec sh -euxc ' \ + strip --strip-all "$@" || : + ' -- '{}' + + fi + + make -j"$j" install + + find modules \ + -maxdepth 1 \ + -name '*.so' \ + -exec basename '{}' ';' \ + | xargs -r docker-php-ext-enable ${iniName:+--ini-name "$iniName"} + + make -j"$j" clean + + cd "$popDir" +done + +if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then + apk del --no-network $apkDel +fi + +if [ -e /usr/src/php/.docker-delete-me ]; then + docker-php-source delete +fi diff --git a/bin/docker/docker-php-source b/bin/docker/docker-php-source new file mode 100755 index 00000000000..9033d243de2 --- /dev/null +++ b/bin/docker/docker-php-source @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +dir=/usr/src/php + +usage() { + echo "usage: $0 COMMAND" + echo + echo "Manage php source tarball lifecycle." + echo + echo "Commands:" + echo " extract extract php source tarball into directory $dir if not already done." + echo " delete delete extracted php source located into $dir if not already done." + echo +} + +case "$1" in + extract) + mkdir -p "$dir" + if [ ! -f "$dir/.docker-extracted" ]; then + tar -Jxf /usr/src/php.tar.xz -C "$dir" --strip-components=1 + touch "$dir/.docker-extracted" + fi + ;; + + delete) + rm -rf "$dir" + ;; + + *) + usage + exit 1 + ;; +esac diff --git a/bin/docker/entrypoint.sh b/bin/docker/entrypoint.sh new file mode 100644 index 00000000000..c61cfaf5ea5 --- /dev/null +++ b/bin/docker/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh -l +set -e + +TAINT_ANALYSIS="" +if [ "$INPUT_SECURITY_ANALYSIS" = "true" ]; then + TAINT_ANALYSIS="--taint-analysis" +fi + +REPORT="" +if [ ! -z "$INPUT_REPORT_FILE" ]; then + REPORT="--report=$INPUT_REPORT_FILE" +fi + +SHOW_INFO="" +if [ "$INPUT_SHOW_INFO" = "true" ]; then + SHOW_INFO="--show-info=true" +fi + +PHP_VERSION="" +if [ -n "$INPUT_PHP_VERSION" ]; then + PHP_VERSION="--php-version=$INPUT_PHP_VERSION" +fi + +if [ -n "$INPUT_RELATIVE_DIR" ] +then + if [ -d "$INPUT_RELATIVE_DIR" ]; then + echo "changing directory into $INPUT_RELATIVE_DIR" + cd "$INPUT_RELATIVE_DIR" + else + echo "given relative_dir not existing" + exit 1 + fi +fi + +/composer/vendor/bin/psalm --force-jit --no-cache $TAINT_ANALYSIS $REPORT $SHOW_INFO $PHP_VERSION $* diff --git a/bin/docker/php.ini b/bin/docker/php.ini new file mode 100644 index 00000000000..21b5fce6ffe --- /dev/null +++ b/bin/docker/php.ini @@ -0,0 +1,36 @@ +memory_limit = -1 +zend.assertions = -1 +display_errors = On +display_startup_errors = On + +ffi.enable=true + +zend_extension=opcache + +[opcache] +opcache.memory_consumption=512M +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=function +opcache.validate_timestamps=0 +opcache.jit_buffer_size=128M +opcache.file_update_protection=0 +opcache.max_accelerated_files=1000000 +opcache.interned_strings_buffer=64 + +opcache.jit_prof_threshold=0.000000001 +opcache.jit_max_root_traces= 100000 +opcache.jit_max_side_traces= 100000 +opcache.jit_max_exit_counters=100000 +opcache.jit_hot_loop=1 +opcache.jit_hot_func=1 +opcache.jit_hot_return=1 +opcache.jit_hot_side_exit=1 +opcache.optimization_level=0x7FFEBFFF +opcache.log_verbosity_level=0 +opcache.save_comments=1 + +opcache.jit_blacklist_root_trace=255 +opcache.jit_blacklist_side_trace=255 + +opcache.protect_memory=1 diff --git a/docs/running_psalm/installation.md b/docs/running_psalm/installation.md index 862a394ac50..fdbcb160308 100644 --- a/docs/running_psalm/installation.md +++ b/docs/running_psalm/installation.md @@ -1,6 +1,6 @@ # Installation -The latest version of Psalm requires PHP >= 7.4 and [Composer](https://getcomposer.org/). +The latest version of Psalm requires PHP >= 8.2 and [Composer](https://getcomposer.org/). ```bash composer require --dev vimeo/psalm @@ -17,11 +17,19 @@ Psalm will scan your project and figure out an appropriate [error level](error_l Then run Psalm: ```bash -./vendor/bin/psalm +./vendor/bin/psalm --no-cache ``` Psalm will probably find a number of issues - find out how to deal with them in [Dealing with code issues](dealing_with_code_issues.md). +## Docker image + +It is recommended to run Psalm in the official docker image: it uses a custom build of PHP built from scratch, running Psalm **+30% faster** on average than normal PHP (**+50% faster** if comparing to PHP without opcache installed). + +```bash +docker run -v $PWD:/app --rm -it gchr.io/vimeo/psalm /app/vendor/bin/psalm --no-cache +``` + ## Installing plugins While Psalm can figure out the types used by various libraries based on diff --git a/docs/running_psalm/issues/MissingOverrideAttribute.md b/docs/running_psalm/issues/MissingOverrideAttribute.md index e59b35bd11e..622b9ef0a5d 100644 --- a/docs/running_psalm/issues/MissingOverrideAttribute.md +++ b/docs/running_psalm/issues/MissingOverrideAttribute.md @@ -21,3 +21,11 @@ class B extends A { ## Why this is bad Having an `Override` attribute on overridden methods makes intentions clear. Read the [PHP RFC](https://wiki.php.net/rfc/marking_overriden_methods) for more details. + +## How to fix + +Declare the `#[\Override]` attribute on all indicated methods, or run `vendor/bin/psalter --issues=MissingOverrideAttribute` to let Psalm do it for you. + +Note that the `#[\Override]` attribute is compatible with **all PHP versions**, even PHP 4. + +On PHP 8.0-8.2, require [symfony/polyfill-php83](https://packagist.org/packages/symfony/polyfill-php83) to polyfill the missing Override attribute. \ No newline at end of file diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 8087baa8de9..ebe746214dd 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -797,7 +797,7 @@ public function getTraitNode(string $fq_trait_name): PhpParser\Node\Stmt\Trait_ return $trait_node; } - throw new UnexpectedValueException("Could not locate trait statement for $fq_trait_name"); + throw new UnexpectedValueException('Could not locate trait statement'); } public function addClassAlias(string $fq_class_name, string $alias_name): void diff --git a/src/Psalm/Internal/PhpVisitor/TraitFinder.php b/src/Psalm/Internal/PhpVisitor/TraitFinder.php index c7092b7751c..90cfb68490c 100644 --- a/src/Psalm/Internal/PhpVisitor/TraitFinder.php +++ b/src/Psalm/Internal/PhpVisitor/TraitFinder.php @@ -45,10 +45,10 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): $fq_trait_name_parts = explode('\\', $this->fq_trait_name); /** @psalm-suppress PossiblyNullPropertyFetch */ - if ($node->name->name !== null && strcasecmp($node->name->name, end($fq_trait_name_parts)) === 0) { + if ($node->name->name === end($fq_trait_name_parts)) { $this->matching_trait_nodes[] = $node; } - } elseif (strcasecmp($resolved_name, $this->fq_trait_name) === 0) { + } elseif ($resolved_name === $this->fq_trait_name) { $this->matching_trait_nodes[] = $node; } }