diff --git a/devbox.go b/devbox.go index 5df7574b432..2786084e86e 100644 --- a/devbox.go +++ b/devbox.go @@ -107,7 +107,11 @@ func (d *Devbox) Plan() (*plansdk.Plan, error) { SharedPlan: d.cfg.SharedPlan, } - return plansdk.MergeUserPlan(userPlan, planner.GetPlan(d.srcDir)) + automatedPlan, err := planner.GetPlan(d.srcDir) + if err != nil { + return nil, err + } + return plansdk.MergeUserPlan(userPlan, automatedPlan) } // Generate creates the directory of Nix files and the Dockerfile that define diff --git a/devbox_test.go b/devbox_test.go index 6c177b3ff1e..5d0bf75e1c8 100644 --- a/devbox_test.go +++ b/devbox_test.go @@ -71,6 +71,8 @@ func assertPlansMatch(t *testing.T, expected *plansdk.Plan, actual *plansdk.Plan assert.ElementsMatch(expected.InstallStage.GetInputFiles(), getFileNames(actual.InstallStage.GetInputFiles()), "InstallStage.InputFiles should match") assert.ElementsMatch(expected.BuildStage.GetInputFiles(), getFileNames(actual.BuildStage.GetInputFiles()), "BuildStage.InputFiles should match") assert.ElementsMatch(expected.StartStage.GetInputFiles(), getFileNames(actual.StartStage.GetInputFiles()), "StartStage.InputFiles should match") + + assert.ElementsMatch(expected.Definitions, actual.Definitions, "Definitions should match") } func fileExists(path string) bool { diff --git a/examples/php/composer.json b/examples/php/composer.json index b85759de98c..131e7589b99 100644 --- a/examples/php/composer.json +++ b/examples/php/composer.json @@ -1,11 +1,8 @@ { "require": { "slim/slim": "^4.10", - "slim/psr7": "^1.5" - }, - "config": { - "platform": { - "php": "8.1.10" - } + "slim/psr7": "^1.5", + "ext-mbstring": "*", + "php": "^8.1" } } diff --git a/examples/php/composer.lock b/examples/php/composer.lock index 6de3d5f29bf..63ff2e28c58 100644 --- a/examples/php/composer.lock +++ b/examples/php/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7d663301ad115500878a85269b118353", + "content-hash": "216dc58eec28d948d71d5fc808279ae3", "packages": [ { "name": "fig/http-message-util", @@ -770,5 +770,8 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], + "platform-overrides": { + "php": "8.1.10" + }, "plugin-api-version": "2.3.0" } diff --git a/planner/languages/php/php_planner.go b/planner/languages/php/php_planner.go index 63e7c7eaea2..c65e582a0db 100644 --- a/planner/languages/php/php_planner.go +++ b/planner/languages/php/php_planner.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/pkg/errors" "go.jetpack.io/devbox/boxcli/usererr" "go.jetpack.io/devbox/planner/plansdk" ) @@ -50,6 +51,7 @@ func (p *Planner) GetPlan(srcDir string) *plansdk.Plan { fmt.Sprintf("php%s", v.MajorMinorConcatenated()), fmt.Sprintf("php%sPackages.composer", v.MajorMinorConcatenated()), }, + Definitions: p.definitions(srcDir, v), } if !plansdk.FileExists(filepath.Join(srcDir, "public/index.php")) { 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 { return plan } +type composerPackages struct { + Config struct { + Platform struct { + PHP string `json:"php"` + } `json:"platform"` + } `json:"config"` + Require map[string]string `json:"require"` +} + func (p *Planner) version(srcDir string) *plansdk.Version { latestVersion, _ := plansdk.NewVersion(supportedPHPVersions[0]) - composerJSONPath := filepath.Join(srcDir, "composer.json") - content, err := os.ReadFile(composerJSONPath) + project, err := p.parseComposerPackages(srcDir) if err != nil { return latestVersion } - composerJSON := struct { - Config struct { - Platform struct { - PHP string `json:"php"` - } `json:"platform"` - } `json:"config"` - }{} - if err := json.Unmarshal(content, &composerJSON); err != nil || - composerJSON.Config.Platform.PHP == "" { + composerPHPVersion := project.Require["php"] + if composerPHPVersion == "" { + composerPHPVersion = project.Config.Platform.PHP + } + + if composerPHPVersion == "" { return latestVersion } - version, err := plansdk.NewVersion(composerJSON.Config.Platform.PHP) + version, err := plansdk.NewVersion(composerPHPVersion) if err != nil { return latestVersion } @@ -110,3 +117,49 @@ func (p *Planner) version(srcDir string) *plansdk.Version { // might as well pick the latest version. return latestVersion } + +func (p *Planner) definitions(srcDir string, v *plansdk.Version) []string { + extensions, err := p.extensions(srcDir) + if len(extensions) == 0 || err != nil { + return []string{} + } + return []string{ + fmt.Sprintf( + "php%s = pkgs.php%s.withExtensions ({ enabled, all }: enabled ++ (with all; [ %s ]));", + v.MajorMinorConcatenated(), + v.MajorMinorConcatenated(), + strings.Join(extensions, " "), + ), + } +} + +func (p *Planner) extensions(srcDir string) ([]string, error) { + project, err := p.parseComposerPackages(srcDir) + if err != nil { + return nil, errors.WithStack(err) + } + + extensions := []string{} + for requirement := range project.Require { + if strings.HasPrefix(requirement, "ext-") { + name := strings.Split(requirement, "-")[1] + if name != "" && name != "json" { + extensions = append(extensions, name) + } + } + } + + return extensions, nil +} + +func (p *Planner) parseComposerPackages(srcDir string) (*composerPackages, error) { + composerJSONPath := filepath.Join(srcDir, "composer.json") + content, err := os.ReadFile(composerJSONPath) + + if err != nil { + return nil, errors.WithStack(err) + } + + project := &composerPackages{} + return project, errors.WithStack(json.Unmarshal(content, project)) +} diff --git a/planner/planner.go b/planner/planner.go index ed1f083d278..df3635c894e 100644 --- a/planner/planner.go +++ b/planner/planner.go @@ -67,15 +67,20 @@ var PLANNERS = []plansdk.Planner{ &zig.Planner{}, } -func GetPlan(srcDir string) *plansdk.Plan { +func GetPlan(srcDir string) (*plansdk.Plan, error) { result := &plansdk.Plan{ DevPackages: []string{}, RuntimePackages: []string{}, } + var err error for _, p := range getRelevantPlanners(srcDir) { - result = plansdk.MergePlans(result, p.GetPlan(srcDir)) + result, err = plansdk.MergePlans(result, p.GetPlan(srcDir)) + if err != nil { + return nil, err + } + } - return result + return result, nil } func IsBuildable(srcDir string) (bool, error) { diff --git a/planner/plansdk/plansdk.go b/planner/plansdk/plansdk.go index af13a266e34..30d504bb0ce 100644 --- a/planner/plansdk/plansdk.go +++ b/planner/plansdk/plansdk.go @@ -27,6 +27,8 @@ type Plan struct { // application. RuntimePackages []string `cue:"[...string]" json:"runtime_packages"` + Definitions []string `cue:"[...string]" json:"definitions"` + Errors []PlanError `json:"errors,omitempty"` } @@ -118,23 +120,21 @@ func (p *Plan) WithError(err error) *Plan { return p } -func MergePlans(plans ...*Plan) *Plan { - plan := &Plan{ - DevPackages: []string{}, - RuntimePackages: []string{}, - } +func MergePlans(plans ...*Plan) (*Plan, error) { + plan := &Plan{} for _, p := range plans { err := mergo.Merge( plan, &Plan{ DevPackages: p.DevPackages, RuntimePackages: p.RuntimePackages, + Definitions: p.Definitions, }, - // Only WithAppendSlice the dev and runtime packages field. + // Only WithAppendSlice definitions, dev, and runtime packages field. mergo.WithAppendSlice, ) if err != nil { - panic(err) // TODO: propagate error. + return nil, err } } @@ -142,7 +142,7 @@ func MergePlans(plans ...*Plan) *Plan { plan.RuntimePackages = pkgslice.Unique(plan.RuntimePackages) plan.SharedPlan = findBuildablePlan(plans...).SharedPlan - return plan + return plan, nil } func findBuildablePlan(plans ...*Plan) *Plan { @@ -156,7 +156,10 @@ func findBuildablePlan(plans ...*Plan) *Plan { } func MergeUserPlan(userPlan *Plan, automatedPlan *Plan) (*Plan, error) { - plan := MergePlans(userPlan, automatedPlan) + plan, err := MergePlans(userPlan, automatedPlan) + if err != nil { + return nil, err + } sharedPlan := &Plan{ SharedPlan: userPlan.SharedPlan, } diff --git a/planner/plansdk/plansdk_test.go b/planner/plansdk/plansdk_test.go index 53b5a76c4e2..9f0499d84b0 100644 --- a/planner/plansdk/plansdk_test.go +++ b/planner/plansdk/plansdk_test.go @@ -24,7 +24,8 @@ func TestMergePlans(t *testing.T) { RuntimePackages: []string{"a", "b", "c"}, SharedPlan: SharedPlan{}, } - actual := MergePlans(plan1, plan2) + actual, err := MergePlans(plan1, plan2) + assert.NoError(t, err) assert.Equal(t, expected, actual) // Base plan (the first one) takes precedence: @@ -51,7 +52,8 @@ func TestMergePlans(t *testing.T) { }, }, } - actual = MergePlans(plan1, plan2) + actual, err = MergePlans(plan1, plan2) + assert.NoError(t, err) assert.Equal(t, expected, actual) // InputFiles can be overwritten: @@ -80,7 +82,8 @@ func TestMergePlans(t *testing.T) { }, }, } - actual = MergePlans(plan1, plan2) + actual, err = MergePlans(plan1, plan2) + assert.NoError(t, err) assert.Equal(t, expected, actual) } diff --git a/testdata/php/php8.1/composer.json b/testdata/php/php8.1/composer.json index e69de29bb2d..bd4f794896b 100644 --- a/testdata/php/php8.1/composer.json +++ b/testdata/php/php8.1/composer.json @@ -0,0 +1,7 @@ +{ + "require": { + "ext-mbstring": "*", + "ext-imagick": "*", + "php": "^8.1" + } +} diff --git a/testdata/php/php8.1/plan.json b/testdata/php/php8.1/plan.json index 8b45032ee7b..da24852a706 100644 --- a/testdata/php/php8.1/plan.json +++ b/testdata/php/php8.1/plan.json @@ -21,5 +21,8 @@ "runtime_packages": [ "php81", "php81Packages.composer" + ], + "definitions": [ + "php81 = pkgs.php81.withExtensions ({ enabled, all }: enabled ++ (with all; [ mbstring imagick ]));" ] -} \ No newline at end of file +} diff --git a/tmpl/development.nix.tmpl b/tmpl/development.nix.tmpl index 4789c2075fc..9d6337ce13e 100644 --- a/tmpl/development.nix.tmpl +++ b/tmpl/development.nix.tmpl @@ -5,6 +5,9 @@ let url = "https://github.com/nixos/nixpkgs/archive/af9e00071d0971eb292fd5abef334e66eda3cb69.tar.gz"; sha256 = "1mdwy0419m5i9ss6s5frbhgzgyccbwycxm5nal40c8486bai0hwy"; }) {}; + {{- range .Definitions}} + {{.}} + {{end -}} in with pkgs; buildEnv { name = "devbox-development"; diff --git a/tmpl/runtime.nix.tmpl b/tmpl/runtime.nix.tmpl index fe686aa31c7..152d867a12a 100644 --- a/tmpl/runtime.nix.tmpl +++ b/tmpl/runtime.nix.tmpl @@ -5,6 +5,9 @@ let url = "https://github.com/nixos/nixpkgs/archive/af9e00071d0971eb292fd5abef334e66eda3cb69.tar.gz"; sha256 = "1mdwy0419m5i9ss6s5frbhgzgyccbwycxm5nal40c8486bai0hwy"; }) {}; + {{- range .Definitions}} + {{.}} + {{end -}} in with pkgs; buildEnv { name = "devbox-runtime"; diff --git a/tmpl/shell.nix.tmpl b/tmpl/shell.nix.tmpl index 05c642678b7..daec6bc5e78 100644 --- a/tmpl/shell.nix.tmpl +++ b/tmpl/shell.nix.tmpl @@ -5,6 +5,9 @@ let url = "https://github.com/nixos/nixpkgs/archive/af9e00071d0971eb292fd5abef334e66eda3cb69.tar.gz"; sha256 = "1mdwy0419m5i9ss6s5frbhgzgyccbwycxm5nal40c8486bai0hwy"; }) {}; + {{- range .Definitions}} + {{.}} + {{end -}} in with pkgs; mkShell { shellHook =