Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: check check-fast fmt fmt-check vet lint tidy tidy-check test test-node build clean \
build-linux-amd64 build-linux-arm64 bundle-php bundle-ext gc-bundles-dry-run \
local-ci
local-ci ci-cell ci

# Path to the native phpup binary used by bundle-php / bundle-ext. Overridable
# so CI / power users can point at a pre-built binary.
Expand Down Expand Up @@ -130,6 +130,58 @@ bundle-ext: $(PHPUP_BIN)
--registry $(or $(REGISTRY),oci-layout:./out/oci-layout) \
--repo .

# Run one compat-harness cell via `phpup test`: loads the published (or local
# OCI-layout) bundles for the requested OS/ARCH/PHP inside docker and exercises
# every matching row in test/compat/fixtures.yaml. Caller supplies OS, ARCH,
# PHP; REGISTRY defaults to the local OCI layout used by bundle-php/bundle-ext.
# Exercised end-to-end via `make ci` and from test/smoke/local-ci.sh.
#
# The cell container is always linux/<ARCH>, so phpup must be cross-compiled
# to match — a darwin/arm64 host binary or a linux/amd64 host binary running
# an aarch64 cell would trip `exec format error`. We resolve the right
# bin/phpup-linux-<arch> via a shell case, rebuild both the host-native and
# linux binaries on demand, then thread the linux binary via --self-binary.
ci-cell:
@case "$(ARCH)" in \
x86_64|amd64) phpup_linux_bin="bin/phpup-linux-amd64" ;; \
aarch64|arm64) phpup_linux_bin="bin/phpup-linux-arm64" ;; \
*) echo "ci-cell: unknown ARCH=$(ARCH), want x86_64 or aarch64" >&2; exit 2 ;; \
esac; \
$(MAKE) "$$phpup_linux_bin" && \
$(MAKE) $(PHPUP_BIN) && \
$(PHPUP_BIN) test \
--os $(OS) \
--arch $(ARCH) \
--php $(PHP) \
--self-binary "$$PWD/$$phpup_linux_bin" \
--registry $(or $(REGISTRY),oci-layout:./out/oci-layout) \
--fixtures test/compat/fixtures.yaml \
--repo .

# Cross-compiled phpup binaries used by ci-cell to mount the correct
# architecture-matching binary into the bare-ubuntu container. Declared as
# file targets so Make only rebuilds when the embedded lockfile changes.
bin/phpup-linux-amd64: cmd/phpup/bundles.lock
@mkdir -p bin
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/phpup-linux-amd64 ./cmd/phpup

bin/phpup-linux-arm64: cmd/phpup/bundles.lock
@mkdir -p bin
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bin/phpup-linux-arm64 ./cmd/phpup

# Iterate ci-cell across jammy+noble × x86_64+aarch64 × PHP 8.1-8.5. Short-
# circuits on first failure thanks to `set -e`. Requires docker + the relevant
# bundles in REGISTRY (published GHCR by default).
ci: $(PHPUP_BIN)
@set -e; for os in jammy noble; do \
for arch in x86_64 aarch64; do \
for php in 8.1 8.2 8.3 8.4 8.5; do \
echo "==> ci-cell OS=$$os ARCH=$$arch PHP=$$php"; \
$(MAKE) ci-cell OS=$$os ARCH=$$arch PHP=$$php; \
done; \
done; \
done

# Clean build artifacts
clean:
rm -rf bin/ dist/
Expand Down
24 changes: 24 additions & 0 deletions cmd/phpup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/buildrush/setup-php/internal/oci"
"github.com/buildrush/setup-php/internal/plan"
"github.com/buildrush/setup-php/internal/resolve"
"github.com/buildrush/setup-php/internal/testsuite"
"github.com/buildrush/setup-php/internal/version"
)

Expand Down Expand Up @@ -64,6 +65,29 @@ func main() {
return
}

