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

[planners] Separate shell plan from build plan #227

Merged
merged 5 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Suggest extra packages that the users may need
  • Loading branch information
LucilleH committed Oct 17, 2022
commit ba4a3b7561ea901529c9084b89d7bd491dc105ab
25 changes: 19 additions & 6 deletions devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ func (d *Devbox) Build(flags *docker.BuildFlags) error {
// Plan creates a plan of the actions that devbox will take to generate its
// shell environment.
func (d *Devbox) ShellPlan() *plansdk.ShellPlan {
shellPlan := planner.GetShellPlan(d.srcDir, d.cfg.Packages)
shellPlan.DevPackages = d.cfg.Packages
userDefinedPkgs := d.cfg.Packages
shellPlan := planner.GetShellPlan(d.srcDir, userDefinedPkgs)
shellPlan.DevPackages = userDefinedPkgs

return shellPlan
}
Expand Down Expand Up @@ -195,17 +196,29 @@ func (d *Devbox) Shell() error {
}

nixShellFilePath := filepath.Join(d.srcDir, ".devbox/gen/shell.nix")
sh, err := nix.DetectShell(
shell, err := nix.DetectShell(
nix.WithPlanInitHook(strings.Join(plan.ShellInitHook, "\n")),
nix.WithProfile(profileDir),
nix.WithHistoryFile(filepath.Join(d.srcDir, shellHistoryFile)),
)
if err != nil {
// Fall back to using a plain Nix shell.
sh = &nix.Shell{}
shell = &nix.Shell{}
}
sh.UserInitHook = d.cfg.Shell.InitHook.String()
return sh.Run(nixShellFilePath)

allPkgs := planner.GetShellPackageSuggestion(d.srcDir, d.cfg.Packages)
pkgsToSuggest, _ := lo.Difference(allPkgs, d.cfg.Packages)
if len(pkgsToSuggest) > 0 {
s := fmt.Sprintf("devbox add %s", strings.Join(pkgsToSuggest, " "))
fmt.Fprintf(
d.writer,
"We detected extra packages you may need. To install them, run `%s`\n",
color.HiYellowString(s),
)
}

shell.UserInitHook = d.cfg.Shell.InitHook.String()
return shell.Run(nixShellFilePath)
}

func (d *Devbox) Exec(cmds ...string) error {
Expand Down
14 changes: 0 additions & 14 deletions pkgslice/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,3 @@ func Exclude(s []string, elems []string) []string {
}
return filtered
}

// returns true when superset includes all elements from subset.
func Contains(superset []string, subset []string) bool {
sMap := make(map[string]bool, len(superset))
for _, str := range superset {
sMap[str] = true
}
for _, e := range subset {
if _, ok := sMap[e]; !ok {
return false
}
}
return true
}
12 changes: 11 additions & 1 deletion planner/languages/dotnet/dotnet_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
proj, err := project(srcDir)
if err != nil {
return &plansdk.ShellPlan{}
}
dotNetPkg, err := dotNetNixPackage(proj)
if err != nil {
return &plansdk.ShellPlan{}
}
return &plansdk.ShellPlan{
DevPackages: []string{dotNetPkg},
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
6 changes: 5 additions & 1 deletion planner/languages/golang/golang_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
goPkg := getGoPackage(srcDir)

return &plansdk.ShellPlan{
DevPackages: []string{goPkg},
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
4 changes: 3 additions & 1 deletion planner/languages/haskell/haskell_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
return &plansdk.ShellPlan{
DevPackages: []string{"stack", "libiconv", "libffi", "binutils", "ghc"},
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
13 changes: 12 additions & 1 deletion planner/languages/java/java_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
builderTool, err := p.packageManager(srcDir)
if err != nil {
return &plansdk.ShellPlan{}
}
devPackages, err := p.devPackages(srcDir, builderTool)
if err != nil {
return &plansdk.ShellPlan{}
}

return &plansdk.ShellPlan{
DevPackages: devPackages,
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
8 changes: 7 additions & 1 deletion planner/languages/javascript/nodejs_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
pkgManager := p.packageManager(srcDir)
project := p.nodeProject(srcDir)
packages := p.packages(pkgManager, project)

return &plansdk.ShellPlan{
DevPackages: packages,
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
4 changes: 4 additions & 0 deletions planner/languages/nginx/nginx_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func (p *Planner) IsRelevant(srcDir string) bool {

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{
DevPackages: []string{
"nginx",
"shell-nginx",
},
Definitions: []string{
fmt.Sprintf(nginxShellStartScript, srcDir, p.shellConfig(srcDir)),
},
Expand Down
4 changes: 4 additions & 0 deletions planner/languages/php/php_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (p *Planner) IsRelevant(srcDir string) bool {
func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
v := p.version(srcDir)
return &plansdk.ShellPlan{
DevPackages: []string{
fmt.Sprintf("php%s", v.MajorMinorConcatenated()),
fmt.Sprintf("php%sPackages.composer", v.MajorMinorConcatenated()),
},
Definitions: p.definitions(srcDir, v),
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is a user able to specify the definitions they want to use, if the default-planner one is not correct for them?

Copy link
Collaborator Author

@LucilleH LucilleH Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not right now. Though, this is not a regression because we have that problem before.

}
Expand Down
3 changes: 3 additions & 0 deletions planner/languages/python/python_pip_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func (p *PIPPlanner) IsRelevant(srcDir string) bool {

func (p *PIPPlanner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{
DevPackages: []string{
"python3",
},
ShellInitHook: []string{p.shellInitHook(srcDir)},
}
}
Expand Down
10 changes: 9 additions & 1 deletion planner/languages/python/python_poetry_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ func (p *PoetryPlanner) IsRelevant(srcDir string) bool {
}

func (p *PoetryPlanner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
version := p.PythonVersion(srcDir)
pythonPkg := fmt.Sprintf("python%s", version.MajorMinorConcatenated())

return &plansdk.ShellPlan{
DevPackages: []string{
pythonPkg,
"poetry",
},
}
}

func (p *PoetryPlanner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
12 changes: 12 additions & 0 deletions planner/languages/ruby/ruby_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,19 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
gemfile := filepath.Join(srcDir, "Gemfile")
v := parseRubyVersion(gemfile)
pkg, ok := nixPackages[semver.MajorMinor(v)]
if !ok {
pkg = defaultPkg
}

return &plansdk.ShellPlan{
DevPackages: []string{
pkg,
"gcc", // for rails
"gnumake", // for rails
},
ShellInitHook: []string{plansdk.WelcomeMessage(
"It looks like you are developing a Ruby project.\n" +
"To keep dependencies isolated, it is recommended that you install them in deployment mode, by running:\n" +
Expand Down
15 changes: 14 additions & 1 deletion planner/languages/rust/rust_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,22 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{
plan := &plansdk.ShellPlan{
NixOverlays: []string{RustOxalicaOverlay},
}
manifest, err := p.cargoManifest(srcDir)
if err != nil {
return plan
}
rustVersion, err := p.rustOxalicaVersion(manifest)
if err != nil {
return plan
}

rustPkgDev := fmt.Sprintf("rust-bin.stable.%s.default", rustVersion)
plan.DevPackages = []string{rustPkgDev, "gcc"}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A style nit is that its easier to track what fields are filled in the ShellPlan struct if we construct it at the end, as in:

manifest, err := ...
rustVersion, err := ...
rustPkgDev := fmt.Sprintf(....
return &plansdk.ShellPlan {
   DevPackages: []string{rustPkgDev, "gcc"}
   NixOverlays: ...
}

Copy link
Collaborator Author

@LucilleH LucilleH Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to return the nix overlay on error cases, which is why this is formatted this way. Thoughts?


return plan
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
4 changes: 3 additions & 1 deletion planner/languages/zig/zig_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ func (p *Planner) IsRelevant(srcDir string) bool {
}

func (p *Planner) GetShellPlan(srcDir string) *plansdk.ShellPlan {
return &plansdk.ShellPlan{}
return &plansdk.ShellPlan{
DevPackages: []string{"zig"},
}
}

func (p *Planner) GetBuildPlan(srcDir string) *plansdk.BuildPlan {
Expand Down
24 changes: 20 additions & 4 deletions planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package planner

import (
"github.com/samber/lo"
"go.jetpack.io/devbox/boxcli/usererr"
"go.jetpack.io/devbox/pkgslice"
"go.jetpack.io/devbox/planner/languages/c"
"go.jetpack.io/devbox/planner/languages/clojure"
"go.jetpack.io/devbox/planner/languages/cplusplus"
Expand Down Expand Up @@ -69,27 +69,43 @@ var PLANNERS = []plansdk.Planner{
&zig.Planner{},
}

// Return a merged shell plan from all planners.
// Return a merged shell plan from shell planners if user defined packages
// contain one or more dev packages from a shell planner.
func GetShellPlan(srcDir string, userPkgs []string) *plansdk.ShellPlan {
result := &plansdk.ShellPlan{}
planners := getRelevantPlanners(srcDir)
for _, p := range planners {
if pkgslice.Contains(userPkgs, p.GetShellPlan(srcDir).DevPackages) {
pkgs := p.GetShellPlan(srcDir).DevPackages
mutualPkgs := lo.Intersect(userPkgs, pkgs)
// Only apply shell plan if user packages list all the packages from shell plan.
if len(mutualPkgs) == len(pkgs) {
// if merge fails, we return no errors for now.
result, _ = plansdk.MergeShellPlans(result, p.GetShellPlan(srcDir))
}
}
return result
}

// Return a merged shell plan from all planners.
func GetShellPackageSuggestion(srcDir string, userPkgs []string) []string {
result := &plansdk.ShellPlan{}
planners := getRelevantPlanners(srcDir)
for _, p := range planners {
result, _ = plansdk.MergeShellPlans(result, p.GetShellPlan(srcDir))
}

return result.DevPackages
}

// Return one buildable plan from all planners.
// If no buildable plan is found, return errors from the first unbuildable plan.
func GetBuildPlan(srcDir string, userPkgs []string) (*plansdk.BuildPlan, error) {
buildables := []*plansdk.BuildPlan{}
unbuildables := []*plansdk.BuildPlan{}
for _, p := range getRelevantPlanners(srcDir) {
plan := p.GetBuildPlan(srcDir)
if pkgslice.Contains(userPkgs, plan.DevPackages) {
mutualPkgs := lo.Intersect(userPkgs, plan.DevPackages)
if len(mutualPkgs) > 0 {
if !plan.Invalid() {
buildables = append(buildables, plan)
} else {
Expand Down
9 changes: 4 additions & 5 deletions planner/plansdk/plansdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ type Planner interface {
GetShellPlan(srcDir string) *ShellPlan
}

// MergePlans merges multiple Plans into one. The merged plan's packages, definitions,
// MergeShellPlans merges multiple Plans into one. The merged plan's packages, definitions,
// and overlays is the union of the packages, definitions, and overlays of the input plans,
// respectively. The install/build/start stages of the merged plans are taken from the _first_
// buildable plan (order matters!). If no plan is buildable, returns a non-buildable plan.
// respectively.
func MergeShellPlans(plans ...*ShellPlan) (*ShellPlan, error) {
shellPlan := &ShellPlan{}
for _, p := range plans {
Expand All @@ -91,6 +90,7 @@ func MergeShellPlans(plans ...*ShellPlan) (*ShellPlan, error) {
}
}

shellPlan.DevPackages = pkgslice.Unique(shellPlan.DevPackages)
shellPlan.Definitions = pkgslice.Unique(shellPlan.Definitions)
shellPlan.NixOverlays = pkgslice.Unique(shellPlan.NixOverlays)
shellPlan.ShellInitHook = pkgslice.Unique(shellPlan.ShellInitHook)
Expand Down Expand Up @@ -159,8 +159,7 @@ func MergeUserBuildPlan(userPlan *BuildPlan, automatedPlan *BuildPlan) (*BuildPl
}
// Merging devPackages and runtimePackages fields.
packagesPlan := &BuildPlan{
DevPackages: append([]string{}, userPlan.DevPackages...),
RuntimePackages: []string{},
DevPackages: append([]string{}, userPlan.DevPackages...),
}
err := mergo.Merge(packagesPlan, automatedPlan, mergo.WithAppendSlice)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions planner/plansdk/plansdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
func TestMergeShellPlans(t *testing.T) {
plan1 := &ShellPlan{}
plan2 := &ShellPlan{
DevPackages: []string{},
Definitions: []string{"a"},
NixOverlays: []string{"b"},
ShellInitHook: []string{"a", "b"},
Expand All @@ -36,6 +37,7 @@ func TestMergeShellPlans(t *testing.T) {
ShellInitHook: []string{"a", "b"},
}
expected = &ShellPlan{
DevPackages: []string{},
Definitions: []string{"a"},
NixOverlays: []string{"b", "a"},
ShellInitHook: []string{"c", "a", "b"},
Expand All @@ -59,6 +61,7 @@ func TestMergeShellPlans(t *testing.T) {
},
}
expected = &ShellPlan{
DevPackages: []string{},
Definitions: []string{},
NixOverlays: []string{},
ShellInitHook: []string{},
Expand Down