Skip to content

Commit 4568b9d

Browse files
authored
[PHP] Add extension support and better version detection (#113)
## Summary Use composer.json to figure out if any extensions should be installed. Also, improved version detection (using `require` instead of `config`) @loreto should we be using `systemPackages` instead of packages for languages? ## How was it tested? ``` devbox shell php -m | grep mbstring ```
1 parent 1be7c03 commit 4568b9d

File tree

13 files changed

+125
-36
lines changed

13 files changed

+125
-36
lines changed

devbox.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ func (d *Devbox) Plan() (*plansdk.Plan, error) {
107107
SharedPlan: d.cfg.SharedPlan,
108108
}
109109

110-
return plansdk.MergeUserPlan(userPlan, planner.GetPlan(d.srcDir))
110+
automatedPlan, err := planner.GetPlan(d.srcDir)
111+
if err != nil {
112+
return nil, err
113+
}
114+
return plansdk.MergeUserPlan(userPlan, automatedPlan)
111115
}
112116

113117
// Generate creates the directory of Nix files and the Dockerfile that define

devbox_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func assertPlansMatch(t *testing.T, expected *plansdk.Plan, actual *plansdk.Plan
7171
assert.ElementsMatch(expected.InstallStage.GetInputFiles(), getFileNames(actual.InstallStage.GetInputFiles()), "InstallStage.InputFiles should match")
7272
assert.ElementsMatch(expected.BuildStage.GetInputFiles(), getFileNames(actual.BuildStage.GetInputFiles()), "BuildStage.InputFiles should match")
7373
assert.ElementsMatch(expected.StartStage.GetInputFiles(), getFileNames(actual.StartStage.GetInputFiles()), "StartStage.InputFiles should match")
74+
75+
assert.ElementsMatch(expected.Definitions, actual.Definitions, "Definitions should match")
7476
}
7577

