From 0a98d16dd49fc7cc667dd2631db4860c6238581f Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 1 Jul 2025 16:55:20 -0400 Subject: [PATCH] Added `--ts=dynamic` parameter to `state install` in order to use dynamic imports. --- internal/captain/values.go | 14 ++- internal/runbits/reqop_runbit/update.go | 9 ++ internal/runners/install/install.go | 25 ++++- pkg/buildscript/buildscript.go | 19 ++++ pkg/buildscript/queries.go | 15 +-- pkg/buildscript/unmarshal.go | 2 +- .../api/buildplanner/request/build.go | 60 +++++++++++ .../api/buildplanner/request/evaluate.go | 100 +++++++++++------- pkg/platform/model/buildplanner/build.go | 6 +- pkg/platform/model/buildplanner/commit.go | 4 + pkg/platform/model/buildplanner/evaluate.go | 25 +++++ 11 files changed, 219 insertions(+), 60 deletions(-) create mode 100644 pkg/platform/api/buildplanner/request/build.go create mode 100644 pkg/platform/model/buildplanner/evaluate.go diff --git a/internal/captain/values.go b/internal/captain/values.go index 5f772b13ee..7b14424833 100644 --- a/internal/captain/values.go +++ b/internal/captain/values.go @@ -258,9 +258,10 @@ func (p *PackagesValueNoVersion) Type() string { } type TimeValue struct { - raw string - Time *time.Time - now bool + raw string + Time *time.Time + now bool + dynamic bool } var _ FlagMarshaler = &TimeValue{} @@ -271,7 +272,7 @@ func (u *TimeValue) String() string { func (u *TimeValue) Set(v string) error { u.raw = v - if v != "now" { + if v != "now" && v != "dynamic" { tsv, err := time.Parse(time.RFC3339, v) if err != nil { return locale.WrapInputError(err, "timeflag_format", "Invalid timestamp: Should be RFC3339 formatted.") @@ -279,6 +280,7 @@ func (u *TimeValue) Set(v string) error { u.Time = &tsv } u.now = v == "now" + u.dynamic = v == "dynamic" return nil } @@ -290,6 +292,10 @@ func (u *TimeValue) Now() bool { return u.now } +func (u *TimeValue) Dynamic() bool { + return u.dynamic +} + type IntValue struct { raw string Int *int diff --git a/internal/runbits/reqop_runbit/update.go b/internal/runbits/reqop_runbit/update.go index 395f4797c7..48e5216ba8 100644 --- a/internal/runbits/reqop_runbit/update.go +++ b/internal/runbits/reqop_runbit/update.go @@ -65,6 +65,15 @@ func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit }() pg = output.StartSpinner(out, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) + if script.Dynamic() { + // Evaluate with dynamic imports first. Then commit. + err := bp.Evaluate(pj.Owner(), pj.Name(), script) + if err != nil { + return errs.Wrap(err, "Unable to dynamically evaluate build expression") + } + script.SetDynamic(false) // StageCommit needs to be called with "solve" node and atTime="now" + } + commitParams := buildplanner.StageCommitParams{ Owner: pj.Owner(), Project: pj.Name(), diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 28482abd36..3effddce0b 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -141,7 +141,7 @@ func (i *Install) Run(params Params) (rerr error) { } // Resolve requirements - reqs, err = i.resolveRequirements(params.Packages, ts, languages) + reqs, err = i.resolveRequirements(params.Packages, ts, languages, params.Timestamp.Dynamic()) if err != nil { return errs.Wrap(err, "Unable to resolve requirements") } @@ -153,7 +153,7 @@ func (i *Install) Run(params Params) (rerr error) { // Prepare updated buildscript script := oldCommit.BuildScript() - if err := prepareBuildScript(script, reqs, ts); err != nil { + if err := prepareBuildScript(script, reqs, ts, params.Timestamp.Dynamic()); err != nil { return errs.Wrap(err, "Could not prepare build script") } @@ -181,7 +181,7 @@ type errNoMatches struct { } // resolveRequirements will attempt to resolve the ingredient and namespace for each requested package -func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Time, languages []model.Language) (requirements, error) { +func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Time, languages []model.Language, dynamic bool) (requirements, error) { failed := []*requirement{} reqs := []*requirement{} for _, pkg := range packages { @@ -191,6 +191,13 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti req.Resolved.Namespace = pkg.Namespace } + // When using dynamic imports, the packages may not yet exist in the inventory, so searching + // for them is fruitless. Just pass them along. + if dynamic { + reqs = append(reqs, req) + continue + } + // Find ingredients that match the pkg query ingredients, err := model.SearchIngredientsStrict(pkg.Namespace, pkg.Name, false, false, &ts, i.prime.Auth()) if err != nil { @@ -274,7 +281,9 @@ func resolveVersion(req *requirement) error { } // Verify that the version provided can be resolved - if versionRe.MatchString(version) { + // Note: if the requirement does not have an ingredient, it is being dynamically imported, so + // we cannot resolve its versions yet. + if versionRe.MatchString(version) && req.Resolved.ingredient != nil { match := false for _, knownVersion := range req.Resolved.ingredient.Versions { if knownVersion.Version == version { @@ -340,8 +349,14 @@ func (i *Install) renderUserFacing(reqs requirements) { i.prime.Output().Notice("") } -func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) error { +func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time, dynamic bool) error { script.SetAtTime(ts, true) + + err := script.SetDynamic(dynamic) + if err != nil { + return errs.Wrap(err, "Unable to update solve function") + } + for _, req := range requirements { requirement := types.Requirement{ Namespace: req.Resolved.Namespace, diff --git a/pkg/buildscript/buildscript.go b/pkg/buildscript/buildscript.go index 9909f80527..9010de454d 100644 --- a/pkg/buildscript/buildscript.go +++ b/pkg/buildscript/buildscript.go @@ -22,6 +22,7 @@ type BuildScript struct { project string atTime *time.Time + dynamic bool } func init() { @@ -77,6 +78,24 @@ func (b *BuildScript) SetAtTime(t time.Time, override bool) { _ = b.atTime } +func (b *BuildScript) Dynamic() bool { + return b.dynamic +} + +func (b *BuildScript) SetDynamic(dynamic bool) error { + b.dynamic = dynamic + solveNode, err := b.getSolveNode() + if err != nil { + return errs.Wrap(err, "Unable to find solve node") + } + if dynamic { + solveNode.FuncCall.Name = solveDynamicFuncName + } else { + solveNode.FuncCall.Name = solveFuncName + } + return nil +} + func (b *BuildScript) Equals(other *BuildScript) (bool, error) { b2, err := b.Clone() if err != nil { diff --git a/pkg/buildscript/queries.go b/pkg/buildscript/queries.go index eec73943d0..723941fe2f 100644 --- a/pkg/buildscript/queries.go +++ b/pkg/buildscript/queries.go @@ -12,12 +12,13 @@ import ( ) const ( - solveFuncName = "solve" - solveLegacyFuncName = "solve_legacy" - srcKey = "src" - mergeKey = "merge" - requirementsKey = "requirements" - platformsKey = "platforms" + solveFuncName = "solve" + solveLegacyFuncName = "solve_legacy" + solveDynamicFuncName = "dynamic_solve" + srcKey = "src" + mergeKey = "merge" + requirementsKey = "requirements" + platformsKey = "platforms" ) var errNodeNotFound = errs.New("Could not find node") @@ -177,7 +178,7 @@ func getVersionRequirements(v *value) []types.VersionRequirement { } func isSolveFuncName(name string) bool { - return name == solveFuncName || name == solveLegacyFuncName + return name == solveFuncName || name == solveLegacyFuncName || name == solveDynamicFuncName } func (b *BuildScript) getTargetSolveNode(targets ...string) (*value, error) { diff --git a/pkg/buildscript/unmarshal.go b/pkg/buildscript/unmarshal.go index 5382e135e5..52eb7deb92 100644 --- a/pkg/buildscript/unmarshal.go +++ b/pkg/buildscript/unmarshal.go @@ -83,5 +83,5 @@ func Unmarshal(data []byte) (*BuildScript, error) { atTime = &atTimeVal } - return &BuildScript{raw, project, atTime}, nil + return &BuildScript{raw: raw, project: project, atTime: atTime}, nil } diff --git a/pkg/platform/api/buildplanner/request/build.go b/pkg/platform/api/buildplanner/request/build.go new file mode 100644 index 0000000000..002bf07da9 --- /dev/null +++ b/pkg/platform/api/buildplanner/request/build.go @@ -0,0 +1,60 @@ +package request + +func Build(owner, project, commitId, target string) *build { + return &build{map[string]interface{}{ + "organization": owner, + "project": project, + "commitId": commitId, + "target": target, + }} +} + +type build struct { + vars map[string]interface{} +} + +func (b *build) Query() string { + return ` +mutation ($organization: String!, $project: String!, $commitId: String!, $target: String) { + buildCommitTarget( + input: {organization: $organization, project: $project, commitId: $commitId, target: $target} + ) { + ... on Build { + __typename + status + } + ... on Error { + __typename + message + } + ... on ErrorWithSubErrors { + __typename + subErrors { + __typename + ... on GenericSolveError { + message + isTransient + validationErrors { + error + jsonPath + } + } + ... on RemediableSolveError { + message + isTransient + errorType + validationErrors { + error + jsonPath + } + } + } + } + } +} +` +} + +func (b *build) Vars() (map[string]interface{}, error) { + return b.vars, nil +} diff --git a/pkg/platform/api/buildplanner/request/evaluate.go b/pkg/platform/api/buildplanner/request/evaluate.go index 1c1b3be910..d97a83b45e 100644 --- a/pkg/platform/api/buildplanner/request/evaluate.go +++ b/pkg/platform/api/buildplanner/request/evaluate.go @@ -1,12 +1,30 @@ package request -func Evaluate(owner, project, commitId, target string) *evaluate { - return &evaluate{map[string]interface{}{ - "organization": owner, +import ( + "time" + + "github.com/ActiveState/cli/internal/rtutils/ptr" +) + +func Evaluate(organization, project string, expr []byte, atTime *time.Time, dynamic bool, target string) *evaluate { + eval := &evaluate{map[string]interface{}{ + "organization": organization, "project": project, - "commitId": commitId, + "expr": string(expr), "target": target, }} + + var timestamp *string + if atTime != nil { + timestamp = ptr.To(atTime.Format(time.RFC3339)) + } + if !dynamic { + eval.vars["atTime"] = timestamp + } else { + eval.vars["atTime"] = "dynamic" + } + + return eval } type evaluate struct { @@ -15,42 +33,44 @@ type evaluate struct { func (b *evaluate) Query() string { return ` -mutation ($organization: String!, $project: String!, $commitId: String!, $target: String) { - buildCommitTarget( - input: {organization: $organization, project: $project, commitId: $commitId, target: $target} - ) { - ... on Build { - __typename - status - } - ... on Error { - __typename - message - } - ... on ErrorWithSubErrors { - __typename - subErrors { - __typename - ... on GenericSolveError { - message - isTransient - validationErrors { - error - jsonPath - } - } - ... on RemediableSolveError { - message - isTransient - errorType - validationErrors { - error - jsonPath - } - } - } - } - } +query ($organization: String!, $project: String!, $expr: BuildExpr!, $atTime: AtTime, $target: String) { + project(organization: $organization, project: $project) { + ... on Project { + evaluate(expr: $expr, atTime: $atTime, target: $target) { + ... on Build { + __typename + status + } + ... on Error { + __typename + message + } + ... on ErrorWithSubErrors { + __typename + subErrors { + __typename + ... on GenericSolveError { + message + isTransient + validationErrors { + error + jsonPath + } + } + ... on RemediableSolveError { + message + isTransient + errorType + validationErrors { + error + jsonPath + } + } + } + } + } + } + } } ` } diff --git a/pkg/platform/model/buildplanner/build.go b/pkg/platform/model/buildplanner/build.go index 9abd93156d..36b0e5b1c5 100644 --- a/pkg/platform/model/buildplanner/build.go +++ b/pkg/platform/model/buildplanner/build.go @@ -273,9 +273,9 @@ func (e ErrFailedArtifacts) Error() string { func (bp *BuildPlanner) BuildTarget(owner, project, commitID, target string) error { logging.Debug("BuildTarget, owner: %s, project: %s, commitID: %s, target: %s", owner, project, commitID, target) resp := &response.BuildResponse{} - err := bp.client.Run(request.Evaluate(owner, project, commitID, target), resp) + err := bp.client.Run(request.Build(owner, project, commitID, target), resp) if err != nil { - return processBuildPlannerError(err, "Failed to evaluate target") + return processBuildPlannerError(err, "Failed to build target") } if resp == nil { @@ -283,7 +283,7 @@ func (bp *BuildPlanner) BuildTarget(owner, project, commitID, target string) err } if response.IsErrorResponse(resp.Type) { - return response.ProcessBuildError(resp, "Could not process error response from evaluate target") + return response.ProcessBuildError(resp, "Could not process error response from build target") } return nil diff --git a/pkg/platform/model/buildplanner/commit.go b/pkg/platform/model/buildplanner/commit.go index e6a9ad1b10..9f88ad88d2 100644 --- a/pkg/platform/model/buildplanner/commit.go +++ b/pkg/platform/model/buildplanner/commit.go @@ -38,6 +38,10 @@ func (b *BuildPlanner) StageCommit(params StageCommitParams) (*Commit, error) { return nil, errs.New("Script is nil") } + if script.Dynamic() { + return nil, errs.New("Script cannot be a dynamic_solve") // forgot to call script.SetDynamic(false) earlier + } + expression, err := script.MarshalBuildExpression() if err != nil { return nil, errs.Wrap(err, "Failed to marshal build expression") diff --git a/pkg/platform/model/buildplanner/evaluate.go b/pkg/platform/model/buildplanner/evaluate.go new file mode 100644 index 0000000000..9fc8a7d03a --- /dev/null +++ b/pkg/platform/model/buildplanner/evaluate.go @@ -0,0 +1,25 @@ +package buildplanner + +import ( + "github.com/ActiveState/cli/internal/errs" + + "github.com/ActiveState/cli/pkg/buildscript" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/request" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" +) + +func (bp *BuildPlanner) Evaluate(org, project string, script *buildscript.BuildScript) error { + expression, err := script.MarshalBuildExpression() + if err != nil { + return errs.Wrap(err, "Failed to marshal build expression") + } + + request := request.Evaluate(org, project, expression, script.AtTime(), script.Dynamic(), "") + resp := &response.BuildResponse{} + err = bp.client.Run(request, resp) + if err != nil { + return processBuildPlannerError(err, "Failed to evaluate build expression") + } + + return nil +}