mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
256 lines
7.3 KiB
Go
256 lines
7.3 KiB
Go
package php
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/railwayapp/railpack/core/generate"
|
|
"github.com/railwayapp/railpack/core/plan"
|
|
"github.com/railwayapp/railpack/core/providers/node"
|
|
"github.com/stretchr/objx"
|
|
)
|
|
|
|
const (
|
|
DEFAULT_PHP_VERSION = "8.4.3"
|
|
)
|
|
|
|
type PhpProvider struct{}
|
|
|
|
func (p *PhpProvider) Name() string {
|
|
return "php"
|
|
}
|
|
|
|
func (p *PhpProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
|
|
return ctx.App.HasMatch("index.php") ||
|
|
ctx.App.HasMatch("composer.json"), nil
|
|
}
|
|
|
|
func (p *PhpProvider) Initialize(ctx *generate.GenerateContext) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *PhpProvider) Plan(ctx *generate.GenerateContext) error {
|
|
phpImageStep, err := p.phpImagePackage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Nginx
|
|
nginx := ctx.NewCommandStep("nginx")
|
|
nginx.AddInput(plan.NewStepInput(phpImageStep.Name()))
|
|
nginx.AddCommands([]plan.Command{
|
|
plan.NewFileCommand("/etc/nginx/railpack.conf", "nginx.conf", plan.FileOptions{CustomName: "create nginx config"}),
|
|
plan.NewExecCommand("nginx -t -c /etc/nginx/railpack.conf"),
|
|
plan.NewFileCommand("/etc/php-fpm.conf", "php-fpm.conf", plan.FileOptions{CustomName: "create php-fpm config"}),
|
|
plan.NewFileCommand("/start-nginx.sh", "start-nginx.sh", plan.FileOptions{
|
|
CustomName: "create start nginx script",
|
|
Mode: 0755,
|
|
}),
|
|
})
|
|
|
|
nginx.Assets["start-nginx.sh"] = startNginxScriptAsset
|
|
configFiles, err := p.getConfigFiles(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config files: %w", err)
|
|
}
|
|
|
|
nginx.Assets["nginx.conf"] = configFiles.NginxConf
|
|
nginx.Assets["php-fpm.conf"] = configFiles.PhpFpmConf
|
|
|
|
// Composer
|
|
composer := ctx.NewCommandStep("install:composer")
|
|
composer.AddInput(plan.NewStepInput(nginx.Name()))
|
|
if _, err := p.readComposerJson(ctx); err == nil {
|
|
composer.AddCache(ctx.Caches.AddCache("composer", "/opt/cache/composer"))
|
|
composer.AddEnvVars(map[string]string{"COMPOSER_CACHE_DIR": "/opt/cache/composer"})
|
|
|
|
composer.AddCommands([]plan.Command{
|
|
// Copy composer from the composer image
|
|
plan.CopyCommand{Image: "composer:latest", Src: "/usr/bin/composer", Dest: "/usr/bin/composer"},
|
|
plan.NewCopyCommand("."),
|
|
plan.NewExecCommand("composer install --ignore-platform-reqs"),
|
|
})
|
|
}
|
|
|
|
// Node (if necessary)
|
|
nodeProvider := node.NodeProvider{}
|
|
isNode, err := nodeProvider.Detect(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.usesLaravel(ctx) {
|
|
ctx.Logger.LogInfo("Found Laravel app")
|
|
}
|
|
|
|
if isNode {
|
|
err = nodeProvider.Initialize(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.Logger.LogInfo("Installing Node")
|
|
|
|
miseStep := ctx.GetMiseStepBuilder()
|
|
nodeProvider.InstallMisePackages(ctx, miseStep)
|
|
|
|
install := ctx.NewCommandStep("install:node")
|
|
install.AddInput(plan.NewStepInput(miseStep.Name()))
|
|
nodeProvider.InstallNodeDeps(ctx, install)
|
|
|
|
prune := ctx.NewCommandStep("prune:node")
|
|
prune.AddInput(plan.NewStepInput(install.Name()))
|
|
nodeProvider.PruneNodeDeps(ctx, prune)
|
|
|
|
build := ctx.NewCommandStep("build")
|
|
build.Inputs = []plan.Input{
|
|
plan.NewStepInput(composer.Name()),
|
|
|
|
// Include the app and mise packages (node, pnpm, etc.)
|
|
plan.NewStepInput(install.Name(), plan.InputOptions{
|
|
Include: append([]string{"."}, miseStep.GetOutputPaths()...),
|
|
}),
|
|
}
|
|
nodeProvider.Build(ctx, build)
|
|
|
|
ctx.Deploy.Inputs = []plan.Input{
|
|
plan.NewStepInput(composer.Name()),
|
|
plan.NewStepInput(build.Name(), plan.InputOptions{
|
|
Include: []string{"."},
|
|
Exclude: []string{"node_modules"},
|
|
}),
|
|
plan.NewStepInput(prune.Name(), plan.InputOptions{
|
|
Include: []string{"/app/node_modules"},
|
|
}),
|
|
plan.NewLocalInput("."),
|
|
}
|
|
} else {
|
|
// A manual build command will go here
|
|
build := ctx.NewCommandStep("build")
|
|
build.AddInput(plan.NewStepInput(composer.Name()))
|
|
|
|
ctx.Deploy.Inputs = []plan.Input{
|
|
plan.NewStepInput(build.Name()),
|
|
plan.NewLocalInput("."),
|
|
}
|
|
}
|
|
|
|
ctx.Deploy.StartCmd = "bash /start-nginx.sh"
|
|
|
|
if p.usesLaravel(ctx) {
|
|
ctx.Deploy.Variables["IS_LARAVEL"] = "true"
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PhpProvider) usesLaravel(ctx *generate.GenerateContext) bool {
|
|
return ctx.App.HasMatch("artisan")
|
|
}
|
|
|
|
type ConfigFiles struct {
|
|
NginxConf string
|
|
PhpFpmConf string
|
|
}
|
|
|
|
func (p *PhpProvider) getConfigFiles(ctx *generate.GenerateContext) (*ConfigFiles, error) {
|
|
phpRootDir := "/app"
|
|
if variable := ctx.Env.GetVariable("RAILPACK_PHP_ROOT_DIR"); variable != "" {
|
|
phpRootDir = variable
|
|
} else if p.usesLaravel(ctx) {
|
|
phpRootDir = "/app/public"
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"RAILPACK_PHP_ROOT_DIR": phpRootDir,
|
|
"IS_LARAVEL": p.usesLaravel(ctx),
|
|
}
|
|
|
|
nginxConfTemplate, err := ctx.TemplateFiles([]string{"nginx.template.conf", "nginx.conf"}, nginxConfTemplateAsset, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
phpFpmConf, err := ctx.TemplateFiles([]string{"php-fpm.template.conf", "php-fpm.conf"}, phpFpmConfTemplateAsset, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if nginxConfTemplate.Filename != "" {
|
|
ctx.Logger.LogInfo("Using custom nginx config: %s", nginxConfTemplate.Filename)
|
|
}
|
|
|
|
if phpFpmConf.Filename != "" {
|
|
ctx.Logger.LogInfo("Using custom php-fpm config: %s", phpFpmConf.Filename)
|
|
}
|
|
|
|
return &ConfigFiles{
|
|
NginxConf: nginxConfTemplate.Contents,
|
|
PhpFpmConf: phpFpmConf.Contents,
|
|
}, nil
|
|
}
|
|
|
|
func (p *PhpProvider) phpImagePackage(ctx *generate.GenerateContext) (*generate.ImageStepBuilder, error) {
|
|
imageStep := ctx.NewImageStep("packages:image", func(options *generate.BuildStepOptions) string {
|
|
if phpVersion, ok := options.ResolvedPackages["php"]; ok && phpVersion.ResolvedVersion != nil {
|
|
return getPhpImage(*phpVersion.ResolvedVersion)
|
|
}
|
|
|
|
// Return the default if we were not able to resolve the version
|
|
return getPhpImage(DEFAULT_PHP_VERSION)
|
|
})
|
|
|
|
imageStep.AptPackages = append(imageStep.AptPackages, "nginx", "git", "zip", "unzip")
|
|
|
|
// Include both build and runtime apt packages since we don't have a separate runtime image
|
|
imageStep.AptPackages = append(imageStep.AptPackages, ctx.Config.BuildAptPackages...)
|
|
imageStep.AptPackages = append(imageStep.AptPackages, ctx.Config.Deploy.AptPackages...)
|
|
|
|
php := imageStep.Default("php", DEFAULT_PHP_VERSION)
|
|
|
|
// Read composer.json to get the PHP version
|
|
if composerJson, err := p.readComposerJson(ctx); err == nil {
|
|
phpVersion := objx.New(composerJson).Get("require.php")
|
|
if phpVersion.IsStr() {
|
|
if strings.HasPrefix(phpVersion.Str(), "^") {
|
|
imageStep.Version(php, strings.TrimPrefix(phpVersion.Str(), "^"), "composer.json > require > php")
|
|
} else {
|
|
imageStep.Version(php, phpVersion.Str(), "composer.json > require > php")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure that the version is available on Docker Hub
|
|
imageStep.SetVersionAvailable(php, func(version string) bool {
|
|
image := getPhpImage(version)
|
|
url := fmt.Sprintf("https://registry.hub.docker.com/v2/repositories/library/php/tags/%s", strings.TrimPrefix(image, "php:"))
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode == http.StatusOK
|
|
})
|
|
|
|
return imageStep, nil
|
|
}
|
|
|
|
func getPhpImage(phpVersion string) string {
|
|
return fmt.Sprintf("php:%s-fpm-bookworm", phpVersion)
|
|
}
|
|
|
|
func (p *PhpProvider) readComposerJson(ctx *generate.GenerateContext) (map[string]interface{}, error) {
|
|
var composerJson map[string]interface{}
|
|
err := ctx.App.ReadJSON("composer.json", &composerJson)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return composerJson, nil
|
|
}
|
|
|
|
func (p *PhpProvider) StartCommandHelp() string {
|
|
return ""
|
|
}
|