package php import ( "context" "io" "os" "os/exec" "path/filepath" "strings" "forge.lthn.ai/core/go/pkg/cli" ) // DockerBuildOptions configures Docker image building for PHP projects. type DockerBuildOptions struct { // ProjectDir is the path to the PHP/Laravel project. ProjectDir string // ImageName is the name for the Docker image. ImageName string // Tag is the image tag (default: "latest"). Tag string // Platform specifies the target platform (e.g., "linux/amd64", "linux/arm64"). Platform string // Dockerfile is the path to a custom Dockerfile. // If empty, one will be auto-generated for FrankenPHP. Dockerfile string // NoBuildCache disables Docker build cache. NoBuildCache bool // BuildArgs are additional build arguments. BuildArgs map[string]string // Output is the writer for build output (default: os.Stdout). Output io.Writer } // LinuxKitBuildOptions configures LinuxKit image building for PHP projects. type LinuxKitBuildOptions struct { // ProjectDir is the path to the PHP/Laravel project. ProjectDir string // OutputPath is the path for the output image. OutputPath string // Format is the output format: "iso", "qcow2", "raw", "vmdk". Format string // Template is the LinuxKit template name (default: "server-php"). Template string // Variables are template variables to apply. Variables map[string]string // Output is the writer for build output (default: os.Stdout). Output io.Writer } // ServeOptions configures running a production PHP container. type ServeOptions struct { // ImageName is the Docker image to run. ImageName string // Tag is the image tag (default: "latest"). Tag string // ContainerName is the name for the container. ContainerName string // Port is the host port to bind (default: 80). Port int // HTTPSPort is the host HTTPS port to bind (default: 443). HTTPSPort int // Detach runs the container in detached mode. Detach bool // EnvFile is the path to an environment file. EnvFile string // Volumes maps host paths to container paths. Volumes map[string]string // Output is the writer for output (default: os.Stdout). Output io.Writer } // BuildDocker builds a Docker image for the PHP project. func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { if opts.ProjectDir == "" { cwd, err := os.Getwd() if err != nil { return cli.WrapVerb(err, "get", "working directory") } opts.ProjectDir = cwd } // Validate project directory if !IsPHPProject(opts.ProjectDir) { return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) } // Set defaults if opts.ImageName == "" { opts.ImageName = filepath.Base(opts.ProjectDir) } if opts.Tag == "" { opts.Tag = "latest" } if opts.Output == nil { opts.Output = os.Stdout } // Determine Dockerfile path dockerfilePath := opts.Dockerfile var tempDockerfile string if dockerfilePath == "" { // Generate Dockerfile content, err := GenerateDockerfile(opts.ProjectDir) if err != nil { return cli.WrapVerb(err, "generate", "Dockerfile") } // Write to temporary file m := getMedium() tempDockerfile = filepath.Join(opts.ProjectDir, "Dockerfile.core-generated") if err := m.Write(tempDockerfile, content); err != nil { return cli.WrapVerb(err, "write", "Dockerfile") } defer func() { _ = m.Delete(tempDockerfile) }() dockerfilePath = tempDockerfile } // Build Docker image imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) args := []string{"build", "-t", imageRef, "-f", dockerfilePath} if opts.Platform != "" { args = append(args, "--platform", opts.Platform) } if opts.NoBuildCache { args = append(args, "--no-cache") } for key, value := range opts.BuildArgs { args = append(args, "--build-arg", cli.Sprintf("%s=%s", key, value)) } args = append(args, opts.ProjectDir) cmd := exec.CommandContext(ctx, "docker", args...) cmd.Dir = opts.ProjectDir cmd.Stdout = opts.Output cmd.Stderr = opts.Output if err := cmd.Run(); err != nil { return cli.Wrap(err, "docker build failed") } return nil } // BuildLinuxKit builds a LinuxKit image for the PHP project. func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { if opts.ProjectDir == "" { cwd, err := os.Getwd() if err != nil { return cli.WrapVerb(err, "get", "working directory") } opts.ProjectDir = cwd } // Validate project directory if !IsPHPProject(opts.ProjectDir) { return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) } // Set defaults if opts.Template == "" { opts.Template = "server-php" } if opts.Format == "" { opts.Format = "qcow2" } if opts.OutputPath == "" { opts.OutputPath = filepath.Join(opts.ProjectDir, "dist", filepath.Base(opts.ProjectDir)) } if opts.Output == nil { opts.Output = os.Stdout } // Ensure output directory exists m := getMedium() outputDir := filepath.Dir(opts.OutputPath) if err := m.EnsureDir(outputDir); err != nil { return cli.WrapVerb(err, "create", "output directory") } // Find linuxkit binary linuxkitPath, err := lookupLinuxKit() if err != nil { return err } // Get template content templateContent, err := getLinuxKitTemplate(opts.Template) if err != nil { return cli.WrapVerb(err, "get", "template") } // Apply variables if opts.Variables == nil { opts.Variables = make(map[string]string) } // Add project-specific variables opts.Variables["PROJECT_DIR"] = opts.ProjectDir opts.Variables["PROJECT_NAME"] = filepath.Base(opts.ProjectDir) content, err := applyTemplateVariables(templateContent, opts.Variables) if err != nil { return cli.WrapVerb(err, "apply", "template variables") } // Write template to temp file tempYAML := filepath.Join(opts.ProjectDir, ".core-linuxkit.yml") if err := m.Write(tempYAML, content); err != nil { return cli.WrapVerb(err, "write", "template") } defer func() { _ = m.Delete(tempYAML) }() // Build LinuxKit image args := []string{ "build", "--format", opts.Format, "--name", opts.OutputPath, tempYAML, } cmd := exec.CommandContext(ctx, linuxkitPath, args...) cmd.Dir = opts.ProjectDir cmd.Stdout = opts.Output cmd.Stderr = opts.Output if err := cmd.Run(); err != nil { return cli.Wrap(err, "linuxkit build failed") } return nil } // ServeProduction runs a production PHP container. func ServeProduction(ctx context.Context, opts ServeOptions) error { if opts.ImageName == "" { return cli.Err("image name is required") } // Set defaults if opts.Tag == "" { opts.Tag = "latest" } if opts.Port == 0 { opts.Port = 80 } if opts.HTTPSPort == 0 { opts.HTTPSPort = 443 } if opts.Output == nil { opts.Output = os.Stdout } imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) args := []string{"run"} if opts.Detach { args = append(args, "-d") } else { args = append(args, "--rm") } if opts.ContainerName != "" { args = append(args, "--name", opts.ContainerName) } // Port mappings args = append(args, "-p", cli.Sprintf("%d:80", opts.Port)) args = append(args, "-p", cli.Sprintf("%d:443", opts.HTTPSPort)) // Environment file if opts.EnvFile != "" { args = append(args, "--env-file", opts.EnvFile) } // Volume mounts for hostPath, containerPath := range opts.Volumes { args = append(args, "-v", cli.Sprintf("%s:%s", hostPath, containerPath)) } args = append(args, imageRef) cmd := exec.CommandContext(ctx, "docker", args...) cmd.Stdout = opts.Output cmd.Stderr = opts.Output if opts.Detach { output, err := cmd.Output() if err != nil { return cli.WrapVerb(err, "start", "container") } containerID := strings.TrimSpace(string(output)) cli.Print("Container started: %s\n", containerID[:12]) return nil } return cmd.Run() } // Shell opens a shell in a running container. func Shell(ctx context.Context, containerID string) error { if containerID == "" { return cli.Err("container ID is required") } // Resolve partial container ID fullID, err := resolveDockerContainerID(ctx, containerID) if err != nil { return err } cmd := exec.CommandContext(ctx, "docker", "exec", "-it", fullID, "/bin/sh") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } // IsPHPProject checks if the given directory is a PHP project. func IsPHPProject(dir string) bool { composerPath := filepath.Join(dir, "composer.json") return getMedium().IsFile(composerPath) } // commonLinuxKitPaths defines default search locations for linuxkit. var commonLinuxKitPaths = []string{ "/usr/local/bin/linuxkit", "/opt/homebrew/bin/linuxkit", } // lookupLinuxKit finds the linuxkit binary. func lookupLinuxKit() (string, error) { // Check PATH first if path, err := exec.LookPath("linuxkit"); err == nil { return path, nil } m := getMedium() for _, p := range commonLinuxKitPaths { if m.IsFile(p) { return p, nil } } return "", cli.Err("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit") } // getLinuxKitTemplate retrieves a LinuxKit template by name. func getLinuxKitTemplate(name string) (string, error) { // Default server-php template for PHP projects if name == "server-php" { return defaultServerPHPTemplate, nil } // Try to load from container package templates // This would integrate with forge.lthn.ai/core/go/pkg/container return "", cli.Err("template not found: %s", name) } // applyTemplateVariables applies variable substitution to template content. func applyTemplateVariables(content string, vars map[string]string) (string, error) { result := content for key, value := range vars { placeholder := "${" + key + "}" result = strings.ReplaceAll(result, placeholder, value) } return result, nil } // resolveDockerContainerID resolves a partial container ID to a full ID. func resolveDockerContainerID(ctx context.Context, partialID string) (string, error) { cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--no-trunc", "--format", "{{.ID}}") output, err := cmd.Output() if err != nil { return "", cli.WrapVerb(err, "list", "containers") } lines := strings.Split(strings.TrimSpace(string(output)), "\n") var matches []string for _, line := range lines { if strings.HasPrefix(line, partialID) { matches = append(matches, line) } } switch len(matches) { case 0: return "", cli.Err("no container found matching: %s", partialID) case 1: return matches[0], nil default: return "", cli.Err("multiple containers match '%s', be more specific", partialID) } } // defaultServerPHPTemplate is the default LinuxKit template for PHP servers. const defaultServerPHPTemplate = `# LinuxKit configuration for PHP/FrankenPHP server kernel: image: linuxkit/kernel:6.6.13 cmdline: "console=tty0 console=ttyS0" init: - linuxkit/init:v1.0.1 - linuxkit/runc:v1.0.1 - linuxkit/containerd:v1.0.1 onboot: - name: sysctl image: linuxkit/sysctl:v1.0.1 - name: dhcpcd image: linuxkit/dhcpcd:v1.0.1 command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf"] services: - name: getty image: linuxkit/getty:v1.0.1 env: - INSECURE=true - name: sshd image: linuxkit/sshd:v1.0.1 files: - path: etc/ssh/authorized_keys contents: | ${SSH_KEY:-} `