Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix PHP version detection before Docker image building #547

Closed
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/all-docker-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
description: Python SDK ver to build. Skipped if not specified. Must start with v.
type: string
php-ver:
description: PHP SDK ver to build. Skipped if not specified.
description: PHP SDK ver to build. Skipped if not specified. Must start with v.
type: string
ts-ver:
description: TypeScript SDK ver to build. Skipped if not specified. Must start with v.
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ on: # rebuild any PRs and main branch changes
java_sdk_version:
default: ''
type: string
php_sdk_version:
default: ''
type: string
python_sdk_version:
default: ''
type: string
Expand All @@ -39,6 +42,7 @@ jobs:
go_latest: ${{ steps.latest_version.outputs.go_latest }}
typescript_latest: ${{ steps.latest_version.outputs.typescript_latest }}
java_latest: ${{ steps.latest_version.outputs.java_latest }}
php_latest: ${{ steps.latest_version.outputs.php_latest }}
python_latest: ${{ steps.latest_version.outputs.python_latest }}
csharp_latest: ${{ steps.latest_version.outputs.csharp_latest }}
steps:
Expand Down Expand Up @@ -75,6 +79,13 @@ jobs:
fi
echo "java_latest=$java_latest" >> $GITHUB_OUTPUT

php_latest="${{ github.event.inputs.php_sdk_version }}"
if [ -z "$php_latest" ]; then
php_latest=$(./temporal-features latest-sdk-version --lang php)
echo "Derived latest PHP SDK release version: $php_latest"
fi
echo "php_latest=$php_latest" >> $GITHUB_OUTPUT

