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

Build scripts can distinguish between solve nodes for different targets. #3544

Merged
merged 2 commits into from
Oct 17, 2024
Merged
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
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
Loading