7678
func fileExists(path string) bool {

examples/php/composer.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
{
22
"require": {
33
"slim/slim": "^4.10",
4-
"slim/psr7": "^1.5"
5-
},
6-
"config": {
7-
"platform": {
8-
"php": "8.1.10"
9-
}
4+
"slim/psr7": "^1.5",
5+
"ext-mbstring": "*",
6+
"php": "^8.1"
107
}
118
}

examples/php/composer.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

planner/languages/php/php_planner.go

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"strings"
1212

13+
"github.com/pkg/errors"
1314
"go.jetpack.io/devbox/boxcli/usererr"
1415
"go.jetpack.io/devbox/planner/plansdk"
1516
)
@@ -50,6 +51,7 @@ func (p *Planner) GetPlan(srcDir string) *plansdk.Plan {
5051
fmt.Sprintf("php%s", v.MajorMinorConcatenated()),
5152
fmt.Sprintf("php%sPackages.composer", v.MajorMinorConcatenated()),
5253
},
54+
Definitions: p.definitions(srcDir, v),
5355
}
5456
if !plansdk.FileExists(filepath.Join(srcDir, "public/index.php")) {
5557
return plan.WithError(usererr.New("Can't build. No public/index.php found."))
@@ -66,28 +68,33 @@ func (p *Planner) GetPlan(srcDir string) *plansdk.Plan {
6668
return plan
6769
}
6870

71+
type composerPackages struct {
72+
Config struct {
73+
Platform struct {
74+
PHP string `json:"php"`
75+
} `json:"platform"`
76+
} `json:"config"`
77+
Require map[string]string `json:"require"`
78+
}
79+
6980
func (p *Planner) version(srcDir string) *plansdk.Version {
7081
latestVersion, _ := plansdk.NewVersion(supportedPHPVersions[0])
71-
composerJSONPath := filepath.Join(srcDir, "composer.json")
72-
content, err := os.ReadFile(composerJSONPath)
82+
project, err := p.parseComposerPackages(srcDir)
7383

7484
if err != nil {
7585
return latestVersion
7686
}
7787

78-
composerJSON := struct {
79-
Config struct {
80-
Platform struct {
81-
PHP string `json:"php"`
82-
} `json:"platform"`
83-
} `json:"config"`
84-
}{}
85-
if err := json.Unmarshal(content, &composerJSON); err != nil ||
86-
composerJSON.Config.Platform.PHP == "" {
88+
composerPHPVersion := project.Require["php"]
89+
if composerPHPVersion == "" {
90+
composerPHPVersion = project.Config.Platform.PHP
91+
}
92+
93+
if composerPHPVersion == "" {
8794
return latestVersion
8895
}
8996

90-
version, err := plansdk.NewVersion(composerJSON.Config.Platform.PHP)
97+
version, err := plansdk.NewVersion(composerPHPVersion)
9198
if err != nil {
9299
return latestVersion
93100
}
@@ -110,3 +117,49 @@ func (p *Planner) version(srcDir string) *plansdk.Version {
110117
// might as well pick the latest version.
111118
return latestVersion
112119
}
120+
121+
func (p *Planner) definitions(srcDir string, v *plansdk.Version) []string {
122+
extensions, err := p.extensions(srcDir)
123+
if len(extensions) == 0 || err != nil {
124+
return []string{}
125+
}
126+
return []string{
127+
fmt.Sprintf(
128+
"php%s = pkgs.php%s.withExtensions ({ enabled, all }: enabled ++ (with all; [ %s ]));",
129+
v.MajorMinorConcatenated(),
130+
v.MajorMinorConcatenated(),
131+
strings.Join(extensions, " "),
132+
),
133+
}
134+
}
135+
136+
func (p *Planner) extensions(srcDir string) ([]string, error) {
137+
project, err := p.parseComposerPackages(srcDir)
138+
if err != nil {
139+
return nil, errors.WithStack(err)
140+
}
141+
142+
extensions := []string{}
143+
for requirement := range project.Require {
144+
if strings.HasPrefix(requirement, "ext-") {
145+
name := strings.Split(requirement, "-")[1]
146+
if name != "" && name != "json" {
147+
extensions = append(extensions, name)
148+
}
149+
}
150+
}
151+
152+
return extensions, nil
153+
}
154+
155+
func (p *Planner) parseComposerPackages(srcDir string) (*composerPackages, error) {
156+
composerJSONPath := filepath.Join(srcDir, "composer.json")
157+
content, err := os.ReadFile(composerJSONPath)
158+
159+
if err != nil {
160+
return nil, errors.WithStack(err)
161+
}
162+
163+
project := &composerPackages{}
164+
return project, errors.WithStack(json.Unmarshal(content, project))
165+
}

planner/planner.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,20 @@ var PLANNERS = []plansdk.Planner{
6767
&zig.Planner{},
6868
}
6969

70-
func GetPlan(srcDir string) *plansdk.Plan {
70+
func GetPlan(srcDir string) (*plansdk.Plan, error) {
7171
result := &plansdk.Plan{
7272
DevPackages: []string{},
7373
RuntimePackages: []string{},
7474
}
75+
var err error
7576
for _, p := range getRelevantPlanners(srcDir) {
76-
result = plansdk.MergePlans(result, p.GetPlan(srcDir))
77+
result, err = plansdk.MergePlans(result, p.GetPlan(srcDir))
78+
if err != nil {
79+
return nil, err
80+
}
81+
7782
}
78-
return result
83+
return result, nil
7984
}
8085

8186
func IsBuildable(srcDir string) (bool, error) {

planner/plansdk/plansdk.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Plan struct {
2727
// application.
2828
RuntimePackages []string `cue:"[...string]" json:"runtime_packages"`
2929

30+
Definitions []string `cue:"[...string]" json:"definitions"`
31+
3032
Errors []PlanError `json:"errors,omitempty"`
3133
}
3234

@@ -118,31 +120,29 @@ func (p *Plan) WithError(err error) *Plan {
118120
return p
119121
}
120122

121-
func MergePlans(plans ...*Plan) *Plan {
122-
plan := &Plan{
123-
DevPackages: []string{},
124-
RuntimePackages: []string{},
125-
}
123+
func MergePlans(plans ...*Plan) (*Plan, error) {
124+
plan := &Plan{}
126125
for _, p := range plans {
127126
err := mergo.Merge(
128127
plan,
129128
&Plan{
130129
DevPackages: p.DevPackages,
131130
RuntimePackages: p.RuntimePackages,
131+
Definitions: p.Definitions,
132132
},
133-
// Only WithAppendSlice the dev and runtime packages field.
133+
// Only WithAppendSlice definitions, dev, and runtime packages field.
134134
mergo.WithAppendSlice,
135135
)
136136
if err != nil {
137-
panic(err) // TODO: propagate error.
137+
return nil, err
138138
}
139139
}
140140

141141
plan.DevPackages = pkgslice.Unique(plan.DevPackages)
142142
plan.RuntimePackages = pkgslice.Unique(plan.RuntimePackages)
143143
plan.SharedPlan = findBuildablePlan(plans...).SharedPlan
144144

145-
return plan
145+
return plan, nil
146146
}
147147

148148
func findBuildablePlan(plans ...*Plan) *Plan {
@@ -156,7 +156,10 @@ func findBuildablePlan(plans ...*Plan) *Plan {
156156
}
157157

158158
func MergeUserPlan(userPlan *Plan, automatedPlan *Plan) (*Plan, error) {
159-
plan := MergePlans(userPlan, automatedPlan)
159+
plan, err := MergePlans(userPlan, automatedPlan)
160+
if err != nil {
161+
return nil, err
162+
}
160163
sharedPlan := &Plan{
161164
SharedPlan: userPlan.SharedPlan,
162165
}

planner/plansdk/plansdk_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ func TestMergePlans(t *testing.T) {
2424
RuntimePackages: []string{"a", "b", "c"},
2525
SharedPlan: SharedPlan{},
2626
}
27-
actual := MergePlans(plan1, plan2)
27+
actual, err := MergePlans(plan1, plan2)
28+
assert.NoError(t, err)
2829
assert.Equal(t, expected, actual)
2930

3031
// Base plan (the first one) takes precedence:
@@ -51,7 +52,8 @@ func TestMergePlans(t *testing.T) {
5152
},
5253
},
5354
}
54-
actual = MergePlans(plan1, plan2)
55+
actual, err = MergePlans(plan1, plan2)
56+
assert.NoError(t, err)
5557
assert.Equal(t, expected, actual)
5658

5759
// InputFiles can be overwritten:
@@ -80,7 +82,8 @@ func TestMergePlans(t *testing.T) {
8082
},
8183
},
8284
}
83-
actual = MergePlans(plan1, plan2)
85+
actual, err = MergePlans(plan1, plan2)
86+
assert.NoError(t, err)
8487
assert.Equal(t, expected, actual)
8588
}
8689

testdata/php/php8.1/composer.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"require": {
3+
"ext-mbstring": "*",
4+
"ext-imagick": "*",
5+
"php": "^8.1"
6+
}
7+
}

testdata/php/php8.1/plan.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@
2121
"runtime_packages": [
2222
"php81",
2323
"php81Packages.composer"
24+
],
25+
"definitions": [
26+
"php81 = pkgs.php81.withExtensions ({ enabled, all }: enabled ++ (with all; [ mbstring imagick ]));"
2427
]
25-
}
28+
}

0 commit comments

Comments
 (0)