python_latest="${{ github.event.inputs.python_sdk_version }}"
if [ -z "$python_latest" ]; then
python_latest=$(./temporal-features latest-sdk-version --lang py)
Expand Down Expand Up @@ -209,6 +220,6 @@ jobs:
go-ver: 'v${{ needs.build-go.outputs.go_latest }}'
ts-ver: 'v${{ needs.build-go.outputs.typescript_latest }}'
java-ver: 'v${{ needs.build-go.outputs.java_latest }}'
php-ver: '${{ needs.build-go.outputs.php_latest }}'
php-ver: 'v${{ needs.build-go.outputs.php_latest }}'
py-ver: 'v${{ needs.build-go.outputs.python_latest }}'
cs-ver: 'v${{ needs.build-go.outputs.csharp_latest }}'
2 changes: 2 additions & 0 deletions cmd/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (p *Preparer) Prepare(ctx context.Context) error {
_, err = p.BuildJavaProgram(ctx, true)
case "ts":
_, err = p.BuildTypeScriptProgram(ctx)
case "php":
_, err = p.BuildPhpProgram(ctx)
case "py":
_, err = p.BuildPythonProgram(ctx)
case "cs":
Expand Down
14 changes: 12 additions & 2 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ func (r *Runner) Run(ctx context.Context, patterns []string) error {
if err == nil {
err = r.RunTypeScriptExternal(ctx, run)
}
case "php":
if r.config.DirName != "" {
r.program, err = sdkbuild.PhpProgramFromDir(
filepath.Join(r.rootDir, r.config.DirName),
r.rootDir,
)
}
if err == nil {
err = r.RunPhpExternal(ctx, run)
}
case "py":
if r.config.DirName != "" {
r.program, err = sdkbuild.PythonProgramFromDir(filepath.Join(r.rootDir, r.config.DirName))
Expand Down Expand Up @@ -545,7 +555,7 @@ func (r *Runner) destroyTempDir() {
func normalizeLangName(lang string) (string, error) {
// Normalize to file extension
switch lang {
case "go", "java", "ts", "py", "cs":
case "go", "java", "ts", "php", "py", "cs":
case "typescript":
lang = "ts"
case "python":
Expand All @@ -561,7 +571,7 @@ func normalizeLangName(lang string) (string, error) {
func expandLangName(lang string) (string, error) {
// Expand to lang name
switch lang {
case "go", "java", "typescript", "python":
case "go", "java", "php", "typescript", "python":
case "ts":
lang = "typescript"
case "py":
Expand Down
77 changes: 77 additions & 0 deletions cmd/run_php.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"context"
"fmt"
"path/filepath"
"github.com/temporalio/features/harness/go/cmd"
"github.com/temporalio/features/sdkbuild"
)

// PreparePhpExternal prepares a PHP run without running it. The preparer
// config directory if present is expected to be a subdirectory name just
// beneath the root directory.
func (p *Preparer) BuildPhpProgram(ctx context.Context) (sdkbuild.Program, error) {
p.log.Info("Building PHP project", "DirName", p.config.DirName)

prog, err := sdkbuild.BuildPhpProgram(ctx, sdkbuild.BuildPhpProgramOptions{
DirName: p.config.DirName,
Version: p.config.Version,
RootDir: p.rootDir,
})
if err != nil {
p.log.Error("failed preparing: %w", err)
return nil, fmt.Errorf("failed preparing: %w", err)
}
return prog, nil
}

// RunPhpExternal runs the PHP run in an external process. This expects
// the server to already be started.
func (r *Runner) RunPhpExternal(ctx context.Context, run *cmd.Run) error {
// If program not built, build it
if r.program == nil {
var err error
if r.program, err = NewPreparer(r.config.PrepareConfig).BuildPhpProgram(ctx); err != nil {
return err
}
}

// Compose RoadRunner command options
args := append(
[]string{
// Namespace
"namespace=" + r.config.Namespace,
// Server address
"address=" + r.config.Server,
},
// Features
run.ToArgs()...,
)
// TLS
if r.config.ClientCertPath != "" {
clientCertPath, err := filepath.Abs(r.config.ClientCertPath)
if err != nil {
return err
}
args = append(args, "tls.cert="+clientCertPath)
}
if r.config.ClientKeyPath != "" {
clientKeyPath, err := filepath.Abs(r.config.ClientKeyPath)
if err != nil {
return err
}
args = append(args, "tls.key="+clientKeyPath)
}

// Run
cmd, err := r.program.NewCommand(ctx, args...)
if err == nil {
// r.log.Debug("Running PHP separately", "Args", cmd.Args)
err = cmd.Run()
}
if err != nil {
return fmt.Errorf("failed running: %w", err)
}
return nil
}
5 changes: 5 additions & 0 deletions harness/php/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# PHP stuff
vendor
rr
rr.exe
composer.lock
22 changes: 22 additions & 0 deletions harness/php/.rr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: "3"
rpc:
listen: tcp://127.0.0.1:6001

server:
command: "php worker.php"

# Workflow and activity mesh service
temporal:
address: ${TEMPORAL_ADDRESS:-localhost:7233}
namespace: ${TEMPORAL_NAMESPACE:-default}
activities:
num_workers: 2

kv:
harness:
driver: memory
config: { }

logs:
mode: development
level: info
24 changes: 24 additions & 0 deletions harness/php/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "temporal/harness",
"type": "project",
"description": "Temporal SDK Harness",
"keywords": ["temporal", "sdk", "harness"],
"license": "MIT",
"require": {
"buggregator/trap": "^1.9",
"spiral/core": "^3.13",
"symfony/process": ">=6.4",
"temporal/sdk": "^2.11.0",
"webmozart/assert": "^1.11"
},
"autoload": {
"psr-4": {
"Harness\\": "src/"
}
},
"scripts": {
"rr-get": "rr get"
},
"prefer-stable": true,
"minimum-stability": "dev"
}
137 changes: 137 additions & 0 deletions sdkbuild/php.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package sdkbuild

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
)

// BuildPhpProgramOptions are options for BuildPhpProgram.
type BuildPhpProgramOptions struct {
// Required version. If it contains a slash it is assumed to be a path with
// a single wheel in the dist directory. Otherwise it is a specific version
// (with leading "v" is trimmed if present).
Version string
// If present, this directory is expected to exist beneath base dir. Otherwise
// a temporary dir is created.
DirName string
RootDir string
}

// PhpProgram is a PHP-specific implementation of Program.
type PhpProgram struct {
dir string
source string
}

var _ Program = (*PhpProgram)(nil)

// BuildPhpProgram builds a PHP program. If completed successfully, this
// can be stored and re-obtained via PhpProgramFromDir() with the Dir() value
func BuildPhpProgram(ctx context.Context, options BuildPhpProgramOptions) (*PhpProgram, error) {
// Working directory
// Create temp dir if needed that we will remove if creating is unsuccessful
var dir string
if options.DirName != "" {
dir = filepath.Join(options.RootDir, options.DirName)
} else {
var err error
dir, err = os.MkdirTemp(options.RootDir, "program-")
if err != nil {
return nil, fmt.Errorf("failed making temp dir: %w", err)
}
}

sourceDir := GetSourceDir(options.RootDir)

// Skip if installed
if st, err := os.Stat(filepath.Join(dir, "vendor")); err == nil && st.IsDir() {
return &PhpProgram{dir, sourceDir}, nil
}

// Copy composer.json from sourceDir into dir
data, err := os.ReadFile(filepath.Join(sourceDir, "composer.json"))
if err != nil {
return nil, fmt.Errorf("failed reading composer.json file: %w", err)
}
err = os.WriteFile(filepath.Join(dir, "composer.json"), data, 0755)
if err != nil {
return nil, fmt.Errorf("failed writing composer.json file: %w", err)
}

// Copy .rr.yaml from sourceDir into dir
data, err = os.ReadFile(filepath.Join(sourceDir, ".rr.yaml"))
if err != nil {
return nil, fmt.Errorf("failed reading .rr.yaml file: %w", err)
}
err = os.WriteFile(filepath.Join(dir, ".rr.yaml"), data, 0755)
if err != nil {
return nil, fmt.Errorf("failed writing .rr.yaml file: %w", err)
}

var cmd *exec.Cmd
// Setup required SDK version if specified
if options.Version != "" {
cmd = exec.CommandContext(ctx, "composer", "req", "temporal/sdk", options.Version, "-W", "--no-install")
cmd.Dir = dir
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed installing SDK deps: %w", err)
}
}

// Install dependencies via composer
cmd = exec.CommandContext(ctx, "composer", "i", "-n", "-o", "-q", "--no-scripts", "--ignore-platform-reqs")
cmd.Dir = dir
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed installing SDK deps: %w", err)
}

// Download RoadRunner
rrExe := filepath.Join(dir, "rr")
if runtime.GOOS == "windows" {
rrExe += ".exe"
}
_, err = os.Stat(rrExe)
if os.IsNotExist(err) {
cmd = exec.CommandContext(ctx, "composer", "run", "rr-get")
cmd.Dir = dir
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed downloading RoadRunner: %w", err)
}
}

return &PhpProgram{dir, sourceDir}, nil
}

// PhpProgramFromDir recreates the Php program from a Dir() result of a
// BuildPhpProgram(). Note, the base directory of dir when it was built must
// also be present.
func PhpProgramFromDir(dir string, rootDir string) (*PhpProgram, error) {
// Quick sanity check on the presence of package.json here
if _, err := os.Stat(filepath.Join(dir, "composer.json")); err != nil {
return nil, fmt.Errorf("failed finding composer.json in dir: %w", err)
}
return &PhpProgram{dir, GetSourceDir(rootDir)}, nil
}

func GetSourceDir(rootDir string) string {
return filepath.Join(rootDir, "harness", "php")
}

// Dir is the directory to run in.
func (p *PhpProgram) Dir() string { return p.dir }

// NewCommand makes a new RoadRunner run command
func (p *PhpProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
args = append([]string{filepath.Join(p.source, "runner.php")}, args...)
cmd := exec.CommandContext(ctx, "php", args...)
cmd.Dir = p.dir
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
return cmd, nil
}
Loading