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. #3531

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
89 changes: 75 additions & 14 deletions pkg/buildscript/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"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 @@ -12,6 +13,7 @@ import (
const (
solveFuncName = "solve"
solveLegacyFuncName = "solve_legacy"
srcKey = "src"
requirementsKey = "requirements"
platformsKey = "platforms"
)
Expand Down Expand Up @@ -43,8 +45,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(target ...string) ([]Requirement, error) {
requirementsNode, err := b.getRequirementsNode(target...)
if err != nil {
return nil, errs.Wrap(err, "Could not get requirements node")
}
Expand Down Expand Up @@ -95,8 +99,8 @@ func (b *BuildScript) Requirements() ([]Requirement, error) {
// 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(target ...string) ([]types.Requirement, error) {
reqs, err := b.Requirements(target...)
if err != nil {
return nil, errs.Wrap(err, "Could not get requirements")
}
Expand All @@ -109,8 +113,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(target ...string) (*Value, error) {
node, err := b.getSolveNode(target...)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node")
}
Expand Down Expand Up @@ -147,7 +151,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(target ...string) (*Value, error) {
if len(target) == 0 {
for _, assignment := range b.raw.Assignments {
if assignment.Key != mainKey {
continue
}
if assignment.Value.Ident != nil && *assignment.Value.Ident != "" {
target = []string{*assignment.Value.Ident}
break
}
}
}

var search func([]*Assignment) *Value
search = func(assignments []*Assignment) *Value {
var nextLet []*Assignment
Expand All @@ -157,7 +177,13 @@ func (b *BuildScript) getSolveNode() (*Value, error) {
continue
}

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

if f := a.Value.FuncCall; len(target) == 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

return a.Value
}
}
Expand All @@ -169,15 +195,50 @@ 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(target ...string) (*Value, error) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: This should probably be targets

node, err := b.getTargetSolveNode(target...)
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
}

// 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(target ...string) (*Value, error) {
node, err := b.getSolveNode(target...)
if err != nil {
return nil, errs.Wrap(err, "Could not get solve node")
}
Expand All @@ -191,8 +252,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(target ...string) ([]strfmt.UUID, error) {
node, err := b.getPlatformsNode(target...)
if err != nil {
return nil, errs.Wrap(err, "Could not get platform node")
}
Expand All @@ -204,8 +265,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(target ...string) (*Value, error) {
node, err := b.getSolveNode(target...)
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
}
Comment on lines +55 to +61
Copy link
Member

Choose a reason for hiding this comment

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

Does this still allow multiple solve nodes for different targets? The ACs in the story don't stipulate that it has to be multiple solve nodes for a single target that raise an error. Maybe the story needs to be updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you can have more than one solve node in a buildscript. There's only an issue if you have a buildscript whose target has two different solve nodes. For example:

runtime = state_tool_artifacts_v1(src = sources)
sources = solve(...)
sources = solve(... something different...)
another_solve = solve(...)

This error would identify sources has having duplicate assignments. another_solve is totally fine, as it's a different target or whatever.


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 @@ -103,18 +103,6 @@ func UnmarshalBuildExpression(data []byte, atTime *time.Time) (*BuildScript, err
script.raw.AtTime = atTime
}

// 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 := script.getRequirementsNode()
if err != nil {
return nil, errs.Wrap(err, "Could not get requirements node")
}
if isLegacyRequirementsList(requirements) {
requirements.List = transformRequirements(requirements).List
}

return script, nil
}

Expand Down Expand Up @@ -295,6 +283,13 @@ func unmarshalFuncCall(path []string, m map[string]interface{}) (*FuncCall, erro
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(value) {
// 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(...), ...]
value.List = transformRequirements(value).List
}
args = append(args, &Value{Assignment: &Assignment{key, value}})
}
sort.SliceStable(args, func(i, j int) bool { return args[i].Assignment.Key < args[j].Assignment.Key })
Expand Down
Loading