diff --git a/.dockerignore b/.dockerignore index bf9b07fdc2..fcf47e5e16 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,10 @@ -Dockerfile -Dockerfile.dev +# ignored +**/* + +# authorized +!**/Caddyfile +!**/*.go +!**/go.* +!**/*.c +!**/*.h +!testdata/*.php diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml deleted file mode 100644 index d214271da1..0000000000 --- a/.github/workflows/docker-push.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Build and push Docker image (latest) -on: - push: - branches: - - main - tags: - - v* - workflow_dispatch: - inputs: {} -jobs: - docker-tests: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - dockerfile: [ "Dockerfile", "Dockerfile.alpine" ] - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - with: - install: true - - - name: Build test image - uses: docker/build-push-action@v4 - with: - context: ./ - file: ${{ matrix.dockerfile }} - push: false - pull: true - target: builder - tags: frankenphp:${{ github.sha }}-builder - builder: ${{ steps.buildx.outputs.name }} - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=docker,dest=/tmp/.builder.tar - - - name: Run tests - run: | - docker load -i /tmp/.builder.tar - docker run --rm frankenphp:${{ github.sha }}-builder "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'" - push-image: - runs-on: ubuntu-latest - strategy: - matrix: - dockerfile: [ "Dockerfile", "Dockerfile.alpine" ] - include: - - dockerfile: Dockerfile - flavor: "" - - dockerfile: Dockerfile.alpine - flavor: "-alpine" - steps: - - uses: actions/checkout@v3 - - - name: Docker Login - uses: docker/login-action@v2 - with: - #registry: ${{secrets.REGISTRY_LOGIN_SERVER}} - username: ${{secrets.REGISTRY_USERNAME}} - password: ${{secrets.REGISTRY_PASSWORD}} - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - # list of Docker images to use as base name for tags - images: ${{secrets.IMAGE_NAME}} - flavor: | - suffix=${{matrix.flavor}} - # generate Docker tags based on the following events/attributes - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest,enable={{is_default_branch}} - type=sha - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - with: - install: true - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Build and Push Image - uses: docker/build-push-action@v4 - with: - context: ./ - file: ${{ matrix.dockerfile }} - push: true - pull: true - target: final - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - builder: ${{ steps.buildx.outputs.name }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker-tests.yml b/.github/workflows/docker-tests.yml deleted file mode 100644 index 98ca61d6a3..0000000000 --- a/.github/workflows/docker-tests.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Tests in Docker -on: - pull_request: - branches: - - main -jobs: - docker-tests: - runs-on: ubuntu-latest - strategy: - matrix: - dockerfile: [ "Dockerfile", "Dockerfile.alpine" ] - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - with: - install: true - - - name: Build test image - uses: docker/build-push-action@v4 - with: - context: ./ - file: ${{ matrix.dockerfile }} - push: false - pull: true - target: builder - tags: frankenphp:${{ github.sha }}-builder - builder: ${{ steps.buildx.outputs.name }} - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=docker,dest=/tmp/.builder.tar - - - name: Run tests - run: | - docker load -i /tmp/.builder.tar - docker run --rm frankenphp:${{ github.sha }}-builder "sh -c 'go test -race -v ./... && cd caddy && go test -race -v ./...'" - push-image: - runs-on: ubuntu-latest - strategy: - matrix: - dockerfile: [ "Dockerfile", "Dockerfile.alpine" ] - include: - - dockerfile: Dockerfile - flavor: "" - - dockerfile: Dockerfile.alpine - flavor: "-alpine" - steps: - - uses: actions/checkout@v3 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - flavor: | - suffix=${{matrix.flavor}} - # list of Docker images to use as base name for tags - images: | - frankenphp - # generate Docker tags based on the following events/attributes - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest - type=sha - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - with: - install: true - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Build Image - uses: docker/build-push-action@v4 - with: - context: ./ - file: ${{ matrix.dockerfile }} - push: false - pull: true - target: final - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - builder: ${{ steps.buildx.outputs.name }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..08f714f4dd --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,189 @@ +name: Build Docker images +on: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - v* + workflow_dispatch: + inputs: {} +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + # Push only if we're committing in the main branch + push: ${{toJson(github.ref == 'refs/heads/main' && github.event_name != 'pull_request')}} + variants: ${{ steps.matrix.outputs.variants }} + platforms: ${{ steps.matrix.outputs.platforms }} + metadata: ${{ steps.matrix.outputs.metadata }} + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + + - name: Create variants matrix + id: matrix + run: | + METADATA=$(docker buildx bake --print) + echo "variants=$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< $METADATA)" >> "$GITHUB_OUTPUT" + echo "platforms=$(jq -c 'first(.target[]) | .platforms' <<< $METADATA)" >> "$GITHUB_OUTPUT" + echo "metadata=$(jq -c <<< $METADATA)" >> "$GITHUB_OUTPUT" + env: + LATEST: '1' # TODO: unset this variable when releasing the first tagged version + SHA: ${{github.sha}} + VERSION: ${{github.ref_name}} + + build: + runs-on: ubuntu-latest + needs: + - prepare + strategy: + fail-fast: false + matrix: + variant: ${{ fromJson(needs.prepare.outputs.variants) }} + platform: ${{ fromJson(needs.prepare.outputs.platforms) }} + include: + - race: "" + qemu: true + - platform: linux/amd64 + qemu: false + race: "-race" # The Go race detector is only supported on amd64 + - platform: linux/386 + qemu: false + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + if: matrix.qemu + uses: docker/setup-qemu-action@v2 + with: + platforms: ${{matrix.platform}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + platforms: ${{matrix.platform}} + version: latest + + - name: Login to DockerHub + if: fromJson(needs.prepare.outputs.push) + uses: docker/login-action@v2 + with: + username: ${{secrets.REGISTRY_USERNAME}} + password: ${{secrets.REGISTRY_PASSWORD}} + + - name: Build + id: build + uses: docker/bake-action@v3 + with: + pull: true + targets: | + builder-${{matrix.variant}} + runner-${{matrix.variant}} + # Remove tags to prevent "can't push tagged ref [...] by digest" error + set: | + *.tags= + *.platform=${{matrix.platform}} + *.cache-from=type=gha,scope=${{github.ref}}-${{matrix.platform}} + *.cache-from=type=gha,scope=refs/heads/main-${{matrix.platform}} + *.cache-to=type=gha,scope=${{github.ref}}-${{matrix.platform}} + *.output=type=image,name=dunglas/frankenphp,push-by-digest=true,name-canonical=true,push=${{ needs.prepare.outputs.push }} + env: + LATEST: '1' # TODO: unset this variable when releasing the first tagged version + SHA: ${{github.sha}} + VERSION: ${{github.ref_name}} + + # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600 + - name: Export metadata + if: fromJson(needs.prepare.outputs.push) + run: | + mkdir -p /tmp/metadata/builder /tmp/metadata/runner + + builderDigest=$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) + touch "/tmp/metadata/builder/${builderDigest#sha256:}" + + runnerDigest=$(jq -r '."runner-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) + touch "/tmp/metadata/runner/${runnerDigest#sha256:}" + env: + METADATA: ${{steps.build.outputs.metadata}} + + - name: Upload runner metadata + if: fromJson(needs.prepare.outputs.push) + uses: actions/upload-artifact@v3 + with: + name: metadata-builder-${{matrix.variant}} + path: /tmp/metadata/builder/* + if-no-files-found: error + retention-days: 1 + + - name: Upload runner metadata + if: fromJson(needs.prepare.outputs.push) + uses: actions/upload-artifact@v3 + with: + name: metadata-runner-${{matrix.variant}} + path: /tmp/metadata/runner/* + if-no-files-found: error + retention-days: 1 + + - name: Run tests + if: '!matrix.qemu' + continue-on-error: true + run: | + docker run --platform=${{matrix.platform}} --rm \ + dunglas/frankenphp@$(jq -r '."builder-${{matrix.variant}}"."containerimage.digest"' <<< $METADATA) \ + "sh -c 'go test ${{matrix.race}} -v ./... && cd caddy && go test ${{matrix.race}} -v ./...'" + env: + METADATA: ${{steps.build.outputs.metadata}} + + # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ + push: + runs-on: ubuntu-latest + needs: + - prepare + - build + if: fromJson(needs.prepare.outputs.push) + strategy: + fail-fast: false + matrix: + variant: ${{ fromJson(needs.prepare.outputs.variants) }} + target: ['builder', 'runner'] + steps: + - name: Download metadata + uses: actions/download-artifact@v3 + with: + name: metadata-${{matrix.target}}-${{matrix.variant}} + path: /tmp/metadata + + - run: ls -R /tmp/metadata + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{secrets.REGISTRY_USERNAME}} + password: ${{secrets.REGISTRY_PASSWORD}} + + - name: Create manifest list and push + working-directory: /tmp/metadata + run: | + docker buildx imagetools create $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | map("-t " + .) | join(" ")' <<< $METADATA) \ + $(printf 'dunglas/frankenphp@sha256:%s ' *) + env: + METADATA: ${{needs.prepare.outputs.metadata}} + + - name: Inspect image + run: | + docker buildx imagetools inspect $(jq -cr '.target."${{matrix.target}}-${{matrix.variant}}".tags | first' <<< $METADATA) + env: + METADATA: ${{needs.prepare.outputs.metadata}} + \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea0508577c..a753d03232 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,11 @@ jobs: php-versions: ['8.2', '8.3'] steps: - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: '1.20' + - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} @@ -19,10 +21,13 @@ jobs: coverage: none env: phpts: ts + - name: Set include flags run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV" + - name: Run library tests run: go test -race -v ./... + - name: Run Caddy module tests working-directory: caddy/ run: go test -race -v ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 430e1e2dae..12baa127e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Build the dev Docker image: - docker build -t frankenphp-dev -f Dockerfile.dev . + docker build -t frankenphp-dev -f dev.Dockerfile . docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp-dev The image contains the usual development tools (Go, GDB, Valgrind, Neovim...). diff --git a/Dockerfile b/Dockerfile index 59c9692361..a0ec4459f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,4 @@ -FROM php:8.2-zts-bookworm AS php-base - -FROM golang:1.20-bookworm AS golang-base - +# syntax=docker/dockerfile:1 FROM php-base AS builder COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go @@ -49,7 +46,7 @@ RUN cd caddy/frankenphp && \ ENTRYPOINT ["/bin/bash","-c"] -FROM php-base AS final +FROM php-base AS runner COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ diff --git a/Dockerfile.alpine b/alpine.Dockerfile similarity index 94% rename from Dockerfile.alpine rename to alpine.Dockerfile index 643e558b28..67e3309b77 100644 --- a/Dockerfile.alpine +++ b/alpine.Dockerfile @@ -1,7 +1,4 @@ -FROM php:8.2-zts-alpine3.18 AS php-base - -FROM golang:1.20-alpine3.18 AS golang-base - +# syntax=docker/dockerfile:1 FROM php-base AS builder COPY --from=golang-base /usr/local/go/bin/go /usr/local/bin/go @@ -48,7 +45,7 @@ RUN cd caddy/frankenphp && \ ENTRYPOINT ["/bin/sh","-c"] -FROM php-base AS final +FROM php-base AS runner COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ diff --git a/Dockerfile.dev b/dev.Dockerfile similarity index 98% rename from Dockerfile.dev rename to dev.Dockerfile index a65d74d418..881380cf20 100644 --- a/Dockerfile.dev +++ b/dev.Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 FROM golang:1.20 ENV CFLAGS="-ggdb3" diff --git a/docker-bake.hcl b/docker-bake.hcl index fb8a99ad80..b5930d3a9c 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,29 +1,90 @@ -variable "REPO_NAME" { +variable "IMAGE_NAME" { default = "dunglas/frankenphp" } -group "default" { - targets = ["bookworm", "alpine"] +variable "VERSION" { + default = "dev" } -target "common" { - platforms = ["linux/amd64", "linux/arm64"] +variable "SHA" {} + +variable "LATEST" { + default = false +} + +variable "CACHE" { + default = "" +} + +function "tag" { + params = [version, os, php-version, tgt] + result = [ + version != "" ? format("%s:%s%s-php%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version, os) : "", + os == "bookworm" && php-version == "8.2" && version != "" ? format("%s:%s%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "") : "", + php-version == "8.2" && version != "" ? format("%s:%s%s-%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", os) : "", + os == "bookworm" && version != "" ? format("%s:%s%s-php%s", IMAGE_NAME, version, tgt == "builder" ? "-builder" : "", php-version) : "" + ] } -# -# FrankenPHP -# +# cleanTag ensures that the tag is a valid Docker tag +# see https://github.com/distribution/distribution/blob/v2.8.2/reference/regexp.go#L37 +function "clean_tag" { + params = [tag] + result = substr(regex_replace(regex_replace(tag, "[^\\w.-]", "-"), "^([^\\w])", "r$0"), 0, 127) +} + +# semver adds semver-compliant tag if a semver version number is passed, or returns the revision itself +# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +function "semver" { + params = [rev] + result = __semver(_semver(regexall("^v?(?P0|[1-9]\\d*)\\.(?P0|[1-9]\\d*)\\.(?P0|[1-9]\\d*)(?:-(?P(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", rev))) +} + +function "_semver" { + params = [matches] + result = length(matches) == 0 ? {} : matches[0] +} -target "bookworm" { - inherits = ["common"] - context = "." - dockerfile = "Dockerfile" - tags = ["${REPO_NAME}:bookworm", "${REPO_NAME}:latest"] +function "__semver" { + params = [v] + result = v == {} ? [clean_tag(VERSION)] : v.prerelease == null ? ["latest", v.major, "${v.major}.${v.minor}", "${v.major}.${v.minor}.${v.patch}"] : ["${v.major}.${v.minor}.${v.patch}-${v.prerelease}"] } -target "alpine" { - inherits = ["common"] - context = "." - dockerfile = "Dockerfile.alpine" - tags = ["${REPO_NAME}:alpine"] +target "default" { + name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}" + matrix = { + os = ["bookworm", "alpine"] + php-version = ["8.2", "8.3.0alpha3"] + tgt = ["builder", "runner"] + } + contexts = { + php-base = "docker-image://php:${php-version}-zts-${os}" + golang-base = "docker-image://golang:1.20-${os}" + } + dockerfile = os == "alpine" ? "alpine.Dockerfile" : "Dockerfile" + context = "./" + target = tgt + platforms = [ + "linux/amd64", + "linux/386", + "linux/arm/v6", + "linux/arm/v7", + "linux/arm64", + ] + tags = distinct(flatten([ + LATEST ? tag("latest", os, php-version, tgt) : [], + tag(SHA == "" ? "" : "sha-${substr(SHA, 0, 7)}", os, php-version, tgt), + [for v in semver(VERSION) : tag(v, os, php-version, tgt)] + ])) + labels = { + "org.opencontainers.image.title" = "FrankenPHP" + "org.opencontainers.image.description" = "The modern PHP app server" + "org.opencontainers.image.url" = "https://frankenphp.dev" + "org.opencontainers.image.source" = "https://github.com/dunglas/frankenphp" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.vendor" = "Kévin Dunglas" + "org.opencontainers.image.created" = "${timestamp()}" + "org.opencontainers.image.version" = VERSION + "org.opencontainers.image.revision" = SHA + } } diff --git a/frankenphp.c b/frankenphp.c index 48b0c2fba9..8b0369d2e8 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -583,8 +583,9 @@ static void *manager_thread(void *arg) { free(arg); uintptr_t rh; - while ((rh = go_fetch_request())) + while ((rh = go_fetch_request())) { thpool_add_work(thpool, go_execute_script, (void *) rh); + } /* channel closed, shutdown gracefully */ thpool_wait(thpool); diff --git a/frankenphp.h b/frankenphp.h index 9c7d4d0085..1f8d2a17e2 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -32,7 +32,7 @@ int frankenphp_update_server_context( const char *request_method, char *query_string, - int64_t content_length, + zend_long content_length, char *path_translated, char *request_uri, const char *content_type,