// `phpup test …` is dispatched the same way as `phpup build …`: its
// own FlagSet, its own argv universe. Used by maintainers to run the
// compat-harness fixtures locally against a registry (oci-layout or
// remote). See internal/testsuite.Main for the flag surface.
if len(os.Args) > 1 && os.Args[1] == "test" {
if err := testsuite.Main(os.Args[2:]); err != nil {
log.Fatalf("%v", err)
}
return
}

// `phpup internal <subcmd> …` is the maintainer-only umbrella for
// commands that are driven by the outer tooling rather than by end
// users. Currently only "test-cell" (the inner, container-side part
// of `phpup test`). Kept separate so the user-facing help surface
// stays lean.
if len(os.Args) > 1 && os.Args[1] == "internal" {
if err := testsuite.InternalMain(os.Args[2:]); err != nil {
log.Fatalf("%v", err)
}
return
}

registryFlag := flag.String("registry", "",
"OCI artifact store (e.g., ghcr.io/buildrush or oci-layout:./out/oci-layout). Overrides INPUT_REGISTRY / PHPUP_REGISTRY; defaults to ghcr.io/buildrush.")
flag.Parse()
Expand Down
23 changes: 13 additions & 10 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ func BuildPHP(ctx context.Context, args []string) error {
}

// 4. Invoke builder in docker.
image, err := ubuntuImage(opts.OS)
image, err := UbuntuImage(opts.OS)
if err != nil {
return fmt.Errorf("phpup build php: %w", err)
}
platform, err := dockerPlatform(opts.Arch)
platform, err := DockerPlatform(opts.Arch)
if err != nil {
return fmt.Errorf("phpup build php: %w", err)
}
Expand Down Expand Up @@ -264,11 +264,11 @@ func BuildExt(ctx context.Context, args []string) error {

// 6. Run build container on the sidecar's network so
// fetch-core.sh can reach the sidecar by in-network hostname.
image, err := ubuntuImage(opts.OS)
image, err := UbuntuImage(opts.OS)
if err != nil {
return fmt.Errorf("phpup build ext: %w", err)
}
platform, err := dockerPlatform(opts.Arch)
platform, err := DockerPlatform(opts.Arch)
if err != nil {
return fmt.Errorf("phpup build ext: %w", err)
}
Expand Down Expand Up @@ -573,12 +573,13 @@ func prepareOutDir(path string) error {
return nil
}

// ubuntuImage maps a short OS flavour name onto the concrete docker image
// UbuntuImage maps a short OS flavour name onto the concrete docker image
// tag that builders/linux/build-php.sh expects. Accepts both the short
// ("jammy"/"noble") and long ("ubuntu-22.04"/"ubuntu-24.04") spellings
// because the planner emits the long form and humans tend to type the
// short form; either is unambiguous.
func ubuntuImage(osFlag string) (string, error) {
// short form; either is unambiguous. Exported so the testsuite package
// can reuse the same OS-to-image mapping without duplicating it.
func UbuntuImage(osFlag string) (string, error) {
switch strings.ToLower(osFlag) {
case "jammy", "ubuntu-22.04":
return "ubuntu:22.04", nil
Expand All @@ -604,9 +605,11 @@ func normalizeArch(arch string) (string, error) {
}
}

// dockerPlatform maps the canonical arch name (as produced by
// normalizeArch) onto the docker --platform value.
func dockerPlatform(arch string) (string, error) {
// DockerPlatform maps the canonical arch name (as produced by
// normalizeArch) onto the docker --platform value. Exported so the
// testsuite package can reuse the same arch-to-platform mapping without
// duplicating it.
func DockerPlatform(arch string) (string, error) {
switch arch {
case "x86_64":
return "linux/amd64", nil
Expand Down
120 changes: 120 additions & 0 deletions internal/testsuite/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package testsuite parses and filters test/compat/fixtures.yaml for use by
// the phpup test subcommand (local+CI test runner).
//
// A Fixture mirrors one entry in that YAML. Filter selects the subset of
// fixtures that apply to a given (os, arch, php) cell, applying tolerant
// aliasing on the runner OS (jammy/noble <-> ubuntu-22.04/24.04) and arch
// (amd64/x86_64 and arm64/aarch64 treated as equivalent).
package testsuite

import (
"fmt"
"os"
"path/filepath"

"gopkg.in/yaml.v3"
)

// Fixture mirrors a single entry in test/compat/fixtures.yaml. Field names
// match shivammathur/setup-php@v2 inputs (php-version, extensions, ini-values,
// coverage) so the same YAML feeds both the legacy compat harness and the
// new phpup test subcommand.
type Fixture struct {
Name string `yaml:"name"`
PHPVersion string `yaml:"php-version"`
// Extensions is a comma-separated list. Tokens may be "none" (reset),
// or prefixed with ":" to exclude (e.g. ":opcache"). Empty means no
// extensions beyond the core bundle.
Extensions string `yaml:"extensions"`
// INIValues is a comma-separated list of key=value pairs.
INIValues string `yaml:"ini-values"`
// Coverage is "none", "pcov", or "xdebug".
Coverage string `yaml:"coverage"`
// INIFile is optional: "production" or "development".
INIFile string `yaml:"ini-file,omitempty"`
// Arch is optional; "aarch64" or "x86_64". Empty means x86_64.
Arch string `yaml:"arch,omitempty"`
// RunnerOS is optional; e.g. "ubuntu-22.04". Empty means any OS.
RunnerOS string `yaml:"runner_os,omitempty"`
}

// FixtureSet is the top-level document shape of test/compat/fixtures.yaml.
type FixtureSet struct {
Fixtures []Fixture `yaml:"fixtures"`
}

// Load reads the YAML file at path and returns the parsed FixtureSet.
func Load(path string) (*FixtureSet, error) {
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, fmt.Errorf("testsuite.Load: read %s: %w", path, err)
}
var s FixtureSet
if err := yaml.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("testsuite.Load: parse %s: %w", path, err)
}
return &s, nil
}

// Filter returns fixtures matching the given (os, arch, php) tuple.
//
// Matching rules:
// - php: exact match on Fixture.PHPVersion.
// - arch: Fixture.Arch (default "x86_64" if empty) must equal arch input.
// Both canonical ("x86_64"/"aarch64") and docker aliases
// ("amd64"/"arm64") are accepted on the input side and normalized
// before comparison.
// - os: if Fixture.RunnerOS == "", the fixture is eligible for any OS.
// Otherwise Fixture.RunnerOS must equal the os input, with tolerant
// aliasing: "jammy" <-> "ubuntu-22.04", "noble" <-> "ubuntu-24.04".
//
// The returned slice is never nil; an empty match yields an empty slice.
func (s *FixtureSet) Filter(osName, arch, php string) []Fixture {
wantArch := normalizeArch(arch)
wantOS := normalizeOS(osName)
out := make([]Fixture, 0)
for i := range s.Fixtures {
f := &s.Fixtures[i]
if f.PHPVersion != php {
continue
}
fArch := f.Arch
if fArch == "" {
fArch = "x86_64"
}
if normalizeArch(fArch) != wantArch {
continue
}
if f.RunnerOS != "" && normalizeOS(f.RunnerOS) != wantOS {
continue
}
out = append(out, *f)
}
return out
}

// normalizeArch maps the docker aliases (amd64, arm64) to the canonical
// forms (x86_64, aarch64) used in fixtures.yaml. Unknown inputs are
// returned unchanged so Filter silently drops them instead of panicking.
func normalizeArch(a string) string {
switch a {
case "amd64", "x86_64":
return "x86_64"
case "arm64", "aarch64":
return "aarch64"
}
return a
}

// normalizeOS maps the short codenames (jammy, noble) to the long
// ubuntu-XX.YY form used in fixtures.yaml so either spelling works on
// input. Unknown inputs are returned unchanged.
func normalizeOS(o string) string {
switch o {
case "jammy":
return "ubuntu-22.04"
case "noble":
return "ubuntu-24.04"
}
return o
}
Loading
Loading