diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0dfbaea..4755df1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -5,7 +5,8 @@ on: branches: [main, development] jobs: - test-php-matrix: + test-php-matrix-linux: + name: Test PHP ${{ matrix.php_version }} on Linux timeout-minutes: 60 runs-on: ubuntu-latest strategy: @@ -26,7 +27,6 @@ jobs: env: PHPV_ROOT: ${{ github.workspace }}/phpv-root-${{ matrix.php_version }} - PHPV_EXTENSIONS: "opcache,mbstring,curl,pdo_mysql,openssl,intl,bcmath,zip,xml,gd,pdo,zlib,json,phar,filter,dom,fileinfo,session,tokenizer,xmlwriter,simplexml,xmlreader" steps: - uses: actions/checkout@v4 @@ -37,17 +37,20 @@ jobs: go-version: "1.25" cache: true - - name: Install system dependencies - uses: awalsh128/cache-apt-pkgs-action@v1 - with: - packages: >- - build-essential autoconf automake libtool m4 re2c bison - pkg-config libssl-dev libxml2-dev libsqlite3-dev - libgd-dev libonig-dev libzip-dev libicu-dev - libcurl4-openssl-dev libpng-dev libjpeg-dev - libwebp-dev libxpm-dev libfreetype6-dev + - name: Install system dependencies (apt) + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential autoconf automake libtool m4 re2c bison \ + pkg-config libssl-dev libxml2-dev libsqlite3-dev \ + libgd-dev libonig-dev libzip-dev libicu-dev \ + libcurl4-openssl-dev libpng-dev libjpeg-dev \ + libwebp-dev libxpm-dev libfreetype6-dev \ libmariadb-dev gettext - version: 1.0 + + - name: Set up PKG_CONFIG_PATH + run: | + echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:/usr/lib/pkgconfig" >> $GITHUB_ENV - name: Compute cache key id: cache-key @@ -69,14 +72,14 @@ jobs: go.sum \ .github/workflows/test.yaml \ | sha256sum | cut -d' ' -f1) - echo "key=phpv-${{ matrix.php_version }}-$HASH" >> $GITHUB_OUTPUT - echo "restore-key=phpv-${{ matrix.php_version }}-" >> $GITHUB_OUTPUT + echo "key=phpv-caches-${{ matrix.php_version }}-$HASH" >> $GITHUB_OUTPUT + echo "restore-key=phpv-caches-${{ matrix.php_version }}-" >> $GITHUB_OUTPUT - - name: Cache phpv build artifacts + - name: Cache downloaded archives id: cache-phpv uses: actions/cache@v4 with: - path: ${{ env.PHPV_ROOT }} + path: ${{ env.PHPV_ROOT }}/caches key: ${{ steps.cache-key.outputs.key }} restore-keys: | ${{ steps.cache-key.outputs.restore-key }} @@ -87,28 +90,142 @@ jobs: go build -o $PHPV_ROOT/bin/phpv ./app/phpv.go echo "$PHPV_ROOT/bin" >> $GITHUB_PATH - - name: Get installed PHP version - id: php-state + - name: Install PHP ${{ matrix.php_version }} run: | - PATTERN="$PHPV_ROOT/packages/php" - INSTALLED=$(ls -d $PATTERN/*/bin/php 2>/dev/null || echo "") - if [ -n "$INSTALLED" ]; then - echo "cached=true" >> $GITHUB_OUTPUT - else - echo "cached=false" >> $GITHUB_OUTPUT - fi + eval "$($PHPV_ROOT/bin/phpv init bash)" + phpv install ${{ matrix.php_version }} - - name: Install PHP ${{ matrix.php_version }} - if: steps.php-state.outputs.cached == 'false' + - name: Verify PHP ${{ matrix.php_version }} run: | eval "$($PHPV_ROOT/bin/phpv init bash)" - phpv install ${{ matrix.php_version }} --ext $PHPV_EXTENSIONS -v + phpv use ${{ matrix.php_version }} + + PHP_PATH=$(which php) + echo "PHP binary: $PHP_PATH" + + if [[ "$PHP_PATH" != "$PHPV_ROOT"* ]]; then + echo "Error: PHP is not running from the isolated root!" + exit 1 + fi + + php -v + echo "" + echo "=== Installed extensions ===" + php -m | sort + echo "" + + MISSING="" + for ext in dom gd intl mbstring pdo_mysql zip curl json phar opcache openssl simplexml xml xmlreader xmlwriter tokenizer session fileinfo filter bcmath pdo zlib; do + if php -m | grep -qi -w "$ext"; then + echo "✓ $ext" + elif [ "$ext" = "opcache" ] && \ + php -r ' + if (extension_loaded("Zend OPcache")) exit(0); + $f = ini_get("extension_dir") . "/opcache.so"; + exit(file_exists($f) ? 0 : 1); + ' 2>/dev/null; then + echo "✓ $ext (shared)" + else + echo "✗ $ext MISSING" + MISSING="$MISSING $ext" + fi + done + + if [ -n "$MISSING" ]; then + echo "" + echo "FAILED: Missing extensions:$MISSING" + exit 1 + fi + + echo "" + echo "All critical extensions present ✓" + + test-php-matrix-macos: + name: Test PHP ${{ matrix.php_version }} on macOS + timeout-minutes: 120 + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + php_version: + - "7.4" + - "8.5" + continue-on-error: true - - name: Rebuild PHP ${{ matrix.php_version }} - if: steps.php-state.outputs.cached == 'true' + env: + PHPV_ROOT: ${{ github.workspace }}/phpv-root-${{ matrix.php_version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + + - name: Install Homebrew dependencies + run: | + brew install \ + autoconf automake libtool m4 re2c bison \ + pkg-config openssl@3 libxml2 sqlite3 \ + oniguruma icu4c libpng jpeg-turbo \ + freetype libzip mariadb-connector-c \ + gettext webp libxpm libiconv + + - name: Set up environment variables + run: | + echo "PKG_CONFIG_PATH=/opt/homebrew/opt/openssl@3/lib/pkgconfig:/opt/homebrew/opt/libxml2/lib/pkgconfig:/opt/homebrew/opt/sqlite3/lib/pkgconfig:/opt/homebrew/opt/oniguruma/lib/pkgconfig:/opt/homebrew/opt/icu4c/lib/pkgconfig:/opt/homebrew/opt/libpng/lib/pkgconfig:/opt/homebrew/opt/jpeg-turbo/lib/pkgconfig:/opt/homebrew/opt/freetype/lib/pkgconfig:/opt/homebrew/opt/libzip/lib/pkgconfig:/opt/homebrew/opt/mariadb-connector-c/lib/pkgconfig:/opt/homebrew/opt/webp/lib/pkgconfig:/opt/homebrew/opt/libxpm/lib/pkgconfig:/opt/homebrew/opt/zlib/lib/pkgconfig:/opt/homebrew/opt/bison/lib/pkgconfig:/opt/homebrew/opt/libiconv/lib/pkgconfig" >> $GITHUB_ENV + echo "PATH=/opt/homebrew/opt/bison/bin:$PATH" >> $GITHUB_ENV + + - name: Build phpv binary + run: | + mkdir -p $PHPV_ROOT/bin + go build -o $PHPV_ROOT/bin/phpv ./app/phpv.go + echo "$PHPV_ROOT/bin" >> $GITHUB_PATH + + - name: Compute cache key + id: cache-key + run: | + HASH=$(sha256sum \ + internal/repository/memory/graph.go \ + internal/repository/memory/compiler.go \ + internal/repository/memory/registry.go \ + internal/repository/memory/patcher.go \ + internal/repository/disk/silo.go \ + internal/repository/disk/forge.go \ + internal/repository/disk/config.go \ + internal/repository/disk/doctor.go \ + internal/repository/disk/update.go \ + assembler/service.go \ + bundle/service.go \ + app/phpv.go \ + go.mod \ + go.sum \ + .github/workflows/test.yaml \ + | sha256sum | cut -d' ' -f1) + echo "key=phpv-caches-macos-${{ matrix.php_version }}-$HASH" >> $GITHUB_OUTPUT + echo "restore-key=phpv-caches-macos-${{ matrix.php_version }}-" >> $GITHUB_OUTPUT + + - name: Cache downloaded archives + id: cache-phpv + uses: actions/cache@v4 + with: + path: ${{ env.PHPV_ROOT }}/caches + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + ${{ steps.cache-key.outputs.restore-key }} + + - name: Build phpv binary + run: | + mkdir -p $PHPV_ROOT/bin + go build -o $PHPV_ROOT/bin/phpv ./app/phpv.go + echo "$PHPV_ROOT/bin" >> $GITHUB_PATH + + - name: Install PHP ${{ matrix.php_version }} run: | eval "$($PHPV_ROOT/bin/phpv init bash)" - phpv install ${{ matrix.php_version }} --ext $PHPV_EXTENSIONS --force -v + phpv install ${{ matrix.php_version }} - name: Verify PHP ${{ matrix.php_version }} run: | diff --git a/assembler/resolve_flags.go b/assembler/resolve_flags.go index 2d54d71..de045ad 100644 --- a/assembler/resolve_flags.go +++ b/assembler/resolve_flags.go @@ -3,6 +3,7 @@ package assembler import ( "os" "path/filepath" + "runtime" "sort" "strings" @@ -15,16 +16,18 @@ import ( // to inject dependency paths (e.g., curl's --with-openssl={{dep:openssl}}). // // The placeholder is resolved for every dependency that appears in the build -// plan, even if its install directory does not exist yet. The assembler builds -// dependencies in dependency order, so the directory will be created before any -// dependent package is configured. This keeps fresh-state builds from passing a -// literal {{dep:NAME}} string to ./configure. +// plan, but only if the dependency was actually built locally (not when +// satisfied by a system package). If the local install directory doesn't exist, +// the placeholder is removed entirely, letting the package's configure script +// find the system version via pkg-config or default search paths. func (s *Service) resolveDepPlaceholders(flags []string, deps []domain.Dependency) []string { - result := make([]string, len(flags)) - for i, flag := range flags { + result := make([]string, 0, len(flags)) + for _, flag := range flags { + resolved := flag + skip := false for _, dep := range deps { placeholder := "{{dep:" + dep.Name + "}}" - if !strings.Contains(flag, placeholder) { + if !strings.Contains(resolved, placeholder) { continue } ver := dep.Version @@ -35,9 +38,17 @@ func (s *Service) resolveDepPlaceholders(flags []string, deps []domain.Dependenc continue } prefix := s.silo.PackagePrefix(dep.Name, ver) - flag = strings.ReplaceAll(flag, placeholder, prefix) + // Check if the dependency was actually built locally + if fi, err := os.Stat(prefix); err != nil || !fi.IsDir() { + // Dependency was satisfied by system package, skip this flag + skip = true + break + } + resolved = strings.ReplaceAll(resolved, placeholder, prefix) + } + if !skip { + result = append(result, resolved) } - result[i] = flag } return result } @@ -128,6 +139,18 @@ func (s *Service) resolveDependencyFlags(name, phpVersion string, flags []string result = append(result, "--with-icu-dir="+path) } + case flag == "--with-iconv": + if runtime.GOOS == "darwin" { + brewIconv := "/opt/homebrew/opt/libiconv" + if fi, err := os.Stat(filepath.Join(brewIconv, "include", "iconv.h")); err == nil && !fi.IsDir() { + result = append(result, "--with-iconv="+brewIconv) + } else { + result = append(result, flag) + } + } else { + result = append(result, flag) + } + default: result = append(result, flag) } diff --git a/assembler/service.go b/assembler/service.go index ec85faf..3f7f662 100644 --- a/assembler/service.go +++ b/assembler/service.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "sync" "time" @@ -242,6 +243,15 @@ func (s *Service) Assemble(ctx context.Context, name string, version string, sta if len(prepared.ExtraCFlags) > 0 { buildEnv = []string{"CFLAGS=" + strings.Join(prepared.ExtraCFlags, " "), "CXXFLAGS=" + strings.Join(prepared.ExtraCFlags, " ")} } + if runtime.GOOS == "darwin" { + brewIconv := "/opt/homebrew/opt/libiconv" + if _, err := os.Stat(filepath.Join(brewIconv, "include", "iconv.h")); err == nil { + buildEnv = append(buildEnv, + "CPPFLAGS=-I"+filepath.Join(brewIconv, "include"), + "LDFLAGS=-L"+filepath.Join(brewIconv, "lib")+" -Wl,-rpath,"+filepath.Join(brewIconv, "lib"), + ) + } + } depFlags := s.graph.GetConfigureFlags(dep.Name, depVersion) depFlags = append(depFlags, s.resolveDepPlaceholders(prepared.ConfigureFlags, plan.Deps)...) buildDir, _, err := s.forge.Build(ctx, dep.Name, depVersion, sourceDir, buildEnv, depFlags, depPrefix, verbose, jobs) @@ -298,12 +308,13 @@ func (s *Service) Assemble(ctx context.Context, name string, version string, sta } env = setEnvVar(env, "PATH", strings.Join(append(localBinPaths, existingPath), ":")) } - if len(plan.CFlags) > 0 || len(plan.CompilerFlags) > 0 { + if len(plan.CFlags) > 0 || len(plan.CompilerFlags) > 0 || len(plan.CXXCompilerFlags) > 0 { allCFlags := plan.CFlags allCFlags = append(allCFlags, plan.CompilerFlags...) env = setEnvVar(env, "CFLAGS", strings.Join(allCFlags, " ")) compilerRule := s.graph.GetCompilerStdRule(version) cxxflags := memory.CXXFlagsFromCFlagsWithStd(allCFlags, true, compilerRule) + cxxflags = append(cxxflags, plan.CXXCompilerFlags...) env = setEnvVar(env, "CXXFLAGS", strings.Join(cxxflags, " ")) } @@ -331,7 +342,11 @@ func (s *Service) Assemble(ctx context.Context, name string, version string, sta env = setEnvVar(env, "LDFLAGS", strings.Join(append(rpathFlags, depLdFlags...), " ")) } if len(libPaths) > 0 { - env = setEnvVar(env, "LD_LIBRARY_PATH", strings.Join(libPaths, ":")) + libPathEnv := "LD_LIBRARY_PATH" + if runtime.GOOS == "darwin" { + libPathEnv = "DYLD_LIBRARY_PATH" + } + env = setEnvVar(env, libPathEnv, strings.Join(libPaths, ":")) } } if len(depCppFlags) > 0 { @@ -361,6 +376,26 @@ func (s *Service) Assemble(ctx context.Context, name string, version string, sta configureFlags = append([]string{"--enable-static", "--disable-shared"}, configureFlags...) } + if runtime.GOOS == "darwin" && name == "php" { + existingLdFlags := "" + for _, e := range env { + if strings.HasPrefix(e, "LDFLAGS=") { + existingLdFlags = strings.TrimPrefix(e, "LDFLAGS=") + break + } + } + macosLdFlags := []string{"-lresolv"} + brewIconv := "/opt/homebrew/opt/libiconv" + if _, err := os.Stat(filepath.Join(brewIconv, "lib", "libiconv.dylib")); err == nil { + macosLdFlags = append([]string{ + "-L" + filepath.Join(brewIconv, "lib"), + "-Wl,-rpath," + filepath.Join(brewIconv, "lib"), + "-liconv", + }, macosLdFlags...) + } + env = setEnvVar(env, "LDFLAGS", existingLdFlags+" "+strings.Join(macosLdFlags, " ")) + } + emit("configure", fmt.Sprintf("Configuring %s...", name)) prepared, err := s.patcher.Prepare(name, exactVersion, srcPath) if err != nil { @@ -398,11 +433,18 @@ func (s *Service) Assemble(ctx context.Context, name string, version string, sta Version: exactVersion, Prefix: prefix, Env: map[string]string{ - "LD_LIBRARY_PATH": strings.Join(localLibraryPaths, ":"), + libPathEnvName(): strings.Join(localLibraryPaths, ":"), }, }, nil } +func libPathEnvName() string { + if runtime.GOOS == "darwin" { + return "DYLD_LIBRARY_PATH" + } + return "LD_LIBRARY_PATH" +} + // DownloadFailed returns true if any result has an error. func DownloadFailed(results []DownloadResult) bool { for _, r := range results { @@ -429,6 +471,9 @@ func (s *Service) collectDepFlags(prefix string, cppFlags, ldFlags, pcPaths []st } func collectSystemFlags(cppFlags, ldFlags, pcPaths []string) ([]string, []string, []string) { + if runtime.GOOS == "darwin" { + return cppFlags, ldFlags, pcPaths + } cppFlags = appendUnique(cppFlags, "-I/usr/include") ldFlags = appendUnique(ldFlags, "-L/usr/lib64") ldFlags = appendUnique(ldFlags, "-Wl,-rpath,/usr/lib64") diff --git a/assembler/service_test.go b/assembler/service_test.go index f3d4f11..96503ca 100644 --- a/assembler/service_test.go +++ b/assembler/service_test.go @@ -238,13 +238,12 @@ func TestResolveDependencyFlags_RespectsConstraintFallback(t *testing.T) { } } -// TestResolveDepPlaceholders_ResolvesWithoutInstalledPrefix verifies that +// TestResolveDepPlaceholders_ResolvesWithInstalledPrefix verifies that // {{dep:NAME}} placeholders are resolved for dependencies in the build plan -// even when their install prefix directory does not exist yet. The assembler -// builds dependencies in dependency order, so the directory will exist before -// the dependent is configured. Without this behavior, fresh builds pass a -// literal "{{dep:openssl}}" string to ./configure. -func TestResolveDepPlaceholders_ResolvesWithoutInstalledPrefix(t *testing.T) { +// when their install prefix directory exists (i.e., the dependency was built +// locally). The assembler builds dependencies in dependency order, so the +// directory will exist before the dependent is configured. +func TestResolveDepPlaceholders_ResolvesWithInstalledPrefix(t *testing.T) { tmpDir := t.TempDir() mock := &mockSiloRepo{root: tmpDir} siloSvc := silo.NewService(mock, nil) @@ -253,11 +252,38 @@ func TestResolveDepPlaceholders_ResolvesWithoutInstalledPrefix(t *testing.T) { deps := []domain.Dependency{ {Name: "openssl", Version: "1.1.1w|>=1.1.1,<4.0.0"}, } + // Simulate the dependency being built locally by creating the directory. + prefix := filepath.Join(tmpDir, "packages", "openssl", "1.1.1w") + if err := os.MkdirAll(filepath.Join(prefix, "include", "openssl"), 0o755); err != nil { + t.Fatal(err) + } flags := []string{"--with-openssl={{dep:openssl}}"} got := svc.resolveDepPlaceholders(flags, deps) - want := filepath.Join(tmpDir, "packages", "openssl", "1.1.1w") + want := prefix if len(got) != 1 || got[0] != "--with-openssl="+want { t.Errorf("resolveDepPlaceholders = %q, want %q", got, "--with-openssl="+want) } } + +// TestResolveDepPlaceholders_SkipsWhenPrefixMissing verifies that +// {{dep:NAME}} placeholders are skipped when the dependency's install +// directory doesn't exist (i.e., the dependency was satisfied by a system +// package). In this case, the flag is omitted entirely so the package's +// configure script can find the system version via pkg-config. +func TestResolveDepPlaceholders_SkipsWhenPrefixMissing(t *testing.T) { + tmpDir := t.TempDir() + mock := &mockSiloRepo{root: tmpDir} + siloSvc := silo.NewService(mock, nil) + svc := &Service{silo: siloSvc} + + deps := []domain.Dependency{ + {Name: "openssl", Version: "1.1.1w|>=1.1.1,<4.0.0"}, + } + flags := []string{"--with-openssl={{dep:openssl}}"} + got := svc.resolveDepPlaceholders(flags, deps) + + if len(got) != 0 { + t.Errorf("resolveDepPlaceholders = %q, want empty (flag should be skipped)", got) + } +} diff --git a/graph/service.go b/graph/service.go index 226f66b..4f76642 100644 --- a/graph/service.go +++ b/graph/service.go @@ -2,6 +2,7 @@ package graph import ( "fmt" + "runtime" "sort" "strings" @@ -44,11 +45,12 @@ type GraphRepository interface { // BuildPlan consolidates everything needed to build a package. type BuildPlan struct { - Deps []domain.Dependency - ConfigureFlags []string - CFlags []string - CompilerFlags []string - Warnings []string // non-fatal warnings (e.g. conflicting dep versions) + Deps []domain.Dependency + ConfigureFlags []string + CFlags []string + CompilerFlags []string + CXXCompilerFlags []string + Warnings []string // non-fatal warnings (e.g. conflicting dep versions) } // Service wraps GraphRepository and adds value by consolidating @@ -86,21 +88,23 @@ func (s *Service) GetBuildPlan(name string, version string, extensions []string) } else { configureFlags = s.repo.GetConfigureFlags(name, version) } - cflags := s.repo.GetCompilerFlags("gcc", version) + cflags := s.repo.GetCompilerFlags(detectCompiler(), version) compilerRule := s.repo.GetCompilerStdRule(version) - var compilerFlags []string + var cCompilerFlags []string + var cxxCompilerFlags []string if compilerRule.CStd != "" { - compilerFlags = append(compilerFlags, compilerRule.CStd) + cCompilerFlags = append(cCompilerFlags, compilerRule.CStd) } if compilerRule.CXXStd != "" { - compilerFlags = append(compilerFlags, compilerRule.CXXStd) + cxxCompilerFlags = append(cxxCompilerFlags, compilerRule.CXXStd) } return &BuildPlan{ - Deps: deps, - ConfigureFlags: configureFlags, - CFlags: cflags, - CompilerFlags: compilerFlags, - Warnings: warnings, + Deps: deps, + ConfigureFlags: configureFlags, + CFlags: cflags, + CompilerFlags: cCompilerFlags, + CXXCompilerFlags: cxxCompilerFlags, + Warnings: warnings, }, nil } @@ -362,3 +366,12 @@ func (s *Service) GetCompilerStdRule(phpVersion string) domain.CompilerRule { func (s *Service) GetCompilerFlags(compiler string, phpVersion string) []string { return s.repo.GetCompilerFlags(compiler, phpVersion) } + +// detectCompiler returns the compiler name to use for flag selection. +// macOS uses clang by default; Linux uses gcc. +func detectCompiler() string { + if runtime.GOOS == "darwin" { + return "clang" + } + return "gcc" +} diff --git a/graph/service_test.go b/graph/service_test.go index 5b6f8c5..3e0407b 100644 --- a/graph/service_test.go +++ b/graph/service_test.go @@ -102,8 +102,11 @@ func TestService_GetBuildPlan(t *testing.T) { if len(plan.Deps) != 1 { t.Fatalf("expected 1 dep, got %d", len(plan.Deps)) } - if len(plan.CompilerFlags) != 2 { - t.Fatalf("expected 2 compiler flags, got %d", len(plan.CompilerFlags)) + if len(plan.CompilerFlags) != 1 { + t.Fatalf("expected 1 compiler flag, got %d", len(plan.CompilerFlags)) + } + if len(plan.CXXCompilerFlags) != 1 { + t.Fatalf("expected 1 CXX compiler flag, got %d", len(plan.CXXCompilerFlags)) } } diff --git a/internal/repository/disk/forge.go b/internal/repository/disk/forge.go index 63cbe73..477a061 100644 --- a/internal/repository/disk/forge.go +++ b/internal/repository/disk/forge.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "syscall" "time" @@ -144,18 +145,18 @@ func (f *ForgeRepository) buildConfigure(ctx context.Context, name, version, src var usePerl bool var useConfig bool - if _, err := os.Stat(filepath.Join(srcPath, "configure")); err == nil { - configurePath = filepath.Join(srcPath, "configure") - } else if name == "openssl" || name == "ossl" { - if _, err := os.Stat(filepath.Join(srcPath, "config")); err == nil { - configurePath = filepath.Join(srcPath, "config") - useConfig = true - } else if _, err := os.Stat(filepath.Join(srcPath, "Configure")); err == nil { + if name == "openssl" || name == "ossl" { + if _, err := os.Stat(filepath.Join(srcPath, "Configure")); err == nil { configurePath = filepath.Join(srcPath, "Configure") usePerl = true + } else if _, err := os.Stat(filepath.Join(srcPath, "config")); err == nil { + configurePath = filepath.Join(srcPath, "config") + useConfig = true } else { return "", nil, fmt.Errorf("no configure script found for %s", name) } + } else if _, err := os.Stat(filepath.Join(srcPath, "configure")); err == nil { + configurePath = filepath.Join(srcPath, "configure") } else { return "", nil, fmt.Errorf("configure script not found for %s", name) } @@ -169,7 +170,8 @@ func (f *ForgeRepository) buildConfigure(ctx context.Context, name, version, src var configure *exec.Cmd if usePerl { - perlArgs := []string{configurePath, "linux-x86_64"} + opensslTarget := opensslTarget() + perlArgs := []string{configurePath, opensslTarget} for _, a := range args { perlArgs = append(perlArgs, a) } @@ -330,6 +332,16 @@ func forgeEnv(prefix string) map[string]string { return env } +func opensslTarget() string { + if runtime.GOOS == "darwin" { + if runtime.GOARCH == "arm64" { + return "darwin64-arm64-cc" + } + return "darwin64-x86_64-cc" + } + return "linux-x86_64" +} + func findSourceDir(extractDir, name, version string) string { if hasBuildFile(extractDir) { return extractDir diff --git a/internal/repository/memory/compiler.go b/internal/repository/memory/compiler.go index 297d69f..b53eee7 100644 --- a/internal/repository/memory/compiler.go +++ b/internal/repository/memory/compiler.go @@ -22,6 +22,41 @@ type cstdRule struct { } var compilerFlagRuleCandidates = []compilerFlagRule{ + { + Compiler: "clang", + MinPHP: "5.0", + MaxPHP: "7.3", + CFLAGDefs: []domain.CompilerFlagDef{ + {Flag: "-std=gnu11", Purpose: "C11 standard"}, + {Flag: "-fPIC", Purpose: "position-independent code for shared objects"}, + {Flag: "-DTRUE=1", Purpose: "define TRUE for PHP 7.x intl compat"}, + {Flag: "-DFALSE=0", Purpose: "define FALSE for PHP 7.x intl compat"}, + {Flag: "-Wno-error", Purpose: "never treat warnings as errors"}, + {Flag: "-Wno-incompatible-pointer-types", Purpose: "suppress incompatible-pointer warnings"}, + {Flag: "-Wno-implicit-function-declaration", Purpose: "suppress C99 implicit-decl warnings"}, + }, + }, + { + Compiler: "clang", + MinPHP: "7.4", + MaxPHP: "7.99", + CFLAGDefs: []domain.CompilerFlagDef{ + {Flag: "-std=gnu11", Purpose: "C11 standard"}, + {Flag: "-fPIC", Purpose: "position-independent code for shared objects"}, + {Flag: "-Wno-error", Purpose: "never treat warnings as errors"}, + {Flag: "-Wno-incompatible-pointer-types", Purpose: "suppress incompatible-pointer warnings"}, + {Flag: "-Wno-implicit-function-declaration", Purpose: "suppress C99 implicit-decl warnings"}, + }, + }, + { + Compiler: "clang", + MinPHP: "8.0", + MaxPHP: "", + CFLAGDefs: []domain.CompilerFlagDef{ + {Flag: "-Wno-error", Purpose: "never treat warnings as errors"}, + {Flag: "-fPIC", Purpose: "position-independent code for shared objects"}, + }, + }, { Compiler: "gcc", MinPHP: "5.0", diff --git a/shim/service_test.go b/shim/service_test.go index bafd9ba..d130f6b 100644 --- a/shim/service_test.go +++ b/shim/service_test.go @@ -47,7 +47,13 @@ func TestRenderShim_Binary(t *testing.T) { t.Error("shim should reference PHPV_CURRENT") } if !strings.Contains(content, "LD_LIBRARY_PATH") { - t.Error("shim should set LD_LIBRARY_PATH") + t.Error("shim should reference LD_LIBRARY_PATH") + } + if !strings.Contains(content, "DYLD_LIBRARY_PATH") { + t.Error("shim should reference DYLD_LIBRARY_PATH for macOS") + } + if !strings.Contains(content, "LIB_PATH_VAR") { + t.Error("shim should use LIB_PATH_VAR for platform detection") } if !strings.Contains(content, `exec "$PHPV_PREFIX/bin/php" "$@"`) { t.Error("binary shim should exec php from prefix") @@ -66,6 +72,9 @@ func TestRenderShim_DropsParentLdLibraryPath(t *testing.T) { if strings.Contains(content, `"$PHPV_PREFIX/lib:$LD_LIBRARY_PATH"`) { t.Error("shim should NOT prepend to parent LD_LIBRARY_PATH") } + if strings.Contains(content, `"$PHPV_PREFIX/lib:$DYLD_LIBRARY_PATH"`) { + t.Error("shim should NOT prepend to parent DYLD_LIBRARY_PATH") + } if !strings.Contains(content, "PHPV_EXTRA_LD_LIBRARY_PATH") { t.Error("shim should support PHPV_EXTRA_LD_LIBRARY_PATH allowlist") } @@ -77,11 +86,11 @@ func TestRenderShim_HasCleanRoomBlock(t *testing.T) { if err != nil { t.Fatalf("renderShim: %v", err) } - if !strings.Contains(content, "LD_LIBRARY_PATH=\"$PHPV_PREFIX/lib\"") { - t.Error("shim should start LD_LIBRARY_PATH with only PHP prefix") + if !strings.Contains(content, `eval "$LIB_PATH_VAR=\"\$PHPV_PREFIX/lib\""`) { + t.Error("shim should start LIB_PATH_VAR with only PHP prefix") } - if !strings.Contains(content, "export LD_LIBRARY_PATH") { - t.Error("shim should export LD_LIBRARY_PATH") + if !strings.Contains(content, "export $LIB_PATH_VAR") { + t.Error("shim should export LIB_PATH_VAR") } } diff --git a/shim/template.go b/shim/template.go index 19d4246..ea78161 100644 --- a/shim/template.go +++ b/shim/template.go @@ -34,16 +34,21 @@ if [ ! -d "$PHPV_PREFIX" ]; then fi export PHPV_CURRENT="$PHPV_VERSION" # Clean-room: start with only local phpv-managed lib paths. -# The parent's LD_LIBRARY_PATH is NOT inherited — use +# The parent's *_LIBRARY_PATH is NOT inherited — use # PHPV_EXTRA_LD_LIBRARY_PATH to explicitly add system libs. -LD_LIBRARY_PATH="$PHPV_PREFIX/lib" +if [ "$(uname)" = "Darwin" ]; then + LIB_PATH_VAR="DYLD_LIBRARY_PATH" +else + LIB_PATH_VAR="LD_LIBRARY_PATH" +fi +eval "$LIB_PATH_VAR=\"\$PHPV_PREFIX/lib\"" for dep_lib in "$PHPV_ROOT/packages"/*/*/lib; do - [ -d "$dep_lib" ] && LD_LIBRARY_PATH="$dep_lib:$LD_LIBRARY_PATH" + [ -d "$dep_lib" ] && eval "$LIB_PATH_VAR=\"\$dep_lib:\$$LIB_PATH_VAR\"" done if [ -n "$PHPV_EXTRA_LD_LIBRARY_PATH" ]; then - LD_LIBRARY_PATH="$PHPV_EXTRA_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" + eval "$LIB_PATH_VAR=\"\$PHPV_EXTRA_LD_LIBRARY_PATH:\$$LIB_PATH_VAR\"" fi -export LD_LIBRARY_PATH +export $LIB_PATH_VAR {{EXEC}} `