pkg/php/php.go (199 lines of code) (raw):
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package php contains PHP buildpack library code.
package php
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/buildpacks/pkg/appengine"
"github.com/GoogleCloudPlatform/buildpacks/pkg/cache"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/runtime"
"github.com/buildpacks/libcnb/v2"
)
const (
// composerJSON is the name of the Composer package descriptor file.
composerJSON = "composer.json"
// composerLock is the name of the Composer lock file.
composerLock = "composer.lock"
// Vendor is the name of the Composer vendor directory.
Vendor = "vendor"
phpVersionKey = "php_version"
dependencyHashKey = "dependency_hash"
composerVersionKey = "php"
// PHPIni is the content of the php.ini config file
PHPIni = `
; Copyright 2022 Google Inc.
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
expose_php = Off
memory_limit = -1
max_execution_time = 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Error handling and logging, based on php.ini-production. ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 0
ignore_repeated_errors = Off
ignore_repeated_source = Off
html_errors = Off
zend.assertions = -1
;; Enable maximum file sizes up to Front-End limits.
upload_max_filesize = 32M
post_max_size = 32M
`
// ComposerArgsEnv is an environment variable used to pass custom composer variables.
ComposerArgsEnv = "GOOGLE_COMPOSER_ARGS"
// ComposerVersion is used to determine which version for composer to install.
ComposerVersion = "GOOGLE_COMPOSER_VERSION"
// CustomNginxConfig is an environment variable to pass a custom nginx configuration.
CustomNginxConfig = "GOOGLE_CUSTOM_NGINX_CONFIG"
// NginxServesStaticFiles is an environment variable to configure Nginx to serve static files.
NginxServesStaticFiles = "NGINX_SERVES_STATIC_FILES"
)
type composerScriptsJSON struct {
GCPBuild string `json:"gcp-build"`
}
// ComposerJSON represents the contents of a composer.json file.
type ComposerJSON struct {
Require map[string]string `json:"require"`
Scripts composerScriptsJSON `json:"scripts"`
}
// SupportsAppEngineApis is a function that returns true if App Engine API access is enabled
func SupportsAppEngineApis(ctx *gcp.Context) (bool, error) {
if os.Getenv(env.Runtime) == "php55" {
return true, nil
}
return appengine.ApisEnabled(ctx)
}
// ReadComposerJSON returns the deserialized composer.json from the given dir. Empty dir uses the current working directory.
func ReadComposerJSON(dir string) (*ComposerJSON, error) {
f := filepath.Join(dir, composerJSON)
rawcjs, err := ioutil.ReadFile(f)
if err != nil {
return nil, gcp.InternalErrorf("reading %s: %v", composerJSON, err)
}
var cjs ComposerJSON
if err := json.Unmarshal(rawcjs, &cjs); err != nil {
return nil, gcp.UserErrorf("unmarshalling %s: %v", composerJSON, err)
}
return &cjs, nil
}
// version returns the installed version of PHP.
func version(ctx *gcp.Context) (string, error) {
result, err := ctx.Exec([]string{"php", "-r", "echo PHP_VERSION;"})
if err != nil {
return "", err
}
return result.Stdout, nil
}
// composerInstall runs `composer install` with the given flags.
func composerInstall(ctx *gcp.Context, flags []string) error {
cmd := append([]string{"composer", "install"}, flags...)
if _, err := ctx.Exec(cmd, gcp.WithUserAttribution); err != nil {
return err
}
return nil
}
// ComposerInstall runs `composer install`, using the cache iff a lock file is present.
// It creates a layer, so it returns the layer so that the caller may further modify it
// if they desire.
func ComposerInstall(ctx *gcp.Context, cacheTag string) (*libcnb.Layer, error) {
var flags []string
if composerArgs := os.Getenv(ComposerArgsEnv); composerArgs != "" {
flags = strings.Split(composerArgs, " ")
} else {
// We don't install dev dependencies (i.e. we pass --no-dev to composer) because doing so has caused
// problems for customers in the past. For more information see these links:
// https://github.com/GoogleCloudPlatform/php-docs-samples/issues/736
// https://github.com/GoogleCloudPlatform/runtimes-common/pull/763
// https://github.com/GoogleCloudPlatform/runtimes-common/commit/6c4970f609d80f9436ac58ae272cfcc6bcd57143
flags = []string{"--no-dev", "--no-progress", "--no-interaction", "--optimize-autoloader"}
}
if err := ctx.RemoveAll(Vendor); err != nil {
return nil, err
}
l, err := ctx.Layer("composer", gcp.CacheLayer)
if err != nil {
return nil, fmt.Errorf("creating layer: %w", err)
}
layerVendor := filepath.Join(l.Path, Vendor)
composerLockExists, err := ctx.FileExists(composerLock)
if err != nil {
return nil, err
}
// If there's no composer.lock then don't attempt to cache. We'd have to cache using composer.json,
// which could result in outdated dependencies if the version constraints in composer.json resolve
// to newer versions in the future.
if !composerLockExists {
ctx.Logf("*** Improve build performance by generating and committing %s.", composerLock)
if err := composerInstall(ctx, flags); err != nil {
return nil, err
}
return l, nil
}
currentPHPVersion, err := version(ctx)
if err != nil {
return nil, err
}
hash, cached, err := cache.HashAndCheck(ctx, l, dependencyHashKey, cache.WithFiles(composerJSON, composerLock), cache.WithStrings(currentPHPVersion))
if err != nil {
return nil, err
}
if cached {
// PHP expects the vendor/ directory to be in the application directory.
if _, err := ctx.Exec([]string{"cp", "--archive", layerVendor, Vendor}, gcp.WithUserTimingAttribution); err != nil {
return nil, err
}
} else {
ctx.Logf("Installing application dependencies.")
// Clear layer so we don't end up with outdated dependencies (e.g. something was removed from composer.json).
if err := ctx.ClearLayer(l); err != nil {
return nil, fmt.Errorf("clearing layer %q: %w", l.Name, err)
}
if err := composerInstall(ctx, flags); err != nil {
return nil, err
}
// Update the layer metadata.
cache.Add(ctx, l, dependencyHashKey, hash)
// Ensure vendor exists even if no dependencies were installed.
if err := ctx.MkdirAll(Vendor, 0755); err != nil {
return nil, err
}
if _, err := ctx.Exec([]string{"cp", "--archive", Vendor, layerVendor}, gcp.WithUserTimingAttribution); err != nil {
return nil, err
}
}
return l, nil
}
// ComposerRequire runs `composer require` with the given packages. It expects packages to
// be specified as `composer require` would expect them on the command line, for example
// "myorg/mypackage:^0.7". It does no caching.
func ComposerRequire(ctx *gcp.Context, packages []string) error {
cmd := append([]string{"composer", "require", "--no-progress", "--no-interaction"}, packages...)
if _, err := ctx.Exec(cmd, gcp.WithUserAttribution); err != nil {
return err
}
return nil
}
// GetInstallableRuntime returns the installable runtime prefix.
func GetInstallableRuntime(ctx *gcp.Context) runtime.InstallableRuntime {
return runtime.PHP
}
// ExtractVersion extracts the php version from the environment, composer.json.
func ExtractVersion(ctx *gcp.Context) (string, error) {
// get the runtime version from env.RuntimeVersion
if v := os.Getenv(env.RuntimeVersion); v != "" {
ctx.Logf("Using runtime version from %s: %s", env.RuntimeVersion, v)
return v, nil
}
// get the runtime version from the composer.json file
composerFilePath := filepath.Join(ctx.ApplicationRoot(), composerJSON)
composerFileExists, err := ctx.FileExists(composerFilePath)
if err != nil {
return "", err
}
if composerFileExists {
v, err := composerFileVersion(ctx)
if err != nil {
return "", err
}
if v != "" {
ctx.Logf("Using php version from %s %s: %s", composerJSON, composerVersionKey, v)
return v, nil
}
}
return "", nil
}
// composerFileVersion extracts the version number from composer.json. returns an error in
// case the version cannot be read.
func composerFileVersion(ctx *gcp.Context) (string, error) {
cjs, err := ReadComposerJSON(ctx.ApplicationRoot())
if err != nil {
return "", err
}
// check if composer json has specified php version.
v, ok := cjs.Require[composerVersionKey]
if !ok {
ctx.Logf("composer.json exists but does not specify a php version")
return "", nil
}
return v, nil
}