Skip to content

Commit

Permalink
Merge pull request #3544 from ActiveState/mitchell/dx-2960-2
Browse files Browse the repository at this point in the history
Build scripts can distinguish between solve nodes for different targets.
  • Loading branch information
mitchell-as authored Oct 17, 2024
2 parents f4cc3c9 + 9aa7b31 commit 5ec6a87
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 26 deletions.
110 changes: 96 additions & 14 deletions pkg/buildscript/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ActiveState/cli/internal/logging"
"github.com/go-openapi/strfmt"
"github.com/thoas/go-funk"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
Expand All @@ -13,6 +14,8 @@ import (
const (
solveFuncName = "solve"
solveLegacyFuncName = "solve_legacy"
srcKey = "src"
mergeKey = "merge"
requirementsKey = "requirements"
platformsKey = "platforms"
)
Expand Down Expand Up @@ -44,8 +47,10 @@ type UnknownRequirement struct {

func (r UnknownRequirement) IsRequirement() {}

func (b *BuildScript) Requirements() ([]Requirement, error) {
requirementsNode, err := b.getRequirementsNode()
// Returns the requirements for the given target.
// If no target is given, uses the default target (i.e. the name assigned to 'main').
func (b *BuildScript) Requirements(targets ...string) ([]Requirement, error) {
requirementsNode, err := b.getRequirementsNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get requirements node")
}
Expand Down Expand Up @@ -119,8 +124,8 @@ func parseRequirement(req *value) Requirement {
// DependencyRequirements is identical to Requirements except that it only considers dependency type requirements,
// which are the most common.
// ONLY use this when you know you only need to care about dependencies.
func (b *BuildScript) DependencyRequirements() ([]types.Requirement, error) {
reqs, err := b.Requirements()
func (b *BuildScript) DependencyRequirements(targets ...string) ([]types.Requirement, error) {
reqs, err := b.Requirements(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get requirements")
}
Expand All @@ -133,8 +138,8 @@ func (b *BuildScript) DependencyRequirements() ([]types.Requirement, error) {
return deps, nil
}

func (b *BuildScript) getRequirementsNode() (*value, error) {
node, err := b.getSolveNode()
func (b *BuildScript) getRequirementsNode(targets ...string) (*value, error) {
node, err := b.getSolveNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node")
}
Expand Down Expand Up @@ -171,7 +176,23 @@ func getVersionRequirements(v *value) []types.VersionRequirement {
return reqs
}

func (b *BuildScript) getSolveNode() (*value, error) {
func isSolveFuncName(name string) bool {
return name == solveFuncName || name == solveLegacyFuncName
}

func (b *BuildScript) getTargetSolveNode(targets ...string) (*value, error) {
if len(targets) == 0 {
for _, assignment := range b.raw.Assignments {
if assignment.Key != mainKey {
continue
}
if assignment.Value.Ident != nil && *assignment.Value.Ident != "" {
targets = []string{*assignment.Value.Ident}
break
}
}
}

var search func([]*assignment) *value
search = func(assignments []*assignment) *value {
var nextLet []*assignment
Expand All @@ -181,7 +202,13 @@ func (b *BuildScript) getSolveNode() (*value, error) {
continue
}

if f := a.Value.FuncCall; f != nil && (f.Name == solveFuncName || f.Name == solveLegacyFuncName) {
if funk.Contains(targets, a.Key) && a.Value.FuncCall != nil {
return a.Value
}

if f := a.Value.FuncCall; len(targets) == 0 && f != nil && isSolveFuncName(f.Name) {
// This is coming from a complex build expression with no straightforward way to determine
// a default target. Fall back on a top-level solve node.
return a.Value
}
}
Expand All @@ -193,15 +220,70 @@ func (b *BuildScript) getSolveNode() (*value, error) {

return nil
}

if node := search(b.raw.Assignments); node != nil {
return node, nil
}
return nil, errNodeNotFound
}

func (b *BuildScript) getSolveNode(targets ...string) (*value, error) {
node, err := b.getTargetSolveNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get target node")
}

// If the target is the solve function, we're done.
if isSolveFuncName(node.FuncCall.Name) {
return node, nil
}

// If the target is a merge call, then look at right and left branches (in reverse order since the
// right branch has precedence).
if node.FuncCall.Name == mergeKey {
for i := len(node.FuncCall.Arguments) - 1; i >= 0; i-- {
arg := node.FuncCall.Arguments[i]
if arg.Assignment == nil {
continue
}
a := arg.Assignment
if a.Value.Ident != nil {
if node, err := b.getSolveNode(*a.Value.Ident); err == nil {
return node, nil
}
// Note: ignore errors because either branch may not contain a solve node.
// We'll return an error if both branches do not contain a solve node.
}
}
return nil, errNodeNotFound
}

// Otherwise, the "src" key contains a reference to the solve node.
// For example:
//
// runtime = state_tool_artifacts_v1(src = sources)
// sources = solve(at_time = ..., platforms = [...], requirements = [...], ...)
//
// Look over the build expression again for that referenced node.
for _, arg := range node.FuncCall.Arguments {
if arg.Assignment == nil {
continue
}
a := arg.Assignment
if a.Key == srcKey && a.Value.Ident != nil {
node, err := b.getSolveNode(*a.Value.Ident)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node from target")
}
return node, nil
}
}

return nil, errNodeNotFound
}

func (b *BuildScript) getSolveAtTimeValue() (*value, error) {
node, err := b.getSolveNode()
func (b *BuildScript) getSolveAtTimeValue(targets ...string) (*value, error) {
node, err := b.getSolveNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node")
}
Expand All @@ -215,8 +297,8 @@ func (b *BuildScript) getSolveAtTimeValue() (*value, error) {
return nil, errValueNotFound
}

func (b *BuildScript) Platforms() ([]strfmt.UUID, error) {
node, err := b.getPlatformsNode()
func (b *BuildScript) Platforms(targets ...string) ([]strfmt.UUID, error) {
node, err := b.getPlatformsNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get platform node")
}
Expand All @@ -228,8 +310,8 @@ func (b *BuildScript) Platforms() ([]strfmt.UUID, error) {
return list, nil
}

func (b *BuildScript) getPlatformsNode() (*value, error) {
node, err := b.getSolveNode()
func (b *BuildScript) getPlatformsNode(targets ...string) (*value, error) {
node, err := b.getSolveNode(targets...)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node")
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/buildscript/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,15 @@ func Unmarshal(data []byte) (*BuildScript, error) {
break
}

// Verify there are no duplicate key assignments.
// This is primarily to catch duplicate solve nodes for a given target.
seen := make(map[string]bool)
for _, assignment := range raw.Assignments {
if _, exists := seen[assignment.Key]; exists {
return nil, locale.NewInputError(locale.Tl("err_buildscript_duplicate_keys", "Build script has duplicate '{{.V0}}' assignments", assignment.Key))
}
seen[assignment.Key] = true
}

return &BuildScript{raw}, nil
}
19 changes: 7 additions & 12 deletions pkg/buildscript/unmarshal_buildexpression.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,6 @@ func (b *BuildScript) UnmarshalBuildExpression(data []byte) error {
return errs.Wrap(err, "Could not get at_time node")
}

// If the requirements are in legacy object form, e.g.
// requirements = [{"name": "<name>", "namespace": "<name>"}, {...}, ...]
// then transform them into function call form for the AScript format, e.g.
// requirements = [Req(name = "<name>", namespace = "<name>"), Req(...), ...]
requirements, err := b.getRequirementsNode()
if err != nil {
return errs.Wrap(err, "Could not get requirements node")
}
if isLegacyRequirementsList(requirements) {
requirements.List = transformRequirements(requirements).List
}

return nil
}

Expand Down Expand Up @@ -239,6 +227,13 @@ func unmarshalFuncCall(path []string, fc map[string]interface{}) (*funcCall, err
if err != nil {
return nil, errs.Wrap(err, "Could not parse '%s' function's argument '%s': %v", name, key, valueInterface)
}
if key == requirementsKey && isSolveFuncName(name) && isLegacyRequirementsList(uv) {
// If the requirements are in legacy object form, e.g.
// requirements = [{"name": "<name>", "namespace": "<name>"}, {...}, ...]
// then transform them into function call form for the AScript format, e.g.
// requirements = [Req(name = "<name>", namespace = "<name>"), Req(...), ...]
uv.List = transformRequirements(uv).List
}
args = append(args, &value{Assignment: &assignment{key, uv}})
}
sort.SliceStable(args, func(i, j int) bool { return args[i].Assignment.Key < args[j].Assignment.Key })
Expand Down

0 comments on commit 5ec6a87

Please sign in to comment.