mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
feat: embed PHP apps into the FrankenPHP binary
This commit is contained in:
@@ -10,3 +10,4 @@
|
||||
!testdata/*.php
|
||||
!testdata/*.txt
|
||||
!build-static.sh
|
||||
!embed/*
|
||||
|
||||
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -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)}}
|
||||
|
||||
13
.github/workflows/static.yaml
vendored
13
.github/workflows/static.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
91
embed.go
Normal 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
0
embed/.gitignore
vendored
Normal 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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user