feat: embed PHP apps into the FrankenPHP binary

This commit is contained in:
Kévin Dunglas
2023-11-29 23:20:46 +01:00
parent bb931c99ff
commit 6509cddd2a
13 changed files with 230 additions and 43 deletions

View File

@@ -10,3 +10,4 @@
!testdata/*.php
!testdata/*.txt
!build-static.sh
!embed/*

View File

@@ -84,7 +84,7 @@ jobs:
-
name: Build
id: build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
pull: true
load: ${{!fromJson(needs.prepare.outputs.push)}}

View File

@@ -35,21 +35,30 @@ jobs:
uses: docker/setup-buildx-action@v3
with:
version: latest
-
name: Login to DockerHub
if: ${{toJson(startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request'))}}
uses: docker/login-action@v3
with:
username: ${{secrets.REGISTRY_USERNAME}}
password: ${{secrets.REGISTRY_PASSWORD}}
-
name: Build
id: build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
pull: true
load: true
push: ${{toJson(startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request'))}}
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_type == 'tag' && github.ref_name || github.sha}}
LATEST: '1' # TODO: unset this variable when releasing the first stable version
SHA: ${{github.sha}}
VERSION: ${{github.ref_type == 'tag' && github.ref_name || github.sha}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: Copy binary

View File

@@ -80,6 +80,7 @@ WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
COPY --link embed embed
COPY --link internal internal
COPY --link testdata testdata

View File

@@ -77,6 +77,7 @@ WORKDIR /go/src/app
COPY --link *.* ./
COPY --link caddy caddy
COPY --link C-Thread-Pool C-Thread-Pool
COPY --link embed embed
COPY --link internal internal
COPY --link testdata testdata

View File

@@ -1,7 +1,6 @@
#!/bin/sh
set -o errexit
set -o xtrace
if ! type "git" > /dev/null; then
echo "The \"git\" command must be installed."
@@ -48,37 +47,47 @@ fi
bin="frankenphp-$os-$arch"
mkdir -p dist/
cd dist/
if [ -d "static-php-cli/" ]; then
cd static-php-cli/
git pull
else
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
cd static-php-cli/
if [ "$CLEAN" ]; then
rm -Rf dist/
go clean -cache
fi
composer install --no-dev -a
# Build libphp if ncessary
if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
cd dist/static-php-cli
else
mkdir -p dist/
cd dist/
if type "brew" > /dev/null; then
packages="composer"
if [ "$RELEASE" ]; then
packages="$packages gh"
if [ -d "static-php-cli/" ]; then
cd static-php-cli/
git pull
else
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
cd static-php-cli/
fi
brew install --formula --quiet "$packages"
if type "brew" > /dev/null; then
packages="composer"
if [ "$RELEASE" ]; then
packages="$packages gh"
fi
brew install --formula --quiet "$packages"
fi
composer install --no-dev -a
if [ "$os" = "linux" ]; then
extraOpts="--disable-opcache-jit"
fi
./bin/spc doctor
./bin/spc fetch --with-php="$PHP_VERSION" --for-extensions="$PHP_EXTENSIONS"
# shellcheck disable=SC2086
./bin/spc build --enable-zts --build-embed $extraOpts "$PHP_EXTENSIONS" --with-libs="$PHP_EXTENSION_LIBS"
fi
if [ "$os" = "linux" ]; then
extraOpts="--disable-opcache-jit"
fi
./bin/spc doctor
./bin/spc fetch --with-php="$PHP_VERSION" --for-extensions="$PHP_EXTENSIONS"
# shellcheck disable=SC2086
./bin/spc build --enable-zts --build-embed $extraOpts "$PHP_EXTENSIONS" --with-libs="$PHP_EXTENSION_LIBS"
CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $(./buildroot/bin/php-config --includes | sed s#-I/#-I"$PWD"/buildroot/#g)"
export CGO_CFLAGS
@@ -92,15 +101,28 @@ export CGO_LDFLAGS
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
export LIBPHP_VERSION
cd ../../caddy/frankenphp/
cd ../..
# Embed PHP app, if any
if [ -d "$EMBED" ]; then
mv embed embed.bak
cp -R "$EMBED" embed
fi
cd caddy/frankenphp/
go env
go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags -static-pie -w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $LIBPHP_VERSION Caddy'" -o "../../dist/$bin"
cd ../..
cd ../../dist/
"./$bin" version
if [ -d "$EMBED" ]; then
rm -Rf embed
mv embed.bak embed
fi
"dist/$bin" version
if [ "$RELEASE" ]; then
gh release upload "$FRANKENPHP_VERSION" "$bin" --repo dunglas/frankenphp --clobber
gh release upload "$FRANKENPHP_VERSION" "dist/$bin" --repo dunglas/frankenphp --clobber
fi
if [ "$CURRENT_REF" ]; then

View File

@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strconv"
"github.com/caddyserver/caddy/v2"
@@ -20,6 +21,8 @@ import (
"go.uber.org/zap"
)
const defaultDocumentRoot = "public"
func init() {
caddy.RegisterModule(FrankenPHPApp{})
caddy.RegisterModule(FrankenPHPModule{})
@@ -93,8 +96,6 @@ func (f *FrankenPHPApp) Start() error {
}
}
logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))
return nil
}
@@ -169,6 +170,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if wc.FileName == "" {
return errors.New(`The "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
}
f.Workers = append(f.Workers, wc)
@@ -193,7 +198,7 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
}
type FrankenPHPModule struct {
// Root sets the root folder to the site. Default: `root` directive.
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
Root string `json:"root,omitempty"`
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
SplitPath []string `json:"split_path,omitempty"`
@@ -217,8 +222,18 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Logger(f)
if f.Root == "" {
f.Root = "{http.vars.root}"
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
f.ResolveRootSymlink = false
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
}
if len(f.SplitPath) == 0 {
f.SplitPath = []string{".php"}
}
@@ -425,6 +440,17 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// unmarshaler can read it from the start
dispenser.Reset()
if frankenphp.EmbeddedAppPath != "" {
if phpsrv.Root == "" {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
fsrv.Root = phpsrv.Root
phpsrv.ResolveRootSymlink = false
} else if filepath.IsLocal(fsrv.Root) {
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
fsrv.Root = phpsrv.Root
}
}
// set up a route list that we'll append to
routes := caddyhttp.RouteList{}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"path/filepath"
"strconv"
"time"
@@ -15,6 +16,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/certmagic"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"
"github.com/spf13/cobra"
@@ -60,6 +62,14 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
debug := fs.Bool("debug")
compress := !fs.Bool("no-compress")
if frankenphp.EmbeddedAppPath != "" {
if root == "" {
root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
} else if filepath.IsLocal(root) {
root = filepath.Join(frankenphp.EmbeddedAppPath, root)
}
}
const indexFile = "index.php"
extensions := []string{"php"}
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}

View File

@@ -31,8 +31,6 @@ docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_s
# ...
```
See [the list of supported extensions](https://static-php.dev/en/guide/extensions.html).
To add libraries enabling additional functionality to the extensions you've enabled, you can pass use the `PHP_EXTENSION_LIBS` Docker ARG:
```console
@@ -43,6 +41,8 @@ docker buildx bake \
static-builder
```
See also: [customizing the build](#customizing-the-build)
### GitHub Token
If you hit the GitHub API rate limit, set a GitHub Personal Access Token in an environment variable named `GITHUB_TOKEN`:
@@ -64,4 +64,15 @@ cd frankenphp
Note: this script also works on Linux (and probably on other Unixes), and is used internally by the Docker based static builder we provide.
See [the list of supported extensions](https://static-php.dev/en/guide/extensions.html).
## Customizing The Build
The following environment variables can be passed to `docker build` and to the `build-static.sh`
script to customize the static build:
* `FRANKENPHP_VERSION`: the version of FrankenPHP to use
* `PHP_VERSION`: the version of PHP to use
* `PHP_EXTENSIONS`: the PHP extensions to build ([list of supported extensions](https://static-php.dev/en/guide/extensions.html))
* `PHP_EXTENSION_LIBS`: extra libraries to build that add extra features to the extensions
* `EMBED`: path of the PHP application to embed in the binary
* `CLEAN`: when set, libphp and all its dependencies are built from scratch (no cache)
* `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub

91
embed.go Normal file
View File

@@ -0,0 +1,91 @@
package frankenphp
import (
"crypto/md5"
"embed"
_ "embed"
"encoding/hex"
"io/fs"
"os"
"path/filepath"
"strings"
)
const embedDir = "embed"
// The path of the embedded PHP application (empty if none)
var EmbeddedAppPath string
//go:embed all:embed
var embeddedApp embed.FS
func init() {
entries, err := embeddedApp.ReadDir(embedDir)
if err != nil {
panic(err)
}
if len(entries) == 1 && entries[0].Name() == ".gitignore" {
//no embedded app
return
}
e, err := os.Executable()
if err != nil {
panic(err)
}
e, err = filepath.EvalSymlinks(e)
if err != nil {
panic(err)
}
// TODO: use XXH3 instead of MD5
h := md5.Sum([]byte(e))
appPath := filepath.Join(os.TempDir(), "frankenphp_"+hex.EncodeToString(h[:]))
if err := os.RemoveAll(appPath); err != nil {
panic(err)
}
if err := copyToDisk(appPath, embedDir, entries); err != nil {
os.RemoveAll(appPath)
panic(err)
}
EmbeddedAppPath = appPath
}
func copyToDisk(appPath string, currentDir string, entries []fs.DirEntry) error {
if err := os.Mkdir(appPath+strings.TrimPrefix(currentDir, embedDir), 0700); err != nil {
return err
}
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
entries, err := embeddedApp.ReadDir(currentDir + "/" + name)
if err != nil {
return err
}
if err := copyToDisk(appPath, currentDir+"/"+name, entries); err != nil {
return err
}
continue
}
data, err := embeddedApp.ReadFile(currentDir + "/" + name)
if err != nil {
return err
}
f := appPath + "/" + strings.TrimPrefix(currentDir, embedDir) + "/" + name
if err := os.WriteFile(f, data, 0500); err != nil {
return err
}
}
return nil
}

0
embed/.gitignore vendored Normal file
View File

View File

@@ -152,9 +152,13 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
}
if fc.documentRoot == "" {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
if EmbeddedAppPath != "" {
fc.documentRoot = EmbeddedAppPath
} else {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
}
}
}
@@ -315,7 +319,10 @@ func Init(options ...Option) error {
return err
}
logger.Debug("FrankenPHP started")
logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version))
if EmbeddedAppPath != "" {
logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath))
}
return nil
}
@@ -330,6 +337,11 @@ func Shutdown() {
// Always reset the WaitGroup to ensure we're in a clean state
workersReadyWG = sync.WaitGroup{}
// Remove the installed app
if EmbeddedAppPath != "" {
os.RemoveAll(EmbeddedAppPath)
}
logger.Debug("FrankenPHP shut down")
}

View File

@@ -5,6 +5,8 @@ ARG FRANKENPHP_VERSION=''
ARG PHP_VERSION=''
ARG PHP_EXTENSIONS=''
ARG PHP_EXTENSION_LIBS=''
ARG CLEAN=''
ARG EMBED=''
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN apk update; \
@@ -68,5 +70,6 @@ WORKDIR /go/src/app
COPY *.* ./
COPY caddy caddy
COPY C-Thread-Pool C-Thread-Pool
COPY embed embed
RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh