Browse Source

feat: create a static build of FrankenPHP (#198)

* ci: create a static build of FrankenPHP

* try to use alpine

* path mapping

* cache and fixes

* fix

* fix include path

* fix include path

* fix include path

* switch to Docker

* fix github token

* cleanup

* various improvements

* macOS instructions

* fix GHA

* docs

* feat: mac static builds

* minor fix

* fix wd

* Apple silicon build

* Revert "Apple silicon build"

This reverts commit 7a2997e0920c2e490b2b4317feaaac01a89486b4.

* add opcache

* update builder

* upgrade to PHP 8.2

* switch to upstream static-php-cli, add intl
Kévin Dunglas 1 year ago
parent
commit
1f3007337d

+ 2 - 2
.github/workflows/docker.yml

@@ -20,7 +20,7 @@ jobs:
       platforms: ${{ steps.matrix.outputs.platforms }}
       metadata: ${{ steps.matrix.outputs.metadata }}
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
@@ -57,7 +57,7 @@ jobs:
           - platform: linux/386
             qemu: false
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Set up QEMU
         if: matrix.qemu

+ 97 - 0
.github/workflows/static.yml

@@ -0,0 +1,97 @@
+name: Build binary releases
+on:
+  pull_request:
+    branches:
+      - main
+  push:
+    branches:
+      - main
+    tags:
+      - v*
+  workflow_dispatch:
+    inputs: {}
+jobs:
+  build-linux:
+    name: Build Linux x86_64 binary
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+        with:
+          version: latest
+
+      - name: Build
+        id: build
+        uses: docker/bake-action@v3
+        with:
+          pull: true
+          load: true
+          targets: static-builder
+          set: |
+            *.cache-from=type=gha,scope=${{github.ref}}-static-builder
+            *.cache-from=type=gha,scope=refs/heads/main-static-builder
+            *.cache-to=type=gha,scope=${{github.ref}}-static-builder
+        env:
+          VERSION: ${{github.ref_name}}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Copy binary
+        run: docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp ; docker rm static-builder
+
+      - name: Upload binary
+        uses: actions/upload-artifact@v3
+        with:
+          name: frankenphp-linux-x86_64-dev
+          path: frankenphp
+
+  build-mac:
+    name: Build macOS binaries
+    runs-on: macos-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/checkout@v4
+        with:
+          repository: crazywhalecc/static-php-cli
+          path: static-php-cli
+
+      - uses: actions/setup-go@v4
+        with:
+          go-version: '1.21'
+
+      - name: Install static-php-cli dependencies
+        working-directory: static-php-cli/
+        run: composer install --no-dev -a
+
+      - name: Install missing system dependencies
+        run: ./bin/spc doctor --auto-fix
+        working-directory: static-php-cli/
+
+      - name: Fetch libraries sources
+        working-directory: static-php-cli/
+        run: ./bin/spc fetch --with-php=8.2 -A
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Build static libphp
+        working-directory: static-php-cli/
+        run: ./bin/spc build --enable-zts --build-embed --debug "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
+
+      - name: Set CGO flags
+        working-directory: static-php-cli/
+        run: |
+           echo "CGO_CFLAGS=$(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)" >> "$GITHUB_ENV"
+           echo "CGO_LDFLAGS=-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)" >> "$GITHUB_ENV"
+
+      - name: Build FrankenPHP
+        working-directory: caddy/frankenphp/
+        run: go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s"
+
+      - name: Upload binary
+        uses: actions/upload-artifact@v3
+        with:
+          name: frankenphp-mac-x86_64-dev
+          path: caddy/frankenphp/frankenphp

+ 1 - 1
.github/workflows/tests.yml

@@ -7,7 +7,7 @@ jobs:
       matrix:
         php-versions: ['8.2', '8.3']
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-go@v4
         with:

+ 18 - 1
docker-bake.hcl

@@ -6,6 +6,10 @@ variable "VERSION" {
     default = "dev"
 }
 
+variable "GO_VERSION" {
+    default = "1.21"
+}
+
 variable "SHA" {}
 
 variable "LATEST" {
@@ -59,7 +63,7 @@ target "default" {
     }
     contexts = {
         php-base = "docker-image://php:${php-version}-zts-${os}"
-        golang-base = "docker-image://golang:1.21-${os}"
+        golang-base = "docker-image://golang:${GO_VERSION}-${os}"
     }
     dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile"
     context = "./"
@@ -91,3 +95,16 @@ target "default" {
         FRANKENPHP_VERSION = VERSION
     }
 }
+
+target "static-builder" {
+    contexts = {
+        golang-base = "docker-image://golang:${GO_VERSION}-alpine"
+    }
+    dockerfile = "static-builder.Dockerfile"
+    context = "./"
+    tags = ["${IMAGE_NAME}:static-builder"]
+    args = {
+        FRANKENPHP_VERSION = VERSION
+    }
+    secret = ["id=github-token,env=GITHUB_TOKEN"]
+}

+ 5 - 0
docs/compile.md

@@ -1,5 +1,10 @@
 # Compile From Sources
 
+This document explain how to create a FrankenPHP build that will load PHP as a dymanic library.
+This is the recommended method.
+
+Alternatively, [creating static builds](static.md) is also possible.
+
 ## Install PHP
 
 FrankenPHP is compatible with the PHP 8.2 and superior.

+ 64 - 0
docs/static.md

@@ -0,0 +1,64 @@
+# Create a Static Build
+
+Instead of using a local installation of the PHP library,
+it's possible to create a static build of FrankenPHP thanks to the great [static-php-cli project](https://github.com/crazywhalecc/static-php-cli) (despite its name, this project support all SAPIs, not only CLI).
+
+With this method, a single, portable, binary will contain the PHP interpreter, the Caddy web server and FrankenPHP!
+
+## Linux
+
+We provide a Docker image to build a Linux static binary:
+
+```console
+docker buildx bake --load static-builder
+docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/caddy/frankenphp/frankenphp frankenphp ; docker rm static-builder
+```
+
+The resulting static binary is named `frankenphp` and is available in the current directory.
+
+If you want to build the static binary without Docker, take a look to the `static-builder.Dockerfile` file.
+
+### Custom Extensions
+
+By default, most popular PHP extensions are compiled.
+
+To reduce the size of the binary and to reduce the attack surface, you can choose the list of extensions to build using the `PHP_EXTENSIONS` Docker ARG.
+
+For instance, run the following command to only build the `opcache` extension:
+
+```console
+docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
+# ...
+```
+
+See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).
+
+### GitHub Token
+
+If you hit the GitHub API rate limit, set a GitHub Personal Access Token in an environment variable named `GITHUB_TOKEN`:
+
+```console
+GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
+# ...
+```
+
+## macOS
+
+Run the following command to create a static binary for macOS:
+
+```console
+git clone --depth=1 https://github.com/crazywhalecc/static-php-cli.git
+cd static-php-cli
+composer install --no-dev -a
+./bin/spc doctor --auto-fix
+./bin/spc fetch --with-php=8.2 -A
+./bin/spc build --enable-zts --build-embed --debug "bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu"
+export CGO_CFLAGS="$(./buildroot/bin/php-config --includes | sed s#-I/#-I$PWD/buildroot/#g)"
+export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
+
+git clone --depth=1 https://github.com/dunglas/frankenphp.git
+cd frankenphp/caddy/frankenphp
+go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie"
+```
+
+See [the list of supported extensions](https://static-php-cli.zhamao.me/en/guide/extensions.html).

+ 8 - 3
frankenphp.go

@@ -12,13 +12,18 @@ package frankenphp
 // Use PHP includes corresponding to your PHP installation by running:
 //
 //   export CGO_CFLAGS=$(php-config --includes)
+//   export CGO_LDFLAGS=$(php-config --ldflags --libs)
+//
+// We also set these flags for hardening: https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59
 
-// #cgo pkg-config: libxml-2.0 sqlite3
-// #cgo CFLAGS: -Wall -Werror
+// #cgo darwin pkg-config: libxml-2.0 sqlite3
+// #cgo CFLAGS: -Wall -Werror -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
 // #cgo CFLAGS: -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
 // #cgo linux CFLAGS: -D_GNU_SOURCE
+// #cgo CPPFLAGS: -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
 // #cgo darwin LDFLAGS: -L/opt/homebrew/opt/libiconv/lib -liconv
-// #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lresolv -ldl -lm -lutil
+// #cgo linux LDFLAGS: -Wl,-O1
+// #cgo LDFLAGS: -pie -L/usr/local/lib -L/usr/lib -lphp -lresolv -ldl -lm -lutil
 // #include <stdlib.h>
 // #include <stdint.h>
 // #include <php_variables.h>

+ 79 - 0
static-builder.Dockerfile

@@ -0,0 +1,79 @@
+# syntax=docker/dockerfile:1
+FROM golang-base
+
+ARG FRANKENPHP_VERSION='dev'
+ARG PHP_VERSION='8.2'
+ARG PHP_EXTENSIONS='bcmath,calendar,ctype,curl,dba,dom,exif,filter,fileinfo,gd,iconv,intl,mbstring,mbregex,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sqlite3,tokenizer,xml,xmlreader,xmlwriter,zip,zlib,apcu'
+
+RUN apk update; \
+    apk add --no-cache \
+        autoconf \
+        automake \
+        bash \
+        binutils \
+        bison \
+        build-base \
+        cmake \
+        curl \
+        file \
+        flex \
+        g++ \
+        gcc \
+        git \
+        jq \
+        libgcc \
+        libstdc++ \
+        linux-headers \
+        m4 \
+        make \
+        php82 \
+        php82-common \
+        php82-dom \
+        php82-mbstring \
+        php82-openssl \
+        php82-pcntl \
+        php82-phar \
+        php82-posix \
+        php82-tokenizer \
+        php82-xml \
+        php82-xmlwriter \
+        pkgconfig \
+        wget \
+        xz ; \
+    ln -sf /usr/bin/php82 /usr/bin/php
+
+# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
+ENV COMPOSER_ALLOW_SUPERUSER=1
+ENV PATH="${PATH}:/root/.composer/vendor/bin"
+
+COPY --from=composer/composer:2-bin --link /composer /usr/bin/composer
+
+WORKDIR /static-php-cli
+
+RUN git clone --depth=1 https://github.com/crazywhalecc/static-php-cli . && \
+    composer install --no-cache --no-dev --classmap-authoritative
+
+RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./bin/spc download --with-php=$PHP_VERSION --all
+
+RUN ./bin/spc build --build-embed --enable-zts --debug "$PHP_EXTENSIONS"
+
+ENV PATH="/static-php-cli/buildroot/bin:/static-php-cli/buildroot/usr/bin:$PATH"
+
+WORKDIR /go/src/app
+
+COPY go.mod go.sum ./
+RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
+
+RUN mkdir caddy && cd caddy
+COPY caddy/go.mod caddy/go.sum ./caddy/
+
+RUN cd caddy && go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
+
+COPY *.* ./
+COPY caddy caddy
+COPY C-Thread-Pool C-Thread-Pool
+
+RUN cd caddy/frankenphp && \
+    CGO_CFLAGS="$(/static-php-cli/buildroot/bin/php-config --includes | sed s#-I/#-I/static-php-cli/buildroot/#g)" \
+    CGO_LDFLAGS="$(/static-php-cli/buildroot/bin/php-config --ldflags) $(/static-php-cli/buildroot/bin/php-config --libs | sed -e 's/-lgcc_s//g')" \
+    go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -s -w -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION Caddy'"