From d859109b062b8bfb2e9dd530b6920adc339d7f2b Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 5 Sep 2024 13:39:07 -0700 Subject: [PATCH 01/49] Implemented package install runner that is self contained (not using requirement op runbit) --- cmd/state/internal/cmdtree/bundles.go | 7 +- cmd/state/internal/cmdtree/packages.go | 15 +- internal/locale/locales/en-us.yaml | 10 +- internal/runbits/cves/cves.go | 1 + internal/runbits/runtime/trigger/trigger.go | 1 + internal/runners/install/install.go | 383 ++++++++++++++++++++ internal/runners/install/rationalize.go | 99 +++++ internal/runners/packages/info.go | 2 +- internal/runners/packages/install.go | 61 ---- internal/runners/packages/search.go | 2 +- internal/runners/packages/uninstall.go | 2 +- pkg/buildscript/mutations.go | 18 +- pkg/platform/model/vcs.go | 14 +- test/integration/install_int_test.go | 4 +- 14 files changed, 529 insertions(+), 90 deletions(-) create mode 100644 internal/runners/install/install.go create mode 100644 internal/runners/install/rationalize.go delete mode 100644 internal/runners/packages/install.go diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index fcea913183..73e61d5175 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -4,6 +4,7 @@ import ( "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runners/install" "github.com/ActiveState/cli/internal/runners/packages" "github.com/ActiveState/cli/pkg/platform/model" ) @@ -43,9 +44,9 @@ func newBundlesCommand(prime *primer.Values) *captain.Command { } func newBundleInstallCommand(prime *primer.Values) *captain.Command { - runner := packages.NewInstall(prime) + runner := install.NewInstall(prime) - params := packages.InstallRunParams{} + params := install.InstallRunParams{} return captain.NewCommand( "install", @@ -62,7 +63,7 @@ func newBundleInstallCommand(prime *primer.Values) *captain.Command { }, }, func(_ *captain.Command, _ []string) error { - return runner.Run(params, model.NamespaceBundle) + return runner.Run(params) }, ).SetSupportsStructuredOutput() } diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index 39b66a1cb0..3c314c7311 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -4,6 +4,7 @@ import ( "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runners/install" "github.com/ActiveState/cli/internal/runners/packages" "github.com/ActiveState/cli/pkg/platform/model" ) @@ -49,9 +50,9 @@ func newPackagesCommand(prime *primer.Values) *captain.Command { } func newInstallCommand(prime *primer.Values) *captain.Command { - runner := packages.NewInstall(prime) + runner := install.NewInstall(prime) - params := packages.InstallRunParams{} + params := install.InstallRunParams{} var packagesRaw string cmd := captain.NewCommand( @@ -65,12 +66,6 @@ func newInstallCommand(prime *primer.Values) *captain.Command { Description: locale.T("package_flag_ts_description"), Value: ¶ms.Timestamp, }, - { - Name: "revision", - Shorthand: "r", - Description: locale.T("package_flag_rev_description"), - Value: ¶ms.Revision, - }, }, []*captain.Argument{ { @@ -83,10 +78,10 @@ func newInstallCommand(prime *primer.Values) *captain.Command { func(_ *captain.Command, args []string) error { for _, p := range args { if err := params.Packages.Set(p); err != nil { - return locale.WrapInputError(err, "err_install_packages_args", "Invalid package install arguments") + return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments") } } - return runner.Run(params, model.NamespacePackage) + return runner.Run(params) }, ) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 62aa856235..4ddf73edc2 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -604,6 +604,8 @@ package_search_flag_exact-term_description: other: Ensures that search results match search term exactly package_import_flag_filename_description: other: The file to import +commit_message_added: + other: "Added: {{.V0}}" commit_message_added_package: other: "Added package: {{.V0}}@{{.V1}}" commit_message_removed_package: @@ -1155,13 +1157,13 @@ package_ingredient_alternatives_nosuggest: No results found for search term "[NOTICE]{{.V0}}[/RESET]". Run "[ACTIONABLE]state search {{.V0}}[/RESET]" to find alternatives. -package_ingredient_alternatives_nolang: +package_requirements_no_match: other: | - No results found for search term "[NOTICE]{{.V0}}[/RESET]". + No results found for following packages: {{.V0}}. - This may be because you have not installed a language for your project. Install a language by running "[ACTIONABLE]state languages install [/RESET]". + Run "[ACTIONABLE]state search {{.V0}}[/RESET]" to find alternatives. progress_search: - other: "• Searching for [ACTIONABLE]{{.V0}}[/RESET] in the ActiveState Catalog" + other: "• Searching for packages in the ActiveState Catalog" progress_cve_search: other: "• Checking for vulnerabilities (CVEs) on [ACTIONABLE]{{.V0}}[/RESET] and its dependencies" setup_runtime: diff --git a/internal/runbits/cves/cves.go b/internal/runbits/cves/cves.go index 53276b13de..d1965dee9c 100644 --- a/internal/runbits/cves/cves.go +++ b/internal/runbits/cves/cves.go @@ -196,6 +196,7 @@ func (c *CveReport) promptForSecurity() (bool, error) { if err != nil { return false, locale.WrapError(err, "err_pkgop_confirm", "Need a confirmation.") } + c.prime.Output().Notice("") // Empty line return confirm, nil } diff --git a/internal/runbits/runtime/trigger/trigger.go b/internal/runbits/runtime/trigger/trigger.go index b7189a552a..a64f2aa1a5 100644 --- a/internal/runbits/runtime/trigger/trigger.go +++ b/internal/runbits/runtime/trigger/trigger.go @@ -29,6 +29,7 @@ const ( TriggerShell Trigger = "shell" TriggerCheckout Trigger = "checkout" TriggerUse Trigger = "use" + TriggerInstall Trigger = "install" ) func NewExecTrigger(cmd string) Trigger { diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go new file mode 100644 index 0000000000..8b574b6099 --- /dev/null +++ b/internal/runners/install/install.go @@ -0,0 +1,383 @@ +package install + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/ActiveState/cli/internal/captain" + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/rtutils/ptr" + buildscript_runbit "github.com/ActiveState/cli/internal/runbits/buildscript" + "github.com/ActiveState/cli/internal/runbits/commits_runbit" + "github.com/ActiveState/cli/internal/runbits/cves" + "github.com/ActiveState/cli/internal/runbits/dependencies" + "github.com/ActiveState/cli/internal/runbits/rationalize" + runtime_runbit "github.com/ActiveState/cli/internal/runbits/runtime" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" + "github.com/ActiveState/cli/internal/sliceutils" + "github.com/ActiveState/cli/pkg/buildscript" + "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/pkg/platform/model" + bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" + "github.com/ActiveState/cli/pkg/runtime" + "github.com/go-openapi/strfmt" +) + +type primeable interface { + primer.Outputer + primer.Prompter + primer.Projecter + primer.Auther + primer.Configurer + primer.Analyticer + primer.SvcModeler +} + +// InstallRunParams tracks the info required for running Install. +type InstallRunParams struct { + Packages captain.PackagesValue + Timestamp captain.TimeValue +} + +type requirement struct { + input *captain.PackageValue + resolvedVersionReq []types.VersionRequirement + resolvedNamespace *model.Namespace + matchedIngredients []*model.IngredientAndVersion +} + +type requirements []*requirement + +func (r requirements) String() string { + result := []string{} + for _, req := range r { + if req.resolvedNamespace != nil { + result = append(result, fmt.Sprintf("%s/%s", req.resolvedNamespace.String(), req.input.Name)) + } else { + result = append(result, req.input.Name) + } + } + return strings.Join(result, ", ") +} + +// Install manages the installing execution context. +type Install struct { + prime primeable +} + +// NewInstall prepares an installation execution context for use. +func NewInstall(prime primeable) *Install { + return &Install{prime} +} + +// Run executes the install behavior. +func (i *Install) Run(params InstallRunParams) (rerr error) { + defer rationalizeError(i.prime.Auth(), &rerr) + + logging.Debug("ExecuteInstall") + + pj := i.prime.Project() + out := i.prime.Output() + bp := bpModel.NewBuildPlannerModel(i.prime.Auth()) + + // Verify input + if pj == nil { + return rationalize.ErrNoProject + } + if pj.IsHeadless() { + return rationalize.ErrHeadless + } + + out.Notice(locale.Tr("operating_message", pj.NamespaceString(), pj.Dir())) + + var pg *output.Spinner + defer func() { + if pg != nil { + // This is a bit awkward, but it would be even more awkward to manually address this for every error condition + pg.Stop(locale.T("progress_fail")) + } + }() + + // Start process of resolving requirements + var err error + var oldCommit *bpModel.Commit + var reqs requirements + var ts time.Time + { + pg = output.StartSpinner(out, locale.T("progress_search"), constants.TerminalAnimationInterval) + + // Resolve timestamp, commit and languages used for current project. + // This will be used to resolve the requirements. + ts, err = commits_runbit.ExpandTimeForProject(¶ms.Timestamp, i.prime.Auth(), i.prime.Project()) + if err != nil { + return errs.Wrap(err, "Unable to get timestamp from params") + } + + // Grab local commit info + localCommitID, err := localcommit.Get(i.prime.Project().Dir()) + if err != nil { + return errs.Wrap(err, "Unable to get local commit") + } + oldCommit, err = bp.FetchCommit(localCommitID, pj.Owner(), pj.Name(), nil) + if err != nil { + return errs.Wrap(err, "Failed to fetch old build result") + } + + // Get languages used in current project + languages, err := model.FetchLanguagesForCommit(localCommitID, i.prime.Auth()) + if err != nil { + logging.Debug("Could not get language from project: %v", err) + } + + // Resolve requirements + reqs, err = i.resolveRequirements(params.Packages, ts, languages) + if err != nil { + return errs.Wrap(err, "Unable to resolve requirements") + } + + // Done resolving requirements + pg.Stop(locale.T("progress_found")) + } + + // Start process of creating the commit, which also solves it + var newCommit *bpModel.Commit + { + pg = output.StartSpinner(out, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) + + script, err := i.prepareBuildScript(oldCommit.BuildScript(), reqs, ts) + if err != nil { + return errs.Wrap(err, "Could not prepare build script") + } + + bsv, _ := script.Marshal() + logging.Debug("Buildscript: %s", string(bsv)) + + commitParams := bpModel.StageCommitParams{ + Owner: pj.Owner(), + Project: pj.Name(), + ParentCommit: string(oldCommit.CommitID), + Description: locale.Tr("commit_message_added", reqs.String()), + Script: script, + } + + // Solve runtime + newCommit, err = bp.StageCommit(commitParams) + if err != nil { + return errs.Wrap(err, "Could not stage commit") + } + + // Stop process of creating the commit + pg.Stop(locale.T("progress_success")) + pg = nil + } + + // Report changes and CVEs to user + { + dependencies.OutputChangeSummary(out, newCommit.BuildPlan(), oldCommit.BuildPlan()) + if err := cves.NewCveReport(i.prime).Report(newCommit.BuildPlan(), oldCommit.BuildPlan()); err != nil { + return errs.Wrap(err, "Could not report CVEs") + } + } + + // Start runtime sourcing UI + if !i.prime.Config().GetBool(constants.AsyncRuntimeConfig) { + // refresh or install runtime + _, err := runtime_runbit.Update(i.prime, trigger.TriggerInstall, + runtime_runbit.WithCommit(newCommit), + runtime_runbit.WithoutBuildscriptValidation(), + ) + if err != nil { + if !IsBuildError(err) { + // If the error is not a build error we still want to update the commit + if err2 := i.updateCommitID(newCommit.CommitID); err2 != nil { + return errs.Pack(err, locale.WrapError(err2, "err_package_update_commit_id")) + } + } + return errs.Wrap(err, "Failed to refresh runtime") + } + } + + // Update commit ID + if err := i.updateCommitID(newCommit.CommitID); err != nil { + return locale.WrapError(err, "err_package_update_commit_id") + } + + // All done + out.Notice(locale.T("operation_success_local")) + + return nil +} + +type errNoMatches struct { + error + requirements []*requirement +} + +// 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) { + var disambiguate []*requirement + var failed []*requirement + reqs := []*requirement{} + for _, pkg := range packages { + req := &requirement{input: &pkg} + if pkg.Namespace != "" { + req.resolvedNamespace = ptr.To(model.NewNamespaceRaw(pkg.Namespace)) + } + + // Find ingredients that match the pkg query + ingredients, err := model.SearchIngredientsStrict(pkg.Namespace, pkg.Name, false, false, &ts, i.prime.Auth()) + if err != nil { + return nil, locale.WrapError(err, "err_pkgop_search_err", "Failed to check for ingredients.") + } + + // Resolve matched ingredients + if pkg.Namespace == "" { + // Filter out ingredients that don't target one of the supported languages + ingredients = sliceutils.Filter(ingredients, func(i *model.IngredientAndVersion) bool { + il := model.LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) + for _, l := range languages { + if l.Name == il { + return true + } + } + return false + }) + } + req.matchedIngredients = ingredients + + // Validate that the ingredient is resolved, and prompt the user if multiple ingredients matched + if req.resolvedNamespace == nil { + len := len(ingredients) + switch { + case len == 1: + req.resolvedNamespace = ptr.To(model.ParseNamespace(*ingredients[0].Ingredient.PrimaryNamespace)) + case len > 1: + disambiguate = append(disambiguate, req) + case len == 0: + failed = append(failed, req) + } + } + + reqs = append(reqs, req) + } + + // Fail if not all requirements could be resolved + if len(failed) > 0 { + return nil, errNoMatches{error: errs.New("Failed to resolve requirements"), requirements: failed} + } + + // Disambiguate requirements that match multiple ingredients + if len(disambiguate) > 0 { + for _, req := range disambiguate { + ingredient, err := i.promptForMatchingIngredient(req) + if err != nil { + return nil, errs.Wrap(err, "Prompting for namespace failed") + } + req.matchedIngredients = []*model.IngredientAndVersion{ingredient} + req.resolvedNamespace = ptr.To(model.ParseNamespace(*ingredient.Ingredient.PrimaryNamespace)) + } + } + + // Now that we have the ingredient resolved we can also resolve the version requirement + for _, req := range reqs { + version := req.input.Version + if req.input.Version == "" { + continue + } + if _, err := strconv.Atoi(version); err == nil { + // If the version number provided is a straight up integer (no dots or dashes) then assume it's a wildcard + version = fmt.Sprintf("%d.x", version) + } + var err error + req.resolvedVersionReq, err = bpModel.VersionStringToRequirements(version) + if err != nil { + return nil, errs.Wrap(err, "Could not process version string into requirements") + } + } + + return reqs, nil +} + +func (i *Install) promptForMatchingIngredient(req *requirement) (*model.IngredientAndVersion, error) { + if len(req.matchedIngredients) <= 1 { + return nil, errs.New("promptForNamespace should never be called if there are no multiple ingredient matches") + } + + choices := []string{} + values := map[string]*model.IngredientAndVersion{} + for _, i := range req.matchedIngredients { + // Generate ingredient choices to present to the user + name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, i.Ingredient.PrimaryNamespace) + choices = append(choices, name) + values[name] = i + } + + // Prompt the user with the ingredient choices + choice, err := i.prime.Prompt().Select( + locale.Tl("prompt_pkgop_ingredient", "Multiple Matches"), + locale.Tl("prompt_pkgop_ingredient_msg", "Your query has multiple matches. Which one would you like to use?"), + choices, &choices[0], + ) + if err != nil { + return nil, errs.Wrap(err, "prompting failed") + } + + // Return the user selected ingredient + return values[choice], nil +} + +func (i *Install) prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) (*buildscript.BuildScript, error) { + script.SetAtTime(ts) + for _, req := range requirements { + requirement := types.Requirement{ + Namespace: req.resolvedNamespace.String(), + Name: req.input.Name, + VersionRequirement: req.resolvedVersionReq, + } + + err := script.AddRequirement(requirement) + if err != nil { + return nil, errs.Wrap(err, "Failed to update build expression with requirement") + } + } + + return script, nil +} + +func (i *Install) updateCommitID(commitID strfmt.UUID) error { + if err := localcommit.Set(i.prime.Project().Dir(), commitID.String()); err != nil { + return locale.WrapError(err, "err_package_update_commit_id") + } + + if i.prime.Config().GetBool(constants.OptinBuildscriptsConfig) { + bp := bpModel.NewBuildPlannerModel(i.prime.Auth()) + script, err := bp.GetBuildScript(commitID.String()) + if err != nil { + return errs.Wrap(err, "Could not get remote build expr and time") + } + + err = buildscript_runbit.Update(i.prime.Project(), script) + if err != nil { + return locale.WrapError(err, "err_update_build_script") + } + } + + return nil +} + +func IsBuildError(err error) bool { + var errBuild *runtime.BuildError + var errBuildPlanner *response.BuildPlannerError + + return errors.As(err, &errBuild) || errors.As(err, &errBuildPlanner) +} diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go new file mode 100644 index 0000000000..ac69eed7bf --- /dev/null +++ b/internal/runners/install/rationalize.go @@ -0,0 +1,99 @@ +package install + +import ( + "errors" + "fmt" + "strings" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/multilog" + bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/platform/model" +) + +func rationalizeError(auth *authentication.Auth, rerr *error) { + var commitError *bpResp.CommitError + var noMatchErr errNoMatches + + switch { + case rerr == nil: + return + + // No matches found + case errors.As(*rerr, &noMatchErr): + names := []string{} + for _, r := range noMatchErr.requirements { + names = append(names, fmt.Sprintf(`"[[ACTIONABLE]%s[/RESET]"`, r.input.Name)) + } + if len(noMatchErr.requirements) > 1 { + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_requirements_no_match", strings.Join(names, ", "))) + return + } + suggestions, err := getSuggestions(noMatchErr.requirements[0], auth) + if err != nil { + multilog.Error("Failed to retrieve suggestions: %v", err) + } + + if len(suggestions) == 0 { + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives_nosuggest", strings.Join(names, ", "))) + return + } + + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "))) + + // Error staging a commit during install. + case errors.As(*rerr, &commitError): + switch commitError.Type { + case types.NotFoundErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_not_found", "Could not make runtime changes because your project was not found."), + errs.SetInput(), + errs.SetTips(locale.T("tip_private_project_auth")), + ) + case types.ForbiddenErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_forbidden", "Could not make runtime changes because you do not have permission to do so."), + errs.SetInput(), + errs.SetTips(locale.T("tip_private_project_auth")), + ) + case types.HeadOnBranchMovedErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.T("err_buildplanner_head_on_branch_moved"), + errs.SetInput(), + ) + case types.NoChangeSinceLastCommitErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_exist", "The requested package(s) is already installed."), + errs.SetInput(), + ) + default: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_buildplanner_error", "Could not make runtime changes due to the following error: {{.V0}}", commitError.Message), + errs.SetInput(), + ) + } + + } +} + +func getSuggestions(req *requirement, auth *authentication.Auth) ([]string, error) { + results, err := model.SearchIngredients(req.input.Namespace, req.input.Name, false, nil, auth) + if err != nil { + return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", req.input.Name) + } + + maxResults := 5 + if len(results) > maxResults { + results = results[:maxResults] + } + + suggestions := make([]string, 0, maxResults+1) + for _, result := range results { + suggestions = append(suggestions, fmt.Sprintf(" - %s", *result.Ingredient.Name)) + } + + return suggestions, nil +} diff --git a/internal/runners/packages/info.go b/internal/runners/packages/info.go index fb559b8ecc..2366cae3ff 100644 --- a/internal/runners/packages/info.go +++ b/internal/runners/packages/info.go @@ -52,7 +52,7 @@ func (i *Info) Run(params InfoRunParams, nstype model.NamespaceType) error { var ns *model.Namespace if params.Package.Namespace != "" { - ns = ptr.To(model.NewRawNamespace(params.Package.Namespace)) + ns = ptr.To(model.NewNamespaceRaw(params.Package.Namespace)) } else { nsTypeV = &nstype } diff --git a/internal/runners/packages/install.go b/internal/runners/packages/install.go deleted file mode 100644 index 30193f4cf8..0000000000 --- a/internal/runners/packages/install.go +++ /dev/null @@ -1,61 +0,0 @@ -package packages - -import ( - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/internal/runbits/commits_runbit" - "github.com/ActiveState/cli/internal/runbits/runtime/requirements" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" - "github.com/ActiveState/cli/pkg/platform/model" -) - -// InstallRunParams tracks the info required for running Install. -type InstallRunParams struct { - Packages captain.PackagesValue - Timestamp captain.TimeValue - Revision captain.IntValue -} - -// Install manages the installing execution context. -type Install struct { - prime primeable -} - -// NewInstall prepares an installation execution context for use. -func NewInstall(prime primeable) *Install { - return &Install{prime} -} - -// Run executes the install behavior. -func (a *Install) Run(params InstallRunParams, nsType model.NamespaceType) (rerr error) { - defer rationalizeError(a.prime.Auth(), &rerr) - - logging.Debug("ExecuteInstall") - var reqs []*requirements.Requirement - for _, p := range params.Packages { - req := &requirements.Requirement{ - Name: p.Name, - Version: p.Version, - Operation: types.OperationAdded, - } - - if p.Namespace != "" { - req.Namespace = ptr.To(model.NewRawNamespace(p.Namespace)) - } else { - req.NamespaceType = &nsType - } - - req.Revision = params.Revision.Int - - reqs = append(reqs, req) - } - - ts, err := commits_runbit.ExpandTimeForProject(¶ms.Timestamp, a.prime.Auth(), a.prime.Project()) - if err != nil { - return errs.Wrap(err, "Unable to get timestamp from params") - } - - return requirements.NewRequirementOperation(a.prime).ExecuteRequirementOperation(&ts, reqs...) -} diff --git a/internal/runners/packages/search.go b/internal/runners/packages/search.go index ed6ad2e4f0..6af93400cf 100644 --- a/internal/runners/packages/search.go +++ b/internal/runners/packages/search.go @@ -56,7 +56,7 @@ func (s *Search) Run(params SearchRunParams, nstype model.NamespaceType) error { ns = model.NewNamespacePkgOrBundle(language, nstype) } else { - ns = model.NewRawNamespace(params.Ingredient.Namespace) + ns = model.NewNamespaceRaw(params.Ingredient.Namespace) } ts, err := commits_runbit.ExpandTimeForProject(¶ms.Timestamp, s.auth, s.proj) diff --git a/internal/runners/packages/uninstall.go b/internal/runners/packages/uninstall.go index a9008261dd..6deeccae73 100644 --- a/internal/runners/packages/uninstall.go +++ b/internal/runners/packages/uninstall.go @@ -43,7 +43,7 @@ func (u *Uninstall) Run(params UninstallRunParams, nsType model.NamespaceType) ( } if p.Namespace != "" { - req.Namespace = ptr.To(model.NewRawNamespace(p.Namespace)) + req.Namespace = ptr.To(model.NewNamespaceRaw(p.Namespace)) } else { req.NamespaceType = &nsType } diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index 865c0d36e3..b7fd20282a 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -1,6 +1,8 @@ package buildscript import ( + "errors" + "github.com/go-openapi/strfmt" "github.com/ActiveState/cli/internal/errs" @@ -15,15 +17,15 @@ func (b *BuildScript) UpdateRequirement(operation types.Operation, requirement t var err error switch operation { case types.OperationAdded: - err = b.addRequirement(requirement) + err = b.AddRequirement(requirement) case types.OperationRemoved: - err = b.removeRequirement(requirement) + err = b.RemoveRequirement(requirement) case types.OperationUpdated: - err = b.removeRequirement(requirement) + err = b.RemoveRequirement(requirement) if err != nil { break } - err = b.addRequirement(requirement) + err = b.AddRequirement(requirement) default: return errs.New("Unsupported operation") } @@ -33,7 +35,11 @@ func (b *BuildScript) UpdateRequirement(operation types.Operation, requirement t return nil } -func (b *BuildScript) addRequirement(requirement types.Requirement) error { +func (b *BuildScript) AddRequirement(requirement types.Requirement) error { + if err := b.RemoveRequirement(requirement); err != nil && !errors.As(err, ptr.To(&RequirementNotFoundError{})) { + return errs.Wrap(err, "Could not remove requirement") + } + // Use object form for now, and then transform it into function form later. obj := []*Assignment{ {requirementNameKey, newString(requirement.Name)}, @@ -72,7 +78,7 @@ type RequirementNotFoundError struct { *locale.LocalizedError // for legacy non-user-facing error usages } -func (b *BuildScript) removeRequirement(requirement types.Requirement) error { +func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { requirementsNode, err := b.getRequirementsNode() if err != nil { return errs.Wrap(err, "Could not get requirements node") diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 845b576a95..2c4194f81f 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -169,6 +169,18 @@ func (n Namespace) String() string { return n.value } +func ParseNamespace(ns string) Namespace { + if ns == "" { + return Namespace{NamespaceBlank, ns} + } + for _, n := range []NamespaceType{NamespacePackage, NamespaceBundle, NamespaceLanguage, NamespacePlatform, NamespaceOrg} { + if NamespaceMatch(ns, n.Matchable()) { + return Namespace{n, ns} + } + } + return Namespace{nsType: NamespaceRaw, value: ns} +} + func NewNamespacePkgOrBundle(language string, nstype NamespaceType) Namespace { if nstype == NamespaceBundle { return NewNamespaceBundle(language) @@ -181,7 +193,7 @@ func NewNamespacePackage(language string) Namespace { return Namespace{NamespacePackage, fmt.Sprintf("language/%s", language)} } -func NewRawNamespace(value string) Namespace { +func NewNamespaceRaw(value string) Namespace { return Namespace{NamespaceRaw, value} } diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go index fbe8630850..6865e4a67a 100644 --- a/test/integration/install_int_test.go +++ b/test/integration/install_int_test.go @@ -25,7 +25,7 @@ func (suite *InstallIntegrationTestSuite) TestInstall() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "trender") - cp.Expect("Package added") + cp.Expect("project has been updated") cp.ExpectExitCode(0) } @@ -103,7 +103,7 @@ func (suite *InstallIntegrationTestSuite) TestInstall_Resolved() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "requests") - cp.Expect("Package added") + cp.Expect("project has been updated") cp.ExpectExitCode(0) // Run `state packages` to verify a full package version was resolved. From e5da35e712b39664c60b6b6cd3f54fe26c89ce22 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 6 Sep 2024 13:36:26 -0700 Subject: [PATCH 02/49] Implemented new requirement opts for `state platforms add` --- internal/locale/locales/en-us.yaml | 6 +- internal/runbits/commits_runbit/time.go | 4 +- internal/runbits/rationalizers/commit.go | 46 +++++++ internal/runbits/rationalizers/readme.md | 4 + internal/runbits/reqop_runbit/update.go | 147 +++++++++++++++++++++++ internal/runners/install/install.go | 106 ++-------------- internal/runners/install/rationalize.go | 36 +----- internal/runners/platforms/add.go | 97 ++++++++++++--- internal/runners/platforms/platforms.go | 22 ++-- internal/runners/platforms/remove.go | 6 +- pkg/buildscript/mutations.go | 8 +- pkg/platform/model/checkpoints.go | 9 +- pkg/platform/model/inventory.go | 27 ++++- pkg/platform/model/vcs.go | 4 +- test/integration/platforms_int_test.go | 23 ++++ 15 files changed, 365 insertions(+), 180 deletions(-) create mode 100644 internal/runbits/rationalizers/commit.go create mode 100644 internal/runbits/rationalizers/readme.md create mode 100644 internal/runbits/reqop_runbit/update.go diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 4ddf73edc2..64d81f4a71 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -941,7 +941,7 @@ err_arg_required: err_init_no_language: other: "A project has not been set using 'state use'. You must include the [NOTICE]language[/RESET] argument. For more help, run '[ACTIONABLE]state init --help[/RESET]'." platform_added: - other: "[NOTICE]{{.V0}}@{{.V1}}[/RESET] has been added." + other: "[NOTICE]{{.V0}}[/RESET] has been added." platform_removed: other: "[NOTICE]{{.V0}}@{{.V1}}[/RESET] has been removed." deploy_usable_path: @@ -1164,6 +1164,8 @@ package_requirements_no_match: Run "[ACTIONABLE]state search {{.V0}}[/RESET]" to find alternatives. progress_search: other: "• Searching for packages in the ActiveState Catalog" +progress_platform_search: + other: "• Searching for platform in the ActiveState Catalog" progress_cve_search: other: "• Checking for vulnerabilities (CVEs) on [ACTIONABLE]{{.V0}}[/RESET] and its dependencies" setup_runtime: @@ -1583,3 +1585,5 @@ flag_state_upgrade_expand_description: other: Show individual transitive dependency changes rather than a summary flag_state_upgrade_ts_description: other: Manually specify the timestamp to 'upgrade' to. Can be either 'now' or RFC3339 formatted timestamp. +platform_add_not_found: + other: Could not find a platform matching your criteria diff --git a/internal/runbits/commits_runbit/time.go b/internal/runbits/commits_runbit/time.go index 478cfc544d..ac9afb9717 100644 --- a/internal/runbits/commits_runbit/time.go +++ b/internal/runbits/commits_runbit/time.go @@ -17,11 +17,11 @@ import ( // Otherwise, returns the specified timestamp or nil (which falls back on the default Platform // timestamp for a given operation) func ExpandTime(ts *captain.TimeValue, auth *authentication.Auth) (time.Time, error) { - if ts.Time != nil { + if ts != nil && ts.Time != nil { return *ts.Time, nil } - if ts.Now() { + if ts != nil && ts.Now() { latest, err := model.FetchLatestRevisionTimeStamp(auth) if err != nil { return time.Time{}, errs.Wrap(err, "Unable to determine latest revision time") diff --git a/internal/runbits/rationalizers/commit.go b/internal/runbits/rationalizers/commit.go new file mode 100644 index 0000000000..dfa26018b0 --- /dev/null +++ b/internal/runbits/rationalizers/commit.go @@ -0,0 +1,46 @@ +package rationalizers + +import ( + "errors" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" +) + +func HandleCommitErrors(rerr *error) { + var commitError *bpResp.CommitError + if !errors.As(*rerr, &commitError) { + return + } + switch commitError.Type { + case types.NotFoundErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_not_found", "Could not make runtime changes because your project was not found."), + errs.SetInput(), + errs.SetTips(locale.T("tip_private_project_auth")), + ) + case types.ForbiddenErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_forbidden", "Could not make runtime changes because you do not have permission to do so."), + errs.SetInput(), + errs.SetTips(locale.T("tip_private_project_auth")), + ) + case types.HeadOnBranchMovedErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.T("err_buildplanner_head_on_branch_moved"), + errs.SetInput(), + ) + case types.NoChangeSinceLastCommitErrorType: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_exist", "The requested package is already installed."), + errs.SetInput(), + ) + default: + *rerr = errs.WrapUserFacing(*rerr, + locale.Tl("err_packages_buildplanner_error", "Could not make runtime changes due to the following error: {{.V0}}", commitError.Message), + errs.SetInput(), + ) + } +} diff --git a/internal/runbits/rationalizers/readme.md b/internal/runbits/rationalizers/readme.md new file mode 100644 index 0000000000..aed768812d --- /dev/null +++ b/internal/runbits/rationalizers/readme.md @@ -0,0 +1,4 @@ +The rationalizers package is intended to be used for common error rationalizers that are shared between multiple +runners. + +In MOST cases your rationalizer should be package specific, so don't default to using the rationalizers package. diff --git a/internal/runbits/reqop_runbit/update.go b/internal/runbits/reqop_runbit/update.go new file mode 100644 index 0000000000..447cb85c5b --- /dev/null +++ b/internal/runbits/reqop_runbit/update.go @@ -0,0 +1,147 @@ +package reqop_runbit + +import ( + "errors" + "fmt" + "strings" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runbits/buildscript" + "github.com/ActiveState/cli/internal/runbits/cves" + "github.com/ActiveState/cli/internal/runbits/dependencies" + "github.com/ActiveState/cli/internal/runbits/runtime" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" + "github.com/ActiveState/cli/pkg/buildscript" + "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/pkg/platform/model" + "github.com/ActiveState/cli/pkg/platform/model/buildplanner" + "github.com/ActiveState/cli/pkg/runtime" + "github.com/go-openapi/strfmt" +) + +type primeable interface { + primer.Outputer + primer.Prompter + primer.Projecter + primer.Auther + primer.Configurer + primer.Analyticer + primer.SvcModeler +} + +type requirements []*Requirement + +func (r requirements) String() string { + result := []string{} + for _, req := range r { + result = append(result, fmt.Sprintf("%s/%s", req.Namespace, req.Name)) + } + return strings.Join(result, ", ") +} + +type Requirement struct { + Name string + Namespace model.Namespace + Version []types.VersionRequirement +} + +func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit *buildplanner.Commit, commitMsg string) error { + pj := prime.Project() + out := prime.Output() + cfg := prime.Config() + bp := buildplanner.NewBuildPlannerModel(prime.Auth()) + + var pg *output.Spinner + defer func() { + if pg != nil { + pg.Stop(locale.T("progress_fail")) + } + }() + pg = output.StartSpinner(out, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) + + bsv, _ := script.Marshal() + logging.Debug("Buildscript: %s", string(bsv)) + + commitParams := buildplanner.StageCommitParams{ + Owner: pj.Owner(), + Project: pj.Name(), + ParentCommit: string(oldCommit.CommitID), + Description: commitMsg, + Script: script, + } + + // Solve runtime + newCommit, err := bp.StageCommit(commitParams) + if err != nil { + return errs.Wrap(err, "Could not stage commit") + } + + // Stop process of creating the commit + pg.Stop(locale.T("progress_success")) + pg = nil + + // Report changes and CVEs to user + dependencies.OutputChangeSummary(out, newCommit.BuildPlan(), oldCommit.BuildPlan()) + if err := cves.NewCveReport(prime).Report(newCommit.BuildPlan(), oldCommit.BuildPlan()); err != nil { + return errs.Wrap(err, "Could not report CVEs") + } + + // Start runtime sourcing UI + if !cfg.GetBool(constants.AsyncRuntimeConfig) { + // refresh or install runtime + _, err := runtime_runbit.Update(prime, trigger.TriggerInstall, + runtime_runbit.WithCommit(newCommit), + runtime_runbit.WithoutBuildscriptValidation(), + ) + if err != nil { + if !isBuildError(err) { + // If the error is not a build error we still want to update the commit + if err2 := updateCommitID(prime, newCommit.CommitID); err2 != nil { + return errs.Pack(err, locale.WrapError(err2, "err_package_update_commit_id")) + } + } + return errs.Wrap(err, "Failed to refresh runtime") + } + } + + // Update commit ID + if err := updateCommitID(prime, newCommit.CommitID); err != nil { + return locale.WrapError(err, "err_package_update_commit_id") + } + return nil +} + +func updateCommitID(prime primeable, commitID strfmt.UUID) error { + if err := localcommit.Set(prime.Project().Dir(), commitID.String()); err != nil { + return locale.WrapError(err, "err_package_update_commit_id") + } + + if prime.Config().GetBool(constants.OptinBuildscriptsConfig) { + bp := buildplanner.NewBuildPlannerModel(prime.Auth()) + script, err := bp.GetBuildScript(commitID.String()) + if err != nil { + return errs.Wrap(err, "Could not get remote build expr and time") + } + + err = buildscript_runbit.Update(prime.Project(), script) + if err != nil { + return locale.WrapError(err, "err_update_build_script") + } + } + + return nil +} + +func isBuildError(err error) bool { + var errBuild *runtime.BuildError + var errBuildPlanner *response.BuildPlannerError + + return errors.As(err, &errBuild) || errors.As(err, &errBuildPlanner) +} diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 8b574b6099..d4f84a9560 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -1,7 +1,6 @@ package install import ( - "errors" "fmt" "strconv" "strings" @@ -15,22 +14,15 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/rtutils/ptr" - buildscript_runbit "github.com/ActiveState/cli/internal/runbits/buildscript" "github.com/ActiveState/cli/internal/runbits/commits_runbit" - "github.com/ActiveState/cli/internal/runbits/cves" - "github.com/ActiveState/cli/internal/runbits/dependencies" "github.com/ActiveState/cli/internal/runbits/rationalize" - runtime_runbit "github.com/ActiveState/cli/internal/runbits/runtime" - "github.com/ActiveState/cli/internal/runbits/runtime/trigger" + "github.com/ActiveState/cli/internal/runbits/reqop_runbit" "github.com/ActiveState/cli/internal/sliceutils" "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" "github.com/ActiveState/cli/pkg/platform/model" bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" - "github.com/ActiveState/cli/pkg/runtime" - "github.com/go-openapi/strfmt" ) type primeable interface { @@ -103,7 +95,6 @@ func (i *Install) Run(params InstallRunParams) (rerr error) { var pg *output.Spinner defer func() { if pg != nil { - // This is a bit awkward, but it would be even more awkward to manually address this for every error condition pg.Stop(locale.T("progress_fail")) } }() @@ -147,69 +138,18 @@ func (i *Install) Run(params InstallRunParams) (rerr error) { // Done resolving requirements pg.Stop(locale.T("progress_found")) - } - - // Start process of creating the commit, which also solves it - var newCommit *bpModel.Commit - { - pg = output.StartSpinner(out, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) - - script, err := i.prepareBuildScript(oldCommit.BuildScript(), reqs, ts) - if err != nil { - return errs.Wrap(err, "Could not prepare build script") - } - - bsv, _ := script.Marshal() - logging.Debug("Buildscript: %s", string(bsv)) - - commitParams := bpModel.StageCommitParams{ - Owner: pj.Owner(), - Project: pj.Name(), - ParentCommit: string(oldCommit.CommitID), - Description: locale.Tr("commit_message_added", reqs.String()), - Script: script, - } - - // Solve runtime - newCommit, err = bp.StageCommit(commitParams) - if err != nil { - return errs.Wrap(err, "Could not stage commit") - } - - // Stop process of creating the commit - pg.Stop(locale.T("progress_success")) pg = nil } - // Report changes and CVEs to user - { - dependencies.OutputChangeSummary(out, newCommit.BuildPlan(), oldCommit.BuildPlan()) - if err := cves.NewCveReport(i.prime).Report(newCommit.BuildPlan(), oldCommit.BuildPlan()); err != nil { - return errs.Wrap(err, "Could not report CVEs") - } - } - - // Start runtime sourcing UI - if !i.prime.Config().GetBool(constants.AsyncRuntimeConfig) { - // refresh or install runtime - _, err := runtime_runbit.Update(i.prime, trigger.TriggerInstall, - runtime_runbit.WithCommit(newCommit), - runtime_runbit.WithoutBuildscriptValidation(), - ) - if err != nil { - if !IsBuildError(err) { - // If the error is not a build error we still want to update the commit - if err2 := i.updateCommitID(newCommit.CommitID); err2 != nil { - return errs.Pack(err, locale.WrapError(err2, "err_package_update_commit_id")) - } - } - return errs.Wrap(err, "Failed to refresh runtime") - } + // Prepare updated buildscript + script, err := prepareBuildScript(oldCommit.BuildScript(), reqs, ts) + if err != nil { + return errs.Wrap(err, "Could not prepare build script") } - // Update commit ID - if err := i.updateCommitID(newCommit.CommitID); err != nil { - return locale.WrapError(err, "err_package_update_commit_id") + // Update local checkout and source runtime changes + if err := reqop_runbit.UpdateAndReload(i.prime, script, oldCommit, locale.Tr("commit_message_added", reqs.String())); err != nil { + return errs.Wrap(err, "Failed to update local checkout") } // All done @@ -336,7 +276,7 @@ func (i *Install) promptForMatchingIngredient(req *requirement) (*model.Ingredie return values[choice], nil } -func (i *Install) prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) (*buildscript.BuildScript, error) { +func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) (*buildscript.BuildScript, error) { script.SetAtTime(ts) for _, req := range requirements { requirement := types.Requirement{ @@ -353,31 +293,3 @@ func (i *Install) prepareBuildScript(script *buildscript.BuildScript, requiremen return script, nil } - -func (i *Install) updateCommitID(commitID strfmt.UUID) error { - if err := localcommit.Set(i.prime.Project().Dir(), commitID.String()); err != nil { - return locale.WrapError(err, "err_package_update_commit_id") - } - - if i.prime.Config().GetBool(constants.OptinBuildscriptsConfig) { - bp := bpModel.NewBuildPlannerModel(i.prime.Auth()) - script, err := bp.GetBuildScript(commitID.String()) - if err != nil { - return errs.Wrap(err, "Could not get remote build expr and time") - } - - err = buildscript_runbit.Update(i.prime.Project(), script) - if err != nil { - return locale.WrapError(err, "err_update_build_script") - } - } - - return nil -} - -func IsBuildError(err error) bool { - var errBuild *runtime.BuildError - var errBuildPlanner *response.BuildPlannerError - - return errors.As(err, &errBuild) || errors.As(err, &errBuildPlanner) -} diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go index ac69eed7bf..6d988539d7 100644 --- a/internal/runners/install/rationalize.go +++ b/internal/runners/install/rationalize.go @@ -8,14 +8,14 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/rationalizers" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" ) func rationalizeError(auth *authentication.Auth, rerr *error) { - var commitError *bpResp.CommitError var noMatchErr errNoMatches switch { @@ -45,36 +45,8 @@ func rationalizeError(auth *authentication.Auth, rerr *error) { *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "))) // Error staging a commit during install. - case errors.As(*rerr, &commitError): - switch commitError.Type { - case types.NotFoundErrorType: - *rerr = errs.WrapUserFacing(*rerr, - locale.Tl("err_packages_not_found", "Could not make runtime changes because your project was not found."), - errs.SetInput(), - errs.SetTips(locale.T("tip_private_project_auth")), - ) - case types.ForbiddenErrorType: - *rerr = errs.WrapUserFacing(*rerr, - locale.Tl("err_packages_forbidden", "Could not make runtime changes because you do not have permission to do so."), - errs.SetInput(), - errs.SetTips(locale.T("tip_private_project_auth")), - ) - case types.HeadOnBranchMovedErrorType: - *rerr = errs.WrapUserFacing(*rerr, - locale.T("err_buildplanner_head_on_branch_moved"), - errs.SetInput(), - ) - case types.NoChangeSinceLastCommitErrorType: - *rerr = errs.WrapUserFacing(*rerr, - locale.Tl("err_packages_exist", "The requested package(s) is already installed."), - errs.SetInput(), - ) - default: - *rerr = errs.WrapUserFacing(*rerr, - locale.Tl("err_packages_buildplanner_error", "Could not make runtime changes due to the following error: {{.V0}}", commitError.Message), - errs.SetInput(), - ) - } + case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): + rationalizers.HandleCommitErrors(rerr) } } diff --git a/internal/runners/platforms/add.go b/internal/runners/platforms/add.go index 767c569f2a..dcbe238fe3 100644 --- a/internal/runners/platforms/add.go +++ b/internal/runners/platforms/add.go @@ -1,13 +1,23 @@ package platforms import ( + "errors" + + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/commits_runbit" "github.com/ActiveState/cli/internal/runbits/rationalize" - "github.com/ActiveState/cli/internal/runbits/runtime/requirements" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/internal/runbits/rationalizers" + "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/pkg/localcommit" + bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/model" + bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" ) // AddRunParams tracks the info required for running Add. @@ -38,7 +48,9 @@ func NewAdd(prime primeable) *Add { } // Run executes the add behavior. -func (a *Add) Run(ps AddRunParams) error { +func (a *Add) Run(ps AddRunParams) (rerr error) { + defer rationalizeAddPlatformError(&rerr) + logging.Debug("Execute platforms add") params, err := prepareParams(ps.Params) @@ -50,20 +62,75 @@ func (a *Add) Run(ps AddRunParams) error { return rationalize.ErrNoProject } - if err := requirements.NewRequirementOperation(a.prime).ExecuteRequirementOperation( - nil, - &requirements.Requirement{ - Name: params.name, - Version: params.version, - Operation: types.OperationAdded, - BitWidth: params.BitWidth, - NamespaceType: &model.NamespacePlatform, - }, - ); err != nil { - return locale.WrapError(err, "err_add_platform", "Could not add platform.") + pj := a.prime.Project() + out := a.prime.Output() + bp := bpModel.NewBuildPlannerModel(a.prime.Auth()) + + var pg *output.Spinner + defer func() { + if pg != nil { + pg.Stop(locale.T("progress_fail")) + } + }() + + pg = output.StartSpinner(out, locale.T("progress_platform_search"), constants.TerminalAnimationInterval) + + // Grab local commit info + localCommitID, err := localcommit.Get(pj.Dir()) + if err != nil { + return errs.Wrap(err, "Unable to get local commit") + } + oldCommit, err := bp.FetchCommit(localCommitID, pj.Owner(), pj.Name(), nil) + if err != nil { + return errs.Wrap(err, "Failed to fetch old build result") + } + + // Resolve platform + platform, err := model.FetchPlatformByDetails(params.Platform.Name(), params.Platform.Version(), params.BitWidth) + if err != nil { + return errs.Wrap(err, "Could not fetch platform") + } + + pg.Stop(locale.T("progress_found")) + pg = nil + + // Resolve timestamp, commit and languages used for current project. + ts, err := commits_runbit.ExpandTimeForProject(nil, a.prime.Auth(), pj) + if err != nil { + return errs.Wrap(err, "Unable to get timestamp from params") + } + + // Prepare updated buildscript + script := oldCommit.BuildScript() + script.SetAtTime(ts) + script.AddPlatform(*platform.PlatformID) + + // Update local checkout and source runtime changes + if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", *platform.DisplayName)); err != nil { + return errs.Wrap(err, "Failed to update local checkout") } - a.prime.Output().Notice(locale.Tr("platform_added", params.name, params.version)) + out.Notice(locale.Tr("platform_added", *platform.DisplayName)) return nil } + +func rationalizeAddPlatformError(rerr *error) { + switch { + case rerr == nil: + return + + // No matches found + case errors.Is(*rerr, model.ErrPlatformNotFound): + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("platform_add_not_found"), + errs.SetInput(), + ) + + // Error staging a commit during install. + case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): + rationalizers.HandleCommitErrors(rerr) + + } +} diff --git a/internal/runners/platforms/platforms.go b/internal/runners/platforms/platforms.go index 6d335eea64..5cd12af476 100644 --- a/internal/runners/platforms/platforms.go +++ b/internal/runners/platforms/platforms.go @@ -66,19 +66,19 @@ func makePlatformsFromModelPlatforms(platforms []*model.Platform) []*Platform { // Params represents the minimal defining details of a platform. type Params struct { - Platform PlatformVersion - BitWidth int - name string - version string + Platform PlatformVersion + BitWidth int + resolvedName string // Holds the provided platforn name, or defaults to curernt platform name if not provided + resolvedVersion string // Holds the provided platform version, or defaults to latest version if not provided } func prepareParams(ps Params) (Params, error) { - ps.name = ps.Platform.Name() - if ps.name == "" { - ps.name = sysinfo.OS().String() + ps.resolvedName = ps.Platform.Name() + if ps.resolvedName == "" { + ps.resolvedName = sysinfo.OS().String() } - ps.version = ps.Platform.Version() - if ps.version == "" { + ps.resolvedVersion = ps.Platform.Version() + if ps.resolvedVersion == "" { return prepareLatestVersion(ps) } @@ -99,8 +99,8 @@ func prepareLatestVersion(params Params) (Params, error) { if err != nil { return params, locale.WrapError(err, "err_fetch_platform", "Could not get platform details") } - params.name = *platform.Kernel.Name - params.version = *platform.KernelVersion.Version + params.resolvedName = *platform.Kernel.Name + params.resolvedVersion = *platform.KernelVersion.Version bitWidth, err := strconv.Atoi(*platform.CPUArchitecture.BitWidth) if err != nil { diff --git a/internal/runners/platforms/remove.go b/internal/runners/platforms/remove.go index 3690be8257..b8fe20c01f 100644 --- a/internal/runners/platforms/remove.go +++ b/internal/runners/platforms/remove.go @@ -43,8 +43,8 @@ func (r *Remove) Run(ps RemoveRunParams) error { if err := requirements.NewRequirementOperation(r.prime).ExecuteRequirementOperation( nil, &requirements.Requirement{ - Name: params.name, - Version: params.version, + Name: params.resolvedName, + Version: params.resolvedVersion, Operation: types.OperationRemoved, BitWidth: params.BitWidth, NamespaceType: &model.NamespacePlatform, @@ -53,7 +53,7 @@ func (r *Remove) Run(ps RemoveRunParams) error { return locale.WrapError(err, "err_remove_platform", "Could not remove platform.") } - r.prime.Output().Notice(locale.Tr("platform_removed", params.name, params.version)) + r.prime.Output().Notice(locale.Tr("platform_removed", params.resolvedName, params.resolvedVersion)) return nil } diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index b7fd20282a..913cb521a0 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -119,9 +119,9 @@ func (b *BuildScript) UpdatePlatform(operation types.Operation, platformID strfm var err error switch operation { case types.OperationAdded: - err = b.addPlatform(platformID) + err = b.AddPlatform(platformID) case types.OperationRemoved: - err = b.removePlatform(platformID) + err = b.RemovePlatform(platformID) default: return errs.New("Unsupported operation") } @@ -131,7 +131,7 @@ func (b *BuildScript) UpdatePlatform(operation types.Operation, platformID strfm return nil } -func (b *BuildScript) addPlatform(platformID strfmt.UUID) error { +func (b *BuildScript) AddPlatform(platformID strfmt.UUID) error { platformsNode, err := b.getPlatformsNode() if err != nil { return errs.Wrap(err, "Could not get platforms node") @@ -149,7 +149,7 @@ type PlatformNotFoundError struct { *locale.LocalizedError // for legacy non-user-facing error usages } -func (b *BuildScript) removePlatform(platformID strfmt.UUID) error { +func (b *BuildScript) RemovePlatform(platformID strfmt.UUID) error { platformsNode, err := b.getPlatformsNode() if err != nil { return errs.Wrap(err, "Could not get platforms node") diff --git a/pkg/platform/model/checkpoints.go b/pkg/platform/model/checkpoints.go index fe63fb8632..f7beccbc5d 100644 --- a/pkg/platform/model/checkpoints.go +++ b/pkg/platform/model/checkpoints.go @@ -191,12 +191,7 @@ func PlatformNameToPlatformID(name string) (string, error) { if name == "darwin" { name = "macos" } - id, err := hostPlatformToPlatformID(name) - return id, err -} - -func hostPlatformToPlatformID(os string) (string, error) { - switch strings.ToLower(os) { + switch strings.ToLower(name) { case strings.ToLower(sysinfo.Linux.String()): return constants.LinuxBit64UUID, nil case strings.ToLower(sysinfo.Mac.String()): @@ -204,7 +199,7 @@ func hostPlatformToPlatformID(os string) (string, error) { case strings.ToLower(sysinfo.Windows.String()): return constants.Win10Bit64UUID, nil default: - return "", locale.NewExternalError("err_unsupported_platform", "", os) + return "", ErrPlatformNotFound } } diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index 5e976b9c68..951869db4f 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -1,7 +1,7 @@ package model import ( - "fmt" + "errors" "regexp" "runtime" "sort" @@ -453,7 +453,18 @@ func FetchPlatformByUID(uid strfmt.UUID) (*Platform, error) { return nil, nil } -func FetchPlatformByDetails(name, version string, word int, auth *authentication.Auth) (*Platform, error) { +var ErrPlatformNotFound = errors.New("could not find platform matching provided criteria") + +func FetchPlatformByDetails(name, version string, bitwidth int) (*Platform, error) { + var platformID string + if version == "" && bitwidth == 0 { + var err error + platformID, err = PlatformNameToPlatformID(name) + if err != nil { + return nil, errs.Wrap(err, "platform id from name failed") + } + } + runtimePlatforms, err := FetchPlatforms() if err != nil { return nil, err @@ -462,6 +473,12 @@ func FetchPlatformByDetails(name, version string, word int, auth *authentication lower := strings.ToLower for _, rtPf := range runtimePlatforms { + if platformID != "" { + if rtPf.PlatformID.String() == platformID { + return rtPf, nil + } + continue + } if rtPf.Kernel == nil || rtPf.Kernel.Name == nil { continue } @@ -479,16 +496,14 @@ func FetchPlatformByDetails(name, version string, word int, auth *authentication if rtPf.CPUArchitecture == nil { continue } - if rtPf.CPUArchitecture.BitWidth == nil || *rtPf.CPUArchitecture.BitWidth != strconv.Itoa(word) { + if rtPf.CPUArchitecture.BitWidth == nil || *rtPf.CPUArchitecture.BitWidth != strconv.Itoa(bitwidth) { continue } return rtPf, nil } - details := fmt.Sprintf("%s %d %s", name, word, version) - - return nil, locale.NewExternalError("err_unsupported_platform", "", details) + return nil, ErrPlatformNotFound } func FetchLanguageForCommit(commitID strfmt.UUID, auth *authentication.Auth) (*Language, error) { diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 2c4194f81f..43a762286c 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -556,7 +556,7 @@ func UpdateProjectBranchCommitWithModel(pjm *mono_models.Project, branchName str // CommitInitial creates a root commit for a new branch func CommitInitial(hostPlatform string, langName, langVersion string, auth *authentication.Auth) (strfmt.UUID, error) { - platformID, err := hostPlatformToPlatformID(hostPlatform) + platformID, err := PlatformNameToPlatformID(hostPlatform) if err != nil { return "", err } @@ -665,7 +665,7 @@ func (cs indexedCommits) countBetween(first, last string) (int, error) { func ResolveRequirementNameAndVersion(name, version string, word int, namespace Namespace, auth *authentication.Auth) (string, string, error) { if namespace.Type() == NamespacePlatform { - platform, err := FetchPlatformByDetails(name, version, word, auth) + platform, err := FetchPlatformByDetails(name, version, word) if err != nil { return "", "", errs.Wrap(err, "Could not fetch platform") } diff --git a/test/integration/platforms_int_test.go b/test/integration/platforms_int_test.go index 780edc2f44..4968a5dc59 100644 --- a/test/integration/platforms_int_test.go +++ b/test/integration/platforms_int_test.go @@ -132,6 +132,29 @@ func (suite *PlatformsIntegrationTestSuite) TestPlatforms_addRemoveLatest() { } +func (suite *PlatformsIntegrationTestSuite) TestPlatforms_addNotFound() { + suite.OnlyRunForTags(tagsuite.Platforms) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareEmptyProject() + + // OS name doesn't match + cp := ts.Spawn("platforms", "add", "bunnies") + cp.Expect("Could not find") + cp.ExpectExitCode(1) + + // OS version doesn't match + cp = ts.Spawn("platforms", "add", "windows@99.99.99") + cp.Expect("Could not find") + cp.ExpectExitCode(1) + + // bitwidth version doesn't match + cp = ts.Spawn("platforms", "add", "windows", "--bit-width=999") + cp.Expect("Could not find") + cp.ExpectExitCode(1) +} + func (suite *PlatformsIntegrationTestSuite) TestJSON() { suite.OnlyRunForTags(tagsuite.Platforms, tagsuite.JSON) ts := e2e.New(suite.T(), false) From 2dacc37d8f50289b09083fd669f8692eb9e71a5b Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 9 Sep 2024 10:20:48 -0700 Subject: [PATCH 03/49] Use new runner for `bundles install` --- cmd/state/internal/cmdtree/bundles.go | 11 ++++++++--- cmd/state/internal/cmdtree/packages.go | 4 ++-- internal/captain/values.go | 12 ++++++++---- internal/runners/install/install.go | 9 +++++---- internal/runners/upgrade/upgrade.go | 2 +- pkg/platform/model/vcs.go | 19 +++++++++++-------- test/integration/bundle_int_test.go | 14 ++++++++++++++ 7 files changed, 49 insertions(+), 22 deletions(-) diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index 73e61d5175..b1549568b8 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -44,8 +44,7 @@ func newBundlesCommand(prime *primer.Values) *captain.Command { } func newBundleInstallCommand(prime *primer.Values) *captain.Command { - runner := install.NewInstall(prime) - + runner := install.NewInstall(prime, model.NamespaceBundle) params := install.InstallRunParams{} return captain.NewCommand( @@ -62,7 +61,13 @@ func newBundleInstallCommand(prime *primer.Values) *captain.Command { Required: true, }, }, - func(_ *captain.Command, _ []string) error { + func(_ *captain.Command, args []string) error { + for _, p := range args { + _, err := params.Packages.Add(p) + if err != nil { + return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments") + } + } return runner.Run(params) }, ).SetSupportsStructuredOutput() diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index 3c314c7311..d2e250f954 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -50,7 +50,7 @@ func newPackagesCommand(prime *primer.Values) *captain.Command { } func newInstallCommand(prime *primer.Values) *captain.Command { - runner := install.NewInstall(prime) + runner := install.NewInstall(prime, model.NamespacePackage) params := install.InstallRunParams{} @@ -77,7 +77,7 @@ func newInstallCommand(prime *primer.Values) *captain.Command { }, func(_ *captain.Command, args []string) error { for _, p := range args { - if err := params.Packages.Set(p); err != nil { + if _, err := params.Packages.Add(p); err != nil { return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments") } } diff --git a/internal/captain/values.go b/internal/captain/values.go index d6f4c7faa3..a819291c3f 100644 --- a/internal/captain/values.go +++ b/internal/captain/values.go @@ -203,7 +203,7 @@ func (p *PackageValueNSRequired) Type() string { } // PackagesValue is used to represent multiple PackageValue, this is used when a flag can be passed multiple times. -type PackagesValue []PackageValue +type PackagesValue []*PackageValue var _ FlagMarshaler = &PackagesValue{} @@ -216,12 +216,16 @@ func (p *PackagesValue) String() string { } func (p *PackagesValue) Set(s string) error { + return nil // This is currently not natively supported by captain as it takes a full list of arguments +} + +func (p *PackagesValue) Add(s string) (*PackageValue, error) { pf := &PackageValue{} if err := pf.Set(s); err != nil { - return err + return nil, err } - *p = append(*p, *pf) - return nil + *p = append(*p, pf) + return pf, nil } func (p *PackagesValue) Type() string { diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index d4f84a9560..2c6fc7976e 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -64,12 +64,13 @@ func (r requirements) String() string { // Install manages the installing execution context. type Install struct { - prime primeable + prime primeable + nsType model.NamespaceType } // NewInstall prepares an installation execution context for use. -func NewInstall(prime primeable) *Install { - return &Install{prime} +func NewInstall(prime primeable, nsType model.NamespaceType) *Install { + return &Install{prime, nsType} } // Run executes the install behavior. @@ -169,7 +170,7 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti var failed []*requirement reqs := []*requirement{} for _, pkg := range packages { - req := &requirement{input: &pkg} + req := &requirement{input: pkg} if pkg.Namespace != "" { req.resolvedNamespace = ptr.To(model.NewNamespaceRaw(pkg.Namespace)) } diff --git a/internal/runners/upgrade/upgrade.go b/internal/runners/upgrade/upgrade.go index 9730b39361..9014cd12d0 100644 --- a/internal/runners/upgrade/upgrade.go +++ b/internal/runners/upgrade/upgrade.go @@ -254,7 +254,7 @@ func (u *Upgrade) renderUserFacing(changes []structuredChange, expand bool) erro }) needsDepRow := len(change.TransitiveDeps) > 0 - needsNamespaceRow := strings.HasPrefix(change.Namespace, model.OrgNamespacePrefix) + needsNamespaceRow := strings.HasPrefix(change.Namespace, model.NamespaceOrg.Prefix()) if needsNamespaceRow { treeSymbol := output.TreeEnd diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 43a762286c..7aef7f2fb4 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -134,7 +134,7 @@ var ( NamespaceBundle = NamespaceType{"bundle", "bundles", NamespaceBundlesMatch} NamespaceLanguage = NamespaceType{"language", "", NamespaceLanguageMatch} NamespacePlatform = NamespaceType{"platform", "", NamespacePlatformMatch} - NamespaceOrg = NamespaceType{"org", "org", NamespaceOrgMatch} + NamespaceOrg = NamespaceType{"org", "private/", NamespaceOrgMatch} NamespaceRaw = NamespaceType{"raw", "", ""} NamespaceBlank = NamespaceType{"", "", ""} ) @@ -216,21 +216,24 @@ func NewNamespacePlatform() Namespace { return Namespace{NamespacePlatform, "platform"} } -const OrgNamespacePrefix = "private/" - func NewOrgNamespace(orgName string) Namespace { return Namespace{ nsType: NamespaceOrg, - value: OrgNamespacePrefix + orgName, + value: NamespaceOrg.prefix + "/" + orgName, } } func LanguageFromNamespace(ns string) string { - values := strings.Split(ns, "/") - if len(values) != 2 { - return "" + matchables := []NamespaceMatchable{ + NamespacePackage.Matchable(), + NamespaceBundle.Matchable(), + } + for _, m := range matchables { + if NamespaceMatch(ns, m) { + return strings.Split(ns, "/")[1] + } } - return values[1] + return "" } // FilterSupportedIngredients filters a list of ingredients, returning only those that are currently supported (such that they can be built) by the Platform diff --git a/test/integration/bundle_int_test.go b/test/integration/bundle_int_test.go index c1d6907da7..6207c8f5de 100644 --- a/test/integration/bundle_int_test.go +++ b/test/integration/bundle_int_test.go @@ -36,6 +36,20 @@ func (suite *BundleIntegrationTestSuite) TestBundle_project_name_noData() { cp.ExpectExitCode(0) } +func (suite *BundleIntegrationTestSuite) TestBundle_install() { + suite.OnlyRunForTags(tagsuite.Bundle) + ts := e2e.New(suite.T(), false) + defer ts.Close() + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + + cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") + cp.ExpectExitCode(0) + + cp = ts.Spawn("bundles", "install", "python-module-build-support") + cp.Expect("project has been updated") + cp.ExpectExitCode(0) +} + func (suite *BundleIntegrationTestSuite) TestBundle_searchSimple() { suite.OnlyRunForTags(tagsuite.Bundle) ts := e2e.New(suite.T(), false) From 60c7ee18787117eb90e560746a2d81d35ca849fc Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 9 Sep 2024 10:21:04 -0700 Subject: [PATCH 04/49] Fix suggestions --- internal/locale/locales/en-us.yaml | 8 ++--- internal/runners/install/install.go | 12 ++++--- internal/runners/install/rationalize.go | 43 +++++++++++++++++-------- test/integration/install_int_test.go | 17 ++++++++++ 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 64d81f4a71..1f50ebe6f0 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1147,16 +1147,16 @@ err_exec_recursion: To allow recursion, set [ACTIONABLE]{{.V1}}=true[/RESET] package_ingredient_alternatives: other: | - No results found for search term "[NOTICE]{{.V0}}[/RESET]". Did you mean: + No results found for search term {{.V0}}. Did you mean: {{.V1}} - Run "[ACTIONABLE]state search {{.V0}}[/RESET]" to see more suggestions. + Use "[ACTIONABLE]state search[/RESET]" to find more suggestions. package_ingredient_alternatives_nosuggest: other: | - No results found for search term "[NOTICE]{{.V0}}[/RESET]". + No results found for search term {{.V0}}. - Run "[ACTIONABLE]state search {{.V0}}[/RESET]" to find alternatives. + Use "[ACTIONABLE]state search[/RESET]" to find alternatives. package_requirements_no_match: other: | No results found for following packages: {{.V0}}. diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 2c6fc7976e..74d2fe8cff 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -75,7 +75,7 @@ func NewInstall(prime primeable, nsType model.NamespaceType) *Install { // Run executes the install behavior. func (i *Install) Run(params InstallRunParams) (rerr error) { - defer rationalizeError(i.prime.Auth(), &rerr) + defer i.rationalizeError(&rerr) logging.Debug("ExecuteInstall") @@ -162,6 +162,7 @@ func (i *Install) Run(params InstallRunParams) (rerr error) { type errNoMatches struct { error requirements []*requirement + languages []model.Language } // resolveRequirements will attempt to resolve the ingredient and namespace for each requested package @@ -184,8 +185,11 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti // Resolve matched ingredients if pkg.Namespace == "" { // Filter out ingredients that don't target one of the supported languages - ingredients = sliceutils.Filter(ingredients, func(i *model.IngredientAndVersion) bool { - il := model.LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) + ingredients = sliceutils.Filter(ingredients, func(iv *model.IngredientAndVersion) bool { + if !model.NamespaceMatch(*iv.Ingredient.PrimaryNamespace, i.nsType.Matchable()) { + return false + } + il := model.LanguageFromNamespace(*iv.Ingredient.PrimaryNamespace) for _, l := range languages { if l.Name == il { return true @@ -214,7 +218,7 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti // Fail if not all requirements could be resolved if len(failed) > 0 { - return nil, errNoMatches{error: errs.New("Failed to resolve requirements"), requirements: failed} + return nil, errNoMatches{error: errs.New("Failed to resolve requirements"), requirements: failed, languages: languages} } // Disambiguate requirements that match multiple ingredients diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go index 6d988539d7..ecad652a7d 100644 --- a/internal/runners/install/rationalize.go +++ b/internal/runners/install/rationalize.go @@ -10,12 +10,12 @@ import ( "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/rationalizers" + "github.com/ActiveState/cli/internal/sliceutils" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" - "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" ) -func rationalizeError(auth *authentication.Auth, rerr *error) { +func (i *Install) rationalizeError(rerr *error) { var noMatchErr errNoMatches switch { @@ -26,13 +26,13 @@ func rationalizeError(auth *authentication.Auth, rerr *error) { case errors.As(*rerr, &noMatchErr): names := []string{} for _, r := range noMatchErr.requirements { - names = append(names, fmt.Sprintf(`"[[ACTIONABLE]%s[/RESET]"`, r.input.Name)) + names = append(names, fmt.Sprintf(`[ACTIONABLE]%s[/RESET]`, r.input.Name)) } if len(noMatchErr.requirements) > 1 { *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_requirements_no_match", strings.Join(names, ", "))) return } - suggestions, err := getSuggestions(noMatchErr.requirements[0], auth) + suggestions, err := i.getSuggestions(noMatchErr.requirements[0], noMatchErr.languages) if err != nil { multilog.Error("Failed to retrieve suggestions: %v", err) } @@ -42,7 +42,7 @@ func rationalizeError(auth *authentication.Auth, rerr *error) { return } - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "))) + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "), strings.Join(suggestions, "\n"))) // Error staging a commit during install. case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): @@ -51,20 +51,37 @@ func rationalizeError(auth *authentication.Auth, rerr *error) { } } -func getSuggestions(req *requirement, auth *authentication.Auth) ([]string, error) { - results, err := model.SearchIngredients(req.input.Namespace, req.input.Name, false, nil, auth) +func (i *Install) getSuggestions(req *requirement, languages []model.Language) ([]string, error) { + ingredients, err := model.SearchIngredients(req.input.Namespace, req.input.Name, false, nil, i.prime.Auth()) if err != nil { return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", req.input.Name) } - maxResults := 5 - if len(results) > maxResults { - results = results[:maxResults] + // Filter out irrelevant ingredients + if req.input.Namespace == "" { + // Filter out ingredients that don't target one of the supported languages + ingredients = sliceutils.Filter(ingredients, func(iv *model.IngredientAndVersion) bool { + if !model.NamespaceMatch(*iv.Ingredient.PrimaryNamespace, i.nsType.Matchable()) { + return false + } + il := model.LanguageFromNamespace(*iv.Ingredient.PrimaryNamespace) + for _, l := range languages { + if l.Name == il { + return true + } + } + return false + }) + } + + suggestions := []string{} + for _, ing := range ingredients { + suggestions = append(suggestions, fmt.Sprintf(" - %s/%s", *ing.Ingredient.PrimaryNamespace, *ing.Ingredient.Name)) } - suggestions := make([]string, 0, maxResults+1) - for _, result := range results { - suggestions = append(suggestions, fmt.Sprintf(" - %s", *result.Ingredient.Name)) + maxResults := 5 + if len(suggestions) > maxResults { + suggestions = suggestions[:maxResults] } return suggestions, nil diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go index 6865e4a67a..3adbf3939c 100644 --- a/test/integration/install_int_test.go +++ b/test/integration/install_int_test.go @@ -29,6 +29,23 @@ func (suite *InstallIntegrationTestSuite) TestInstall() { cp.ExpectExitCode(0) } +func (suite *InstallIntegrationTestSuite) TestInstallSuggest() { + suite.OnlyRunForTags(tagsuite.Install, tagsuite.Critical) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + + cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") + cp.ExpectExitCode(0) + + cp = ts.Spawn("install", "djang") + cp.Expect("No results found") + cp.Expect("Did you mean") + cp.Expect("language/python/djang") + cp.ExpectExitCode(1) +} + func (suite *InstallIntegrationTestSuite) TestInstall_InvalidCommit() { suite.OnlyRunForTags(tagsuite.Install) ts := e2e.New(suite.T(), false) From ac10a21ca438ed8ddd05a1b1907f8a0d8b183b9f Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 9 Sep 2024 11:31:25 -0700 Subject: [PATCH 05/49] languages install now uses new runner --- cmd/state/internal/cmdtree/languages.go | 20 ++++++++++++++------ test/integration/languages_int_test.go | 6 ++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cmd/state/internal/cmdtree/languages.go b/cmd/state/internal/cmdtree/languages.go index 5adafc39b5..dabf179d3f 100644 --- a/cmd/state/internal/cmdtree/languages.go +++ b/cmd/state/internal/cmdtree/languages.go @@ -4,7 +4,9 @@ import ( "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/runners/install" "github.com/ActiveState/cli/internal/runners/languages" + "github.com/ActiveState/cli/pkg/platform/model" ) func newLanguagesCommand(prime *primer.Values) *captain.Command { @@ -24,9 +26,8 @@ func newLanguagesCommand(prime *primer.Values) *captain.Command { } func newLanguageInstallCommand(prime *primer.Values) *captain.Command { - runner := languages.NewUpdate(prime) - - params := languages.UpdateParams{} + runner := install.NewInstall(prime, model.NamespaceLanguage) + params := install.InstallRunParams{} return captain.NewCommand( "install", @@ -39,11 +40,18 @@ func newLanguageInstallCommand(prime *primer.Values) *captain.Command { Name: "language", Description: locale.T("arg_languages_install_description"), Required: true, - Value: ¶ms.Language, + Value: ¶ms.Packages, }, }, - func(ccmd *captain.Command, _ []string) error { - return runner.Run(¶ms) + func(ccmd *captain.Command, args []string) error { + for _, p := range args { + pkg, err := params.Packages.Add(p) + if err != nil { + return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments") + } + pkg.Namespace = model.NamespaceLanguage.String() + } + return runner.Run(params) }, ).SetSupportsStructuredOutput() } diff --git a/test/integration/languages_int_test.go b/test/integration/languages_int_test.go index 4b05858ea3..d75f1b641d 100644 --- a/test/integration/languages_int_test.go +++ b/test/integration/languages_int_test.go @@ -51,10 +51,8 @@ func (suite *LanguagesIntegrationTestSuite) TestLanguages_install() { ts.PrepareProject("ActiveState-CLI/Languages", "1eb82b25-a564-42ee-a7d4-d51d2ea73cd5") - ts.LoginAsPersistentUser() - cp := ts.Spawn("languages") - cp.Expect("Name") + cp.Expect("Name", termtest.OptExpectTimeout(60*time.Second)) // Cached solves are often slow too cp.Expect("python") cp.ExpectExitCode(0) @@ -62,7 +60,7 @@ func (suite *LanguagesIntegrationTestSuite) TestLanguages_install() { cp.ExpectExitCode(0) cp = ts.Spawn("languages", "install", "python@3.9.16") - cp.Expect("Language updated: python@3.9.16") + cp.Expect("project has been updated") // This can take a little while cp.ExpectExitCode(0, termtest.OptExpectTimeout(60*time.Second)) From 589c236bce4f7f72373f626da5d4c158dbd1c04c Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 9 Sep 2024 14:52:59 -0700 Subject: [PATCH 06/49] Implemented uninstall runner --- cmd/state/internal/cmdtree/bundles.go | 4 +- cmd/state/internal/cmdtree/languages.go | 4 +- cmd/state/internal/cmdtree/packages.go | 13 ++- internal/locale/locales/en-us.yaml | 6 + internal/runners/install/install.go | 20 ++-- internal/runners/uninstall/rationalize.go | 38 ++++++ internal/runners/uninstall/uninstall.go | 134 ++++++++++++++++++++++ pkg/buildscript/mutations.go | 28 +++-- test/integration/package_int_test.go | 4 +- 9 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 internal/runners/uninstall/rationalize.go create mode 100644 internal/runners/uninstall/uninstall.go diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index b1549568b8..8351a2bc02 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -44,8 +44,8 @@ func newBundlesCommand(prime *primer.Values) *captain.Command { } func newBundleInstallCommand(prime *primer.Values) *captain.Command { - runner := install.NewInstall(prime, model.NamespaceBundle) - params := install.InstallRunParams{} + runner := install.New(prime, model.NamespaceBundle) + params := install.Params{} return captain.NewCommand( "install", diff --git a/cmd/state/internal/cmdtree/languages.go b/cmd/state/internal/cmdtree/languages.go index dabf179d3f..1c6ad1229e 100644 --- a/cmd/state/internal/cmdtree/languages.go +++ b/cmd/state/internal/cmdtree/languages.go @@ -26,8 +26,8 @@ func newLanguagesCommand(prime *primer.Values) *captain.Command { } func newLanguageInstallCommand(prime *primer.Values) *captain.Command { - runner := install.NewInstall(prime, model.NamespaceLanguage) - params := install.InstallRunParams{} + runner := install.New(prime, model.NamespaceLanguage) + params := install.Params{} return captain.NewCommand( "install", diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index d2e250f954..636dc02dc3 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -6,6 +6,7 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runners/install" "github.com/ActiveState/cli/internal/runners/packages" + "github.com/ActiveState/cli/internal/runners/uninstall" "github.com/ActiveState/cli/pkg/platform/model" ) @@ -50,9 +51,9 @@ func newPackagesCommand(prime *primer.Values) *captain.Command { } func newInstallCommand(prime *primer.Values) *captain.Command { - runner := install.NewInstall(prime, model.NamespacePackage) + runner := install.New(prime, model.NamespacePackage) - params := install.InstallRunParams{} + params := install.Params{} var packagesRaw string cmd := captain.NewCommand( @@ -93,9 +94,9 @@ func newInstallCommand(prime *primer.Values) *captain.Command { } func newUninstallCommand(prime *primer.Values) *captain.Command { - runner := packages.NewUninstall(prime) + runner := uninstall.New(prime, model.NamespacePackage) - params := packages.UninstallRunParams{} + params := uninstall.Params{} var packagesRaw string cmd := captain.NewCommand( @@ -114,11 +115,11 @@ func newUninstallCommand(prime *primer.Values) *captain.Command { }, func(_ *captain.Command, args []string) error { for _, p := range args { - if err := params.Packages.Set(p); err != nil { + if _, err := params.Packages.Add(p); err != nil { return locale.WrapInputError(err, "err_uninstall_packages_args", "Invalid package uninstall arguments") } } - return runner.Run(params, model.NamespacePackage) + return runner.Run(params) }, ) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 1f50ebe6f0..10154aa5a5 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -606,6 +606,8 @@ package_import_flag_filename_description: other: The file to import commit_message_added: other: "Added: {{.V0}}" +commit_message_removed: + other: "Removed: {{.V0}}" commit_message_added_package: other: "Added package: {{.V0}}@{{.V1}}" commit_message_removed_package: @@ -1587,3 +1589,7 @@ flag_state_upgrade_ts_description: other: Manually specify the timestamp to 'upgrade' to. Can be either 'now' or RFC3339 formatted timestamp. platform_add_not_found: other: Could not find a platform matching your criteria +err_uninstall_nomatch: + other: "The following package(s) could not be found in your project: {{.V0}}" +progress_requirements: + other: "• Updating requirements" diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 74d2fe8cff..d0aaa90432 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -35,8 +35,8 @@ type primeable interface { primer.SvcModeler } -// InstallRunParams tracks the info required for running Install. -type InstallRunParams struct { +// Params tracks the info required for running Install. +type Params struct { Packages captain.PackagesValue Timestamp captain.TimeValue } @@ -68,13 +68,13 @@ type Install struct { nsType model.NamespaceType } -// NewInstall prepares an installation execution context for use. -func NewInstall(prime primeable, nsType model.NamespaceType) *Install { +// New prepares an installation execution context for use. +func New(prime primeable, nsType model.NamespaceType) *Install { return &Install{prime, nsType} } // Run executes the install behavior. -func (i *Install) Run(params InstallRunParams) (rerr error) { +func (i *Install) Run(params Params) (rerr error) { defer i.rationalizeError(&rerr) logging.Debug("ExecuteInstall") @@ -143,8 +143,8 @@ func (i *Install) Run(params InstallRunParams) (rerr error) { } // Prepare updated buildscript - script, err := prepareBuildScript(oldCommit.BuildScript(), reqs, ts) - if err != nil { + script := oldCommit.BuildScript() + if err := prepareBuildScript(script, reqs, ts); err != nil { return errs.Wrap(err, "Could not prepare build script") } @@ -281,7 +281,7 @@ func (i *Install) promptForMatchingIngredient(req *requirement) (*model.Ingredie return values[choice], nil } -func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) (*buildscript.BuildScript, error) { +func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) error { script.SetAtTime(ts) for _, req := range requirements { requirement := types.Requirement{ @@ -292,9 +292,9 @@ func prepareBuildScript(script *buildscript.BuildScript, requirements requiremen err := script.AddRequirement(requirement) if err != nil { - return nil, errs.Wrap(err, "Failed to update build expression with requirement") + return errs.Wrap(err, "Failed to update build expression with requirement") } } - return script, nil + return nil } diff --git a/internal/runners/uninstall/rationalize.go b/internal/runners/uninstall/rationalize.go new file mode 100644 index 0000000000..7efcdcdce7 --- /dev/null +++ b/internal/runners/uninstall/rationalize.go @@ -0,0 +1,38 @@ +package uninstall + +import ( + "errors" + "fmt" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/rationalizers" + bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" +) + +func (u *Uninstall) rationalizeError(rerr *error) { + var noMatchesErr *errNoMatches + + switch { + case rerr == nil: + return + + // Error staging a commit during uninstall. + case errors.As(*rerr, &noMatchesErr): + pkgs := []string{} + for _, pkg := range noMatchesErr.packages { + name := pkg.Name + if pkg.Namespace != "" { + name = fmt.Sprintf("%s/%s", pkg.Namespace, pkg.Name) + } + pkgs = append(pkgs, fmt.Sprintf("[ACTIONABLE]%s[/RESET]", name)) + } + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("err_uninstall_nomatch", noMatchesErr.packages.String())) + + // Error staging a commit during install. + case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): + rationalizers.HandleCommitErrors(rerr) + + } +} diff --git a/internal/runners/uninstall/uninstall.go b/internal/runners/uninstall/uninstall.go new file mode 100644 index 0000000000..9f4968b650 --- /dev/null +++ b/internal/runners/uninstall/uninstall.go @@ -0,0 +1,134 @@ +package uninstall + +import ( + "errors" + + "github.com/ActiveState/cli/internal/captain" + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/runbits/rationalize" + "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/pkg/buildscript" + "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/pkg/platform/model" + bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" +) + +type primeable interface { + primer.Outputer + primer.Prompter + primer.Projecter + primer.Auther + primer.Configurer + primer.Analyticer + primer.SvcModeler +} + +// Params tracks the info required for running Uninstall. +type Params struct { + Packages captain.PackagesValue +} + +// Uninstall manages the installing execution context. +type Uninstall struct { + prime primeable + nsType model.NamespaceType +} + +// New prepares an installation execution context for use. +func New(prime primeable, nsType model.NamespaceType) *Uninstall { + return &Uninstall{prime, nsType} +} + +type errNoMatches struct { + error + packages captain.PackagesValue +} + +// Run executes the install behavior. +func (u *Uninstall) Run(params Params) (rerr error) { + defer u.rationalizeError(&rerr) + + logging.Debug("ExecuteUninstall") + + pj := u.prime.Project() + out := u.prime.Output() + bp := bpModel.NewBuildPlannerModel(u.prime.Auth()) + + // Verify input + if pj == nil { + return rationalize.ErrNoProject + } + if pj.IsHeadless() { + return rationalize.ErrHeadless + } + + out.Notice(locale.Tr("operating_message", pj.NamespaceString(), pj.Dir())) + + var pg *output.Spinner + defer func() { + if pg != nil { + pg.Stop(locale.T("progress_fail")) + } + }() + + // Start process of updating requirements + pg = output.StartSpinner(out, locale.T("progress_requirements"), constants.TerminalAnimationInterval) + + // Grab local commit info + localCommitID, err := localcommit.Get(u.prime.Project().Dir()) + if err != nil { + return errs.Wrap(err, "Unable to get local commit") + } + oldCommit, err := bp.FetchCommit(localCommitID, pj.Owner(), pj.Name(), nil) + if err != nil { + return errs.Wrap(err, "Failed to fetch old build result") + } + + // Update buildscript + script := oldCommit.BuildScript() + if err := prepareBuildScript(script, params.Packages); err != nil { + return errs.Wrap(err, "Could not prepare build script") + } + + // Done updating requirements + pg.Stop(locale.T("progress_success")) + pg = nil + + // Update local checkout and source runtime changes + if err := reqop_runbit.UpdateAndReload(u.prime, script, oldCommit, locale.Tr("commit_message_added", params.Packages.String())); err != nil { + return errs.Wrap(err, "Failed to update local checkout") + } + + // All done + out.Notice(locale.T("operation_success_local")) + + return nil +} + +func prepareBuildScript(script *buildscript.BuildScript, pkgs captain.PackagesValue) error { + // Remove requirements + var removeErrs error + notFound := captain.PackagesValue{} + for _, pkg := range pkgs { + if err := script.RemoveRequirement(types.Requirement{Name: pkg.Name, Namespace: pkg.Namespace}); err != nil { + if errors.As(err, ptr.To(&buildscript.RequirementNotFoundError{})) { + notFound = append(notFound, pkg) + removeErrs = errs.Pack(removeErrs, err) + } else { + return errs.Wrap(err, "Unable to remove requirement") + } + } + } + if len(notFound) > 0 { + return errs.Pack(&errNoMatches{error: errs.New("Could not find all requested packages"), packages: notFound}, removeErrs) + } + + return nil +} diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index 913cb521a0..1edcb6d32c 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -78,34 +78,44 @@ type RequirementNotFoundError struct { *locale.LocalizedError // for legacy non-user-facing error usages } +// RemoveRequirement will remove any matching requirement. Note that it only operates on the Name and Namespace fields. +// It will not verify if revision or version match. func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { requirementsNode, err := b.getRequirementsNode() if err != nil { return errs.Wrap(err, "Could not get requirements node") } - var found bool + match := false for i, req := range *requirementsNode.List { if req.FuncCall == nil || req.FuncCall.Name != reqFuncName { continue } for _, arg := range req.FuncCall.Arguments { - if arg.Assignment.Key == requirementNameKey && strValue(arg.Assignment.Value) == requirement.Name { - list := *requirementsNode.List - list = append(list[:i], list[i+1:]...) - requirementsNode.List = &list - found = true - break + if arg.Assignment.Key == requirementNameKey { + match := strValue(arg.Assignment.Value) == requirement.Name + if !match || requirement.Namespace == "" { + break + } + } + if arg.Assignment.Key == requirementNamespaceKey { + match = strValue(arg.Assignment.Value) == requirement.Namespace + if !match { + break + } } } - if found { + if match { + list := *requirementsNode.List + list = append(list[:i], list[i+1:]...) + requirementsNode.List = &list break } } - if !found { + if !match { return &RequirementNotFoundError{ requirement.Name, locale.NewInputError("err_remove_requirement_not_found", "", requirement.Name), diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index d7243b1167..22ac663936 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -434,13 +434,13 @@ scripts: func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), false) + ts := e2e.New(suite.T(), true) defer ts.Close() suite.PrepareActiveStateYAML(ts) cp := ts.Spawn("uninstall", "doesNotExist") - cp.Expect("does not exist") + cp.Expect("could not be found") cp.ExpectExitCode(1) ts.IgnoreLogErrors() From db3c6c6d69a2f1f11bbbe78d22fb1ba15dcf5c54 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 10:01:01 -0700 Subject: [PATCH 07/49] Added `--expand` flag to `state manifest` --- cmd/state/internal/cmdtree/manifest.go | 12 ++++++++++-- internal/runners/manifest/manifest.go | 8 ++++++-- internal/runners/manifest/requirements.go | 24 +++++++++++------------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/cmd/state/internal/cmdtree/manifest.go b/cmd/state/internal/cmdtree/manifest.go index 4f61330343..367dfba44c 100644 --- a/cmd/state/internal/cmdtree/manifest.go +++ b/cmd/state/internal/cmdtree/manifest.go @@ -10,15 +10,23 @@ import ( func newManifestCommmand(prime *primer.Values) *captain.Command { runner := manifest.NewManifest(prime) + params := manifest.Params{} + cmd := captain.NewCommand( "manifest", locale.Tl("manifest_title", "Listing Requirements For Your Project"), locale.Tl("manifest_cmd_description", "List the requirements of the current project"), prime, - []*captain.Flag{}, + []*captain.Flag{ + { + Name: "expand", + Description: locale.Tl("manifest_flag_expand", "Expand requirement names to include their namespace"), + Value: ¶ms.Expand, + }, + }, []*captain.Argument{}, func(_ *captain.Command, _ []string) error { - return runner.Run() + return runner.Run(params) }, ) diff --git a/internal/runners/manifest/manifest.go b/internal/runners/manifest/manifest.go index 073182a6b2..9d165bbef7 100644 --- a/internal/runners/manifest/manifest.go +++ b/internal/runners/manifest/manifest.go @@ -29,6 +29,10 @@ type primeable interface { primer.Configurer } +type Params struct { + Expand bool +} + type Manifest struct { prime primeable // The remainder is redundant with the above. Refactoring this will follow in a later story so as not to blow @@ -54,7 +58,7 @@ func NewManifest(prime primeable) *Manifest { } } -func (m *Manifest) Run() (rerr error) { +func (m *Manifest) Run(params Params) (rerr error) { defer rationalizeError(m.project, m.auth, &rerr) if m.project == nil { @@ -78,7 +82,7 @@ func (m *Manifest) Run() (rerr error) { return errs.Wrap(err, "Could not fetch vulnerabilities") } - reqOut := newRequirements(reqs, bpReqs, vulns, !m.out.Type().IsStructured()) + reqOut := newRequirements(reqs, bpReqs, vulns, !m.out.Type().IsStructured(), params.Expand) if m.out.Type().IsStructured() { m.out.Print(reqOut) } else { diff --git a/internal/runners/manifest/requirements.go b/internal/runners/manifest/requirements.go index e93b719bdc..c85cd6b1a1 100644 --- a/internal/runners/manifest/requirements.go +++ b/internal/runners/manifest/requirements.go @@ -1,6 +1,8 @@ package manifest import ( + "fmt" + "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/pkg/buildplan" @@ -18,9 +20,10 @@ type requirement struct { type requirements struct { Requirements []requirement `json:"requirements"` UnknownRequirements []buildscript.UnknownRequirement `json:"unknown_requirements,omitempty"` + expand bool // Whether to show requirements by their full namespace } -func newRequirements(reqs []buildscript.Requirement, bpReqs buildplan.Ingredients, vulns vulnerabilities, shortRevIDs bool) requirements { +func newRequirements(reqs []buildscript.Requirement, bpReqs buildplan.Ingredients, vulns vulnerabilities, shortRevIDs bool, expand bool) requirements { var knownReqs []requirement var unknownReqs []buildscript.UnknownRequirement for _, req := range reqs { @@ -28,7 +31,7 @@ func newRequirements(reqs []buildscript.Requirement, bpReqs buildplan.Ingredient case buildscript.DependencyRequirement: knownReqs = append(knownReqs, requirement{ Name: r.Name, - Namespace: processNamespace(r.Namespace), + Namespace: r.Namespace, ResolvedVersion: resolveVersion(r.Requirement, bpReqs), Vulnerabilities: vulns.get(r.Name, r.Namespace), }) @@ -49,6 +52,7 @@ func newRequirements(reqs []buildscript.Requirement, bpReqs buildplan.Ingredient return requirements{ Requirements: knownReqs, UnknownRequirements: unknownReqs, + expand: expand, } } @@ -63,13 +67,17 @@ func (o requirements) Print(out output.Outputer) { var requirementsOutput []*requirementOutput for _, req := range o.Requirements { + name := req.Name + if o.expand && req.Namespace != "" { + name = req.Namespace + "/" + req.Name + } requirementOutput := &requirementOutput{ - Name: locale.Tl("manifest_name", "[ACTIONABLE]{{.V0}}[/RESET]", req.Name), + Name: fmt.Sprintf("[ACTIONABLE]%s[/RESET]", name), Version: req.ResolvedVersion.String(), Vulnerabilities: req.Vulnerabilities.String(), } - if req.Namespace != "" { + if isCustomNamespace(req.Namespace) { requirementOutput.Namespace = locale.Tr("namespace_row", output.TreeEnd, req.Namespace) } @@ -108,14 +116,6 @@ func (o requirements) MarshalStructured(f output.Format) interface{} { return o } -func processNamespace(namespace string) string { - if !isCustomNamespace(namespace) { - return "" - } - - return namespace -} - func isCustomNamespace(ns string) bool { supportedNamespaces := []platformModel.NamespaceType{ platformModel.NamespacePackage, From f72b5b195bbd6bb870e6aa771680ba7a9b185bc2 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 10:01:44 -0700 Subject: [PATCH 08/49] Tests and error conditions for `state uninstall` --- internal/locale/locales/en-us.yaml | 5 +++ internal/runners/uninstall/rationalize.go | 14 +++----- internal/runners/uninstall/uninstall.go | 25 ++++++++++++++ pkg/buildscript/mutations.go | 2 +- pkg/buildscript/queries.go | 17 ++++++++++ test/integration/package_int_test.go | 40 +++++++++++++++++++++++ 6 files changed, 92 insertions(+), 11 deletions(-) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 10154aa5a5..48dfe399e2 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1591,5 +1591,10 @@ platform_add_not_found: other: Could not find a platform matching your criteria err_uninstall_nomatch: other: "The following package(s) could not be found in your project: {{.V0}}" +err_uninstall_multimatch: + other: | + The following terms match multiple requirements in your project: [ACTIONABLE]{{.V0}}[/RESET]. + Please specify the requirement to uninstall by using its full namespace. + To view the namespace of your project requirements run: [ACTIONABLE]state manifest[/RESET]. progress_requirements: other: "• Updating requirements" diff --git a/internal/runners/uninstall/rationalize.go b/internal/runners/uninstall/rationalize.go index 7efcdcdce7..767b1c3136 100644 --- a/internal/runners/uninstall/rationalize.go +++ b/internal/runners/uninstall/rationalize.go @@ -2,7 +2,6 @@ package uninstall import ( "errors" - "fmt" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" @@ -13,23 +12,18 @@ import ( func (u *Uninstall) rationalizeError(rerr *error) { var noMatchesErr *errNoMatches + var multipleMatchesErr *errMultipleMatches switch { case rerr == nil: return - // Error staging a commit during uninstall. case errors.As(*rerr, &noMatchesErr): - pkgs := []string{} - for _, pkg := range noMatchesErr.packages { - name := pkg.Name - if pkg.Namespace != "" { - name = fmt.Sprintf("%s/%s", pkg.Namespace, pkg.Name) - } - pkgs = append(pkgs, fmt.Sprintf("[ACTIONABLE]%s[/RESET]", name)) - } *rerr = errs.WrapUserFacing(*rerr, locale.Tr("err_uninstall_nomatch", noMatchesErr.packages.String())) + case errors.As(*rerr, &multipleMatchesErr): + *rerr = errs.WrapUserFacing(*rerr, locale.Tr("err_uninstall_multimatch", multipleMatchesErr.packages.String())) + // Error staging a commit during install. case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): rationalizers.HandleCommitErrors(rerr) diff --git a/internal/runners/uninstall/uninstall.go b/internal/runners/uninstall/uninstall.go index 9f4968b650..94d05bc9a0 100644 --- a/internal/runners/uninstall/uninstall.go +++ b/internal/runners/uninstall/uninstall.go @@ -13,6 +13,7 @@ import ( "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/internal/sliceutils" "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" @@ -51,6 +52,11 @@ type errNoMatches struct { packages captain.PackagesValue } +type errMultipleMatches struct { + error + packages captain.PackagesValue +} + // Run executes the install behavior. func (u *Uninstall) Run(params Params) (rerr error) { defer u.rationalizeError(&rerr) @@ -113,6 +119,25 @@ func (u *Uninstall) Run(params Params) (rerr error) { } func prepareBuildScript(script *buildscript.BuildScript, pkgs captain.PackagesValue) error { + reqs, err := script.DependencyRequirements() + if err != nil { + return errs.Wrap(err, "Unable to get requirements") + } + + // Check that we're not matching multiple packages + multipleMatches := captain.PackagesValue{} + for _, pkg := range pkgs { + matches := sliceutils.Filter(reqs, func(req types.Requirement) bool { + return pkg.Name == req.Name && (pkg.Namespace == "" || pkg.Namespace == req.Namespace) + }) + if len(matches) > 1 { + multipleMatches = append(multipleMatches, pkg) + } + } + if len(multipleMatches) > 0 { + return &errMultipleMatches{error: errs.New("Could not find all requested packages"), packages: multipleMatches} + } + // Remove requirements var removeErrs error notFound := captain.PackagesValue{} diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index 1edcb6d32c..de3bba9169 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -94,7 +94,7 @@ func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { for _, arg := range req.FuncCall.Arguments { if arg.Assignment.Key == requirementNameKey { - match := strValue(arg.Assignment.Value) == requirement.Name + match = strValue(arg.Assignment.Value) == requirement.Name if !match || requirement.Namespace == "" { break } diff --git a/pkg/buildscript/queries.go b/pkg/buildscript/queries.go index 0d81d8f3b0..2f4a1a6c54 100644 --- a/pkg/buildscript/queries.go +++ b/pkg/buildscript/queries.go @@ -92,6 +92,23 @@ func (b *BuildScript) Requirements() ([]Requirement, error) { return requirements, nil } +// 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() + if err != nil { + return nil, errs.Wrap(err, "Could not get requirements") + } + var deps []types.Requirement + for _, req := range reqs { + if dep, ok := req.(DependencyRequirement); ok { + deps = append(deps, dep.Requirement) + } + } + return deps, nil +} + func (b *BuildScript) getRequirementsNode() (*Value, error) { node, err := b.getSolveNode() if err != nil { diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 22ac663936..b3da5de5b9 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -431,6 +431,22 @@ scripts: ts.PrepareCommitIdFile("a9d0bc88-585a-49cf-89c1-6c07af781cff") } +func (suite *PackageIntegrationTestSuite) TestPackage_Uninstall() { + suite.OnlyRunForTags(tagsuite.Package) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI-Testing/small-python-with-pkg", "a2115792-2620-4217-89ed-b596c8c11ce3") + cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") + cp.ExpectExitCode(0) + + cp = ts.Spawn("uninstall", "requests") + // cp = ts.SpawnDebuggerForCmdWithOpts(e2e.OptArgs("uninstall", "requests")) + cp.Expect("project has been updated") // , termtest.OptExpectTimeout(600*time.Second)) + cp.ExpectExitCode(0) +} + func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { suite.OnlyRunForTags(tagsuite.Package) @@ -449,6 +465,30 @@ func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { } } +func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDupeMatch() { + suite.OnlyRunForTags(tagsuite.Package) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI-Testing/duplicate-pkg-name", "e5a15d59-9192-446a-a133-9f4c2ebe0898") + cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") + cp.ExpectExitCode(0) + + cp = ts.Spawn("uninstall", "oauth") + cp.Expect("match multiple requirements") + cp.ExpectExitCode(1) + ts.IgnoreLogErrors() + + if strings.Count(cp.Snapshot(), " x ") != 2 { // 2 because "Creating commit x Failed" is also printed + suite.Fail("Expected exactly ONE error message, got: ", cp.Snapshot()) + } + + cp = ts.Spawn("uninstall", "language/python/oauth") + cp.Expect("project has been updated") + cp.ExpectExitCode(0) +} + func (suite *PackageIntegrationTestSuite) TestJSON() { suite.OnlyRunForTags(tagsuite.Package, tagsuite.JSON) ts := e2e.New(suite.T(), false) From dabd14ae013e5eee2d64684cdd9fa6652134c62c Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 10:02:03 -0700 Subject: [PATCH 09/49] Added convenience method to run termtest command in debugger --- internal/testhelpers/e2e/session.go | 75 ++++++++++++++++++---------- test/integration/package_int_test.go | 3 +- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index 030e279c23..699e12aa8b 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -7,6 +7,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strings" "testing" "time" @@ -209,6 +210,20 @@ func (s *Session) Spawn(args ...string) *SpawnedCmd { return s.SpawnCmdWithOpts(s.Exe, OptArgs(args...)) } +func (s *Session) SpawnDebuggerForCmdWithOpts(opts ...SpawnOptSetter) *SpawnedCmd { + spawnOpts := s.newSpawnOpts(opts...) + args := slices.Clone(spawnOpts.Args) + + workDir := spawnOpts.Dir + spawnOpts.Args = []string{"debug", "--wd", workDir, "--headless", "--listen=:2345", "--api-version=2", "github.com/ActiveState/cli/cmd/state", "--"} + spawnOpts.Args = append(spawnOpts.Args, args...) + spawnOpts.Dir = environment.GetRootPathUnsafe() + + return s.SpawnCmdWithOpts("dlv", func(opts *SpawnOpts) { + *opts = spawnOpts + }) +} + // SpawnWithOpts spawns the state tool executable to be tested with arguments func (s *Session) SpawnWithOpts(opts ...SpawnOptSetter) *SpawnedCmd { return s.SpawnCmdWithOpts(s.Exe, opts...) @@ -231,33 +246,7 @@ func (s *Session) SpawnShellWithOpts(shell Shell, opts ...SpawnOptSetter) *Spawn // SpawnCmdWithOpts executes an executable in a pseudo-terminal for integration tests // Arguments and other parameters can be specified by specifying SpawnOptSetter func (s *Session) SpawnCmdWithOpts(exe string, optSetters ...SpawnOptSetter) *SpawnedCmd { - spawnOpts := NewSpawnOpts() - spawnOpts.Env = s.Env - spawnOpts.Dir = s.Dirs.Work - - spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts, - termtest.OptErrorHandler(func(tt *termtest.TermTest, err error) error { - s.T.Fatal(s.DebugMessage(errs.JoinMessage(err))) - return err - }), - termtest.OptDefaultTimeout(defaultTimeout), - termtest.OptCols(140), - termtest.OptRows(30), // Needs to be able to accommodate most JSON output - ) - - // TTYs output newlines in two steps: '\r' (CR) to move the caret to the beginning of the line, - // and '\n' (LF) to move the caret one line down. Terminal emulators do the same thing, so the - // raw terminal output will contain "\r\n". Since our multi-line expectation messages often use - // '\n', normalize line endings to that for convenience, regardless of platform ('\n' for Linux - // and macOS, "\r\n" for Windows). - // More info: https://superuser.com/a/1774370 - spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts, - termtest.OptNormalizedLineEnds(true), - ) - - for _, optSet := range optSetters { - optSet(&spawnOpts) - } + spawnOpts := s.newSpawnOpts(optSetters...) var shell string var args []string @@ -319,6 +308,38 @@ func (s *Session) SpawnCmdWithOpts(exe string, optSetters ...SpawnOptSetter) *Sp return spawn } +func (s *Session) newSpawnOpts(optSetters ...SpawnOptSetter) SpawnOpts { + spawnOpts := NewSpawnOpts() + spawnOpts.Env = s.Env + spawnOpts.Dir = s.Dirs.Work + + spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts, + termtest.OptErrorHandler(func(tt *termtest.TermTest, err error) error { + s.T.Fatal(s.DebugMessage(errs.JoinMessage(err))) + return err + }), + termtest.OptDefaultTimeout(defaultTimeout), + termtest.OptCols(140), + termtest.OptRows(30), // Needs to be able to accommodate most JSON output + ) + + // TTYs output newlines in two steps: '\r' (CR) to move the caret to the beginning of the line, + // and '\n' (LF) to move the caret one line down. Terminal emulators do the same thing, so the + // raw terminal output will contain "\r\n". Since our multi-line expectation messages often use + // '\n', normalize line endings to that for convenience, regardless of platform ('\n' for Linux + // and macOS, "\r\n" for Windows). + // More info: https://superuser.com/a/1774370 + spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts, + termtest.OptNormalizedLineEnds(true), + ) + + for _, optSet := range optSetters { + optSet(&spawnOpts) + } + + return spawnOpts +} + // PrepareActiveStateYAML creates an activestate.yaml in the session's work directory from the // given YAML contents. func (s *Session) PrepareActiveStateYAML(contents string) { diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index b3da5de5b9..77268712cb 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -442,8 +442,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_Uninstall() { cp.ExpectExitCode(0) cp = ts.Spawn("uninstall", "requests") - // cp = ts.SpawnDebuggerForCmdWithOpts(e2e.OptArgs("uninstall", "requests")) - cp.Expect("project has been updated") // , termtest.OptExpectTimeout(600*time.Second)) + cp.Expect("project has been updated") cp.ExpectExitCode(0) } From a8e99ce9e7a83e19b66f8ce29b29c06dd91bb730 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 10:08:51 -0700 Subject: [PATCH 10/49] Enforce namespace type --- internal/runners/uninstall/uninstall.go | 51 +++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/internal/runners/uninstall/uninstall.go b/internal/runners/uninstall/uninstall.go index 94d05bc9a0..731938070a 100644 --- a/internal/runners/uninstall/uninstall.go +++ b/internal/runners/uninstall/uninstall.go @@ -1,8 +1,6 @@ package uninstall import ( - "errors" - "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -10,7 +8,6 @@ import ( "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" "github.com/ActiveState/cli/internal/sliceutils" @@ -99,7 +96,7 @@ func (u *Uninstall) Run(params Params) (rerr error) { // Update buildscript script := oldCommit.BuildScript() - if err := prepareBuildScript(script, params.Packages); err != nil { + if err := u.prepareBuildScript(script, params.Packages); err != nil { return errs.Wrap(err, "Could not prepare build script") } @@ -118,42 +115,56 @@ func (u *Uninstall) Run(params Params) (rerr error) { return nil } -func prepareBuildScript(script *buildscript.BuildScript, pkgs captain.PackagesValue) error { +func (u *Uninstall) prepareBuildScript(script *buildscript.BuildScript, pkgs captain.PackagesValue) error { reqs, err := script.DependencyRequirements() if err != nil { return errs.Wrap(err, "Unable to get requirements") } - // Check that we're not matching multiple packages + // Resolve requirements and check for errors + toRemove := []types.Requirement{} + notFound := captain.PackagesValue{} multipleMatches := captain.PackagesValue{} for _, pkg := range pkgs { + // Filter matching requirements matches := sliceutils.Filter(reqs, func(req types.Requirement) bool { - return pkg.Name == req.Name && (pkg.Namespace == "" || pkg.Namespace == req.Namespace) + if pkg.Name != req.Name { + return false + } + if pkg.Namespace != "" { + return req.Namespace == pkg.Namespace + } + return model.NamespaceMatch(req.Namespace, u.nsType.Matchable()) }) + toRemove = append(toRemove, matches...) + + // Check for duplicate matches if len(matches) > 1 { multipleMatches = append(multipleMatches, pkg) } + + // Check for no matches + if len(matches) == 0 { + notFound = append(notFound, pkg) + } } + + // Error out on duplicate matches if len(multipleMatches) > 0 { return &errMultipleMatches{error: errs.New("Could not find all requested packages"), packages: multipleMatches} } + // Error out on no matches + if len(notFound) > 0 { + return &errNoMatches{error: errs.New("Could not find all requested packages"), packages: notFound} + } + // Remove requirements - var removeErrs error - notFound := captain.PackagesValue{} - for _, pkg := range pkgs { - if err := script.RemoveRequirement(types.Requirement{Name: pkg.Name, Namespace: pkg.Namespace}); err != nil { - if errors.As(err, ptr.To(&buildscript.RequirementNotFoundError{})) { - notFound = append(notFound, pkg) - removeErrs = errs.Pack(removeErrs, err) - } else { - return errs.Wrap(err, "Unable to remove requirement") - } + for _, req := range toRemove { + if err := script.RemoveRequirement(req); err != nil { + return errs.Wrap(err, "Unable to remove requirement") } } - if len(notFound) > 0 { - return errs.Pack(&errNoMatches{error: errs.New("Could not find all requested packages"), packages: notFound}, removeErrs) - } return nil } From c2aea6b290f97bd2f3462caae41e629fc8a1acaf Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 10:11:05 -0700 Subject: [PATCH 11/49] Bundle uninstall uses new runner --- cmd/state/internal/cmdtree/bundles.go | 7 ++++--- test/integration/bundle_int_test.go | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index 8351a2bc02..a04715ff88 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -6,6 +6,7 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runners/install" "github.com/ActiveState/cli/internal/runners/packages" + "github.com/ActiveState/cli/internal/runners/uninstall" "github.com/ActiveState/cli/pkg/platform/model" ) @@ -74,9 +75,9 @@ func newBundleInstallCommand(prime *primer.Values) *captain.Command { } func newBundleUninstallCommand(prime *primer.Values) *captain.Command { - runner := packages.NewUninstall(prime) + runner := uninstall.New(prime, model.NamespaceBundle) - params := packages.UninstallRunParams{} + params := uninstall.Params{} return captain.NewCommand( "uninstall", @@ -93,7 +94,7 @@ func newBundleUninstallCommand(prime *primer.Values) *captain.Command { }, }, func(_ *captain.Command, _ []string) error { - return runner.Run(params, model.NamespaceBundle) + return runner.Run(params) }, ).SetSupportsStructuredOutput() } diff --git a/test/integration/bundle_int_test.go b/test/integration/bundle_int_test.go index 6207c8f5de..d5254d71a0 100644 --- a/test/integration/bundle_int_test.go +++ b/test/integration/bundle_int_test.go @@ -36,7 +36,7 @@ func (suite *BundleIntegrationTestSuite) TestBundle_project_name_noData() { cp.ExpectExitCode(0) } -func (suite *BundleIntegrationTestSuite) TestBundle_install() { +func (suite *BundleIntegrationTestSuite) TestBundle_install_uninstall() { suite.OnlyRunForTags(tagsuite.Bundle) ts := e2e.New(suite.T(), false) defer ts.Close() @@ -48,6 +48,10 @@ func (suite *BundleIntegrationTestSuite) TestBundle_install() { cp = ts.Spawn("bundles", "install", "python-module-build-support") cp.Expect("project has been updated") cp.ExpectExitCode(0) + + cp = ts.Spawn("bundles", "uninstall", "python-module-build-support") + cp.Expect("project has been updated") + cp.ExpectExitCode(0) } func (suite *BundleIntegrationTestSuite) TestBundle_searchSimple() { From e8e9355f5282a5ae5974b4bcd7b67146004d72c6 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 11:42:28 -0700 Subject: [PATCH 12/49] Implemented new runner for `state platforms remove` --- internal/locale/locales/en-us.yaml | 9 ++ internal/runners/platforms/add.go | 7 +- internal/runners/platforms/remove.go | 124 ++++++++++++++++++++----- pkg/platform/model/inventory.go | 57 ++++++------ test/integration/platforms_int_test.go | 18 ++-- 5 files changed, 150 insertions(+), 65 deletions(-) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 48dfe399e2..211dce172e 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1596,5 +1596,14 @@ err_uninstall_multimatch: The following terms match multiple requirements in your project: [ACTIONABLE]{{.V0}}[/RESET]. Please specify the requirement to uninstall by using its full namespace. To view the namespace of your project requirements run: [ACTIONABLE]state manifest[/RESET]. +err_uninstall_platform_nomatch: + other: "The specified platform does not exist in your project" +err_uninstall_platform_multimatch: + other: | + The platform query you provided matches multiple platforms in your project. + Please specify the platform to uninstall by using its version and bit-width. + To view the platforms your project uses run: [ACTIONABLE]state platforms[/RESET]. progress_requirements: other: "• Updating requirements" +progress_platforms: + other: "• Updating platforms" diff --git a/internal/runners/platforms/add.go b/internal/runners/platforms/add.go index dcbe238fe3..3f963ee00c 100644 --- a/internal/runners/platforms/add.go +++ b/internal/runners/platforms/add.go @@ -48,16 +48,11 @@ func NewAdd(prime primeable) *Add { } // Run executes the add behavior. -func (a *Add) Run(ps AddRunParams) (rerr error) { +func (a *Add) Run(params AddRunParams) (rerr error) { defer rationalizeAddPlatformError(&rerr) logging.Debug("Execute platforms add") - params, err := prepareParams(ps.Params) - if err != nil { - return err - } - if a.prime.Project() == nil { return rationalize.ErrNoProject } diff --git a/internal/runners/platforms/remove.go b/internal/runners/platforms/remove.go index b8fe20c01f..070204744c 100644 --- a/internal/runners/platforms/remove.go +++ b/internal/runners/platforms/remove.go @@ -1,13 +1,22 @@ package platforms import ( + "errors" + + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/rationalize" - "github.com/ActiveState/cli/internal/runbits/runtime/requirements" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" + "github.com/ActiveState/cli/internal/runbits/rationalizers" + "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/pkg/localcommit" + bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/model" + bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" + "github.com/go-openapi/strfmt" ) // RemoveRunParams tracks the info required for running Remove. @@ -15,45 +24,118 @@ type RemoveRunParams struct { Params } -// Remove manages the removeing execution context. +// Remove manages the adding execution context. type Remove struct { prime primeable } -// NewRemove prepares a remove execution context for use. +// NewRemove prepares an add execution context for use. func NewRemove(prime primeable) *Remove { return &Remove{ prime: prime, } } -// Run executes the remove behavior. -func (r *Remove) Run(ps RemoveRunParams) error { +var errNoMatch = errors.New("no platform matched the search criteria") +var errMultiMatch = errors.New("multiple platforms matched the search criteria") + +// Run executes the add behavior. +func (a *Remove) Run(params RemoveRunParams) (rerr error) { + defer rationalizeRemovePlatformError(&rerr) + logging.Debug("Execute platforms remove") - if r.prime.Project() == nil { + if a.prime.Project() == nil { return rationalize.ErrNoProject } - params, err := prepareParams(ps.Params) + pj := a.prime.Project() + out := a.prime.Output() + bp := bpModel.NewBuildPlannerModel(a.prime.Auth()) + + var pg *output.Spinner + defer func() { + if pg != nil { + pg.Stop(locale.T("progress_fail")) + } + }() + + pg = output.StartSpinner(out, locale.T("progress_platforms"), constants.TerminalAnimationInterval) + + // Grab local commit info + localCommitID, err := localcommit.Get(pj.Dir()) + if err != nil { + return errs.Wrap(err, "Unable to get local commit") + } + oldCommit, err := bp.FetchCommit(localCommitID, pj.Owner(), pj.Name(), nil) + if err != nil { + return errs.Wrap(err, "Failed to fetch old build result") + } + + pg.Stop(locale.T("progress_found")) + pg = nil + + // Prepare updated buildscript + script := oldCommit.BuildScript() + platforms, err := script.Platforms() if err != nil { - return errs.Wrap(err, "Could not prepare parameters.") + return errs.Wrap(err, "Failed to get platforms") + } + toRemove := []strfmt.UUID{} + for _, uid := range platforms { + platform, err := model.FetchPlatformByUID(uid) + if err != nil { + return errs.Wrap(err, "Failed to get platform") + } + if model.IsPlatformMatch(platform, params.Platform.Name(), params.Platform.Version(), params.BitWidth) { + toRemove = append(toRemove, uid) + } + } + if len(toRemove) == 0 { + return errNoMatch + } + if len(toRemove) > 1 { + return errMultiMatch } - if err := requirements.NewRequirementOperation(r.prime).ExecuteRequirementOperation( - nil, - &requirements.Requirement{ - Name: params.resolvedName, - Version: params.resolvedVersion, - Operation: types.OperationRemoved, - BitWidth: params.BitWidth, - NamespaceType: &model.NamespacePlatform, - }, - ); err != nil { - return locale.WrapError(err, "err_remove_platform", "Could not remove platform.") + if err := script.RemovePlatform(toRemove[0]); err != nil { + return errs.Wrap(err, "Failed to remove platform") } - r.prime.Output().Notice(locale.Tr("platform_removed", params.resolvedName, params.resolvedVersion)) + // Update local checkout and source runtime changes + if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", params.Platform.String())); err != nil { + return errs.Wrap(err, "Failed to update local checkout") + } + + out.Notice(locale.Tr("platform_added", params.Platform.String())) return nil } + +func rationalizeRemovePlatformError(rerr *error) { + switch { + case rerr == nil: + return + + // No matches found + case errors.Is(*rerr, errNoMatch): + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("err_uninstall_platform_nomatch"), + errs.SetInput(), + ) + + // Multiple matches found + case errors.Is(*rerr, errMultiMatch): + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("err_uninstall_platform_multimatch"), + errs.SetInput(), + ) + + // Error staging a commit during install. + case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): + rationalizers.HandleCommitErrors(rerr) + + } +} diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index 951869db4f..7cb0c806cd 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -456,10 +456,10 @@ func FetchPlatformByUID(uid strfmt.UUID) (*Platform, error) { var ErrPlatformNotFound = errors.New("could not find platform matching provided criteria") func FetchPlatformByDetails(name, version string, bitwidth int) (*Platform, error) { - var platformID string + // For backward compatibility we still want to raise ErrPlatformNotFound due to name ID matching if version == "" && bitwidth == 0 { var err error - platformID, err = PlatformNameToPlatformID(name) + _, err = PlatformNameToPlatformID(name) if err != nil { return nil, errs.Wrap(err, "platform id from name failed") } @@ -470,40 +470,41 @@ func FetchPlatformByDetails(name, version string, bitwidth int) (*Platform, erro return nil, err } - lower := strings.ToLower - for _, rtPf := range runtimePlatforms { - if platformID != "" { - if rtPf.PlatformID.String() == platformID { - return rtPf, nil - } - continue - } - if rtPf.Kernel == nil || rtPf.Kernel.Name == nil { - continue - } - if lower(*rtPf.Kernel.Name) != lower(name) { - continue + if IsPlatformMatch(rtPf, name, version, bitwidth) { + return rtPf, nil } + } - if rtPf.KernelVersion == nil || rtPf.KernelVersion.Version == nil { - continue - } - if lower(*rtPf.KernelVersion.Version) != lower(version) { - continue - } + return nil, ErrPlatformNotFound +} - if rtPf.CPUArchitecture == nil { - continue - } - if rtPf.CPUArchitecture.BitWidth == nil || *rtPf.CPUArchitecture.BitWidth != strconv.Itoa(bitwidth) { - continue +func IsPlatformMatch(platform *Platform, name, version string, bitwidth int) bool { + var platformID string + if version == "" && bitwidth == 0 { + var err error + platformID, err = PlatformNameToPlatformID(name) + if err != nil || platformID == "" { + return false } + return platform.PlatformID.String() == platformID + } - return rtPf, nil + if platform.Kernel == nil || platform.Kernel.Name == nil || + !strings.EqualFold(*platform.Kernel.Name, name) { + return false + } + if version != "" && (platform.KernelVersion == nil || platform.KernelVersion.Version == nil || + !strings.EqualFold(*platform.KernelVersion.Version, version)) { + return false + } + if bitwidth != 0 && (platform.CPUArchitecture == nil || + platform.CPUArchitecture.BitWidth == nil || + !strings.EqualFold(*platform.CPUArchitecture.BitWidth, strconv.Itoa(bitwidth))) { + return false } - return nil, ErrPlatformNotFound + return true } func FetchLanguageForCommit(commitID strfmt.UUID, auth *authentication.Auth) (*Language, error) { diff --git a/test/integration/platforms_int_test.go b/test/integration/platforms_int_test.go index 4968a5dc59..937810f8c2 100644 --- a/test/integration/platforms_int_test.go +++ b/test/integration/platforms_int_test.go @@ -3,11 +3,13 @@ package integration import ( "fmt" "testing" + "time" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/suite" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/ActiveState/termtest" ) type PlatformsIntegrationTestSuite struct { @@ -118,18 +120,14 @@ func (suite *PlatformsIntegrationTestSuite) TestPlatforms_addRemoveLatest() { suite.Require().NotContains(output, "Windows", "Windows platform should not be present after removal") cp = ts.Spawn("platforms", "add", "windows") - cp.ExpectExitCode(0) + // cp = ts.SpawnDebuggerWithOpts(e2e.OptArgs("platforms", "add", "windows")) + cp.ExpectExitCode(0, termtest.OptExpectTimeout(10*time.Minute)) cp = ts.Spawn("platforms") - expectations := []string{ - platform, - version, - "64", - } - for _, expectation := range expectations { - cp.Expect(expectation) - } - + cp.Expect(platform) + cp.Expect(version) + cp.Expect("64") + cp.ExpectExitCode(0) } func (suite *PlatformsIntegrationTestSuite) TestPlatforms_addNotFound() { From 7dc751182062197487f556a18647f53b0474a34d Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 11:42:55 -0700 Subject: [PATCH 13/49] Fixed debug output not supporting multiple invocations of same command, and make ordering consistent --- internal/testhelpers/e2e/session.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index 699e12aa8b..5c780a145a 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -210,7 +210,7 @@ func (s *Session) Spawn(args ...string) *SpawnedCmd { return s.SpawnCmdWithOpts(s.Exe, OptArgs(args...)) } -func (s *Session) SpawnDebuggerForCmdWithOpts(opts ...SpawnOptSetter) *SpawnedCmd { +func (s *Session) SpawnDebuggerWithOpts(opts ...SpawnOptSetter) *SpawnedCmd { spawnOpts := s.newSpawnOpts(opts...) args := slices.Clone(spawnOpts.Args) @@ -490,7 +490,7 @@ func (s *Session) DebugMessage(prefix string) string { prefix = prefix + "\n" } - output := map[string]string{} + output := []string{} for _, spawn := range s.spawned { name := spawn.Cmd().String() if spawn.opts.HideCmdArgs { @@ -501,24 +501,25 @@ func (s *Session) DebugMessage(prefix string) string { // If we encountered a panic it's unlikely the snapshot has enough information to be useful, so in this // case we include the full output. Which we don't normally do as it is just the dump of pty data, and // tends to be overly verbose and difficult to grok. - output[name] = strings.TrimSpace(out) + output = append(output, fmt.Sprintf("Snapshot for Cmd '%s':\n%s", name, strings.TrimSpace(out))) } else { - output[name] = strings.TrimSpace(spawn.Snapshot()) + output = append(output, fmt.Sprintf("Snapshot for Cmd '%s':\n%s", name, strings.TrimSpace(spawn.Snapshot()))) } } + logs := []string{} + for name, log := range s.DebugLogs() { + logs = append(logs, fmt.Sprintf("Log for '%s':\n%s", name, log)) + } + v, err := strutils.ParseTemplate(` {{.Prefix}}Stack: {{.Stacktrace}} -{{range $title, $value := .Outputs}} -{{$.A}}Snapshot for Cmd '{{$title}}': -{{$value}} -{{$.Z}} +{{range $value := .Outputs}} +{{$.A}}{{$value}}{{$.Z}} {{end}} -{{range $title, $value := .Logs}} -{{$.A}}Log '{{$title}}': -{{$value}} -{{$.Z}} +{{range $value := .Logs}} +{{$.A}}{{$value}}{{$.Z}} {{else}} No logs {{end}} @@ -526,7 +527,7 @@ No logs "Prefix": prefix, "Stacktrace": stacktrace.Get().String(), "Outputs": output, - "Logs": s.DebugLogs(), + "Logs": logs, "A": sectionStart, "Z": sectionEnd, }, nil) From 3f582c4009dc4d556b96a4cde2a4937da1e44872 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 11:53:03 -0700 Subject: [PATCH 14/49] Remove debug code --- test/integration/platforms_int_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/integration/platforms_int_test.go b/test/integration/platforms_int_test.go index 937810f8c2..3b21fb75b9 100644 --- a/test/integration/platforms_int_test.go +++ b/test/integration/platforms_int_test.go @@ -3,13 +3,11 @@ package integration import ( "fmt" "testing" - "time" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/suite" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" - "github.com/ActiveState/termtest" ) type PlatformsIntegrationTestSuite struct { @@ -120,8 +118,7 @@ func (suite *PlatformsIntegrationTestSuite) TestPlatforms_addRemoveLatest() { suite.Require().NotContains(output, "Windows", "Windows platform should not be present after removal") cp = ts.Spawn("platforms", "add", "windows") - // cp = ts.SpawnDebuggerWithOpts(e2e.OptArgs("platforms", "add", "windows")) - cp.ExpectExitCode(0, termtest.OptExpectTimeout(10*time.Minute)) + cp.ExpectExitCode(0) cp = ts.Spawn("platforms") cp.Expect(platform) From 448d3432bf01d50a2822f74299965f6b8804809d Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 12:03:47 -0700 Subject: [PATCH 15/49] Drop requirement operation and clean up --- internal/locale/locales/en-us.yaml | 27 - internal/runbits/reqop_runbit/update.go | 4 +- .../runtime/requirements/rationalize.go | 91 --- .../runtime/requirements/requirements.go | 755 ------------------ internal/runbits/runtime/trigger/trigger.go | 37 +- internal/runners/install/install.go | 3 +- internal/runners/install/rationalize.go | 2 +- internal/runners/languages/install.go | 101 --- internal/runners/languages/install_test.go | 39 - internal/runners/languages/languages.go | 11 + internal/runners/packages/uninstall.go | 60 -- internal/runners/platforms/add.go | 3 +- internal/runners/platforms/remove.go | 3 +- internal/runners/uninstall/uninstall.go | 3 +- 14 files changed, 40 insertions(+), 1099 deletions(-) delete mode 100644 internal/runbits/runtime/requirements/rationalize.go delete mode 100644 internal/runbits/runtime/requirements/requirements.go delete mode 100644 internal/runners/languages/install.go delete mode 100644 internal/runners/languages/install_test.go delete mode 100644 internal/runners/packages/uninstall.go diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 211dce172e..31698718d7 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -484,18 +484,6 @@ field_version: other: Version field_bitwidth: other: Bit Width -commit_message_added_platform: - other: "Added platform: {{.V0}} {{.V1}}bit {{.V2}}" -commit_message_updated_platform: - other: "Updated platform: {{.V0}} to {{.V1}}" -commit_message_removed_platform: - other: "Removed platform: {{.V0}} {{.V1}}bit {{.V2}}" -commit_message_added_language: - other: "Added language: {{.V0}} {{.V1}}" -commit_message_updated_language: - other: "Updated language: {{.V0}} to {{.V1}}" -commit_message_removed_language: - other: "Removed language: {{.V0}} {{.V1}}" fileutils_err_amend_file: other: "Could not edit file: [NOTICE]{{.V0}}[/RESET]" err_auth_empty_token: @@ -608,12 +596,6 @@ commit_message_added: other: "Added: {{.V0}}" commit_message_removed: other: "Removed: {{.V0}}" -commit_message_added_package: - other: "Added package: {{.V0}}@{{.V1}}" -commit_message_removed_package: - other: "Removed package: {{.V0}}" -commit_message_updated_package: - other: "Updated package: {{.V0}} to {{.V1}}" commit_message_add_initial: other: Initialize project via the State Tool commit_reqstext_message: @@ -864,10 +846,6 @@ languages_install_cmd_description: other: Update the language of a project arg_languages_install_description: other: The language to update in the form of @ -err_language_format: - other: The language and version provided is not formatting correctly. It must be in the form of @ -err_language_mismatch: - other: Cannot change languages. Only changes to the current project's language version are allowed err_no_recipes: other: No build recipes could be generated for the current project err_order_unknown: @@ -1521,11 +1499,6 @@ err_headless: other: Cannot operate on a headless project. Please visit {{.V0}} to convert your project and try again. notice_needs_buildscript_reset: other: Your project is missing its buildscript file. Please run '[ACTIONABLE]state reset LOCAL[/RESET]' to recreate it. - -err_initial_no_requirement: - other: | - Could not find compatible requirement for initial commit. Please ensure that you are trying to install a valid package. - To check for available packages run '[ACTIONABLE]state search [/RESET]'. manifest_deprecation_warning: other: | [WARNING]Warning:[/RESET] This command is deprecated. Please use '[ACTIONABLE]state manifest[/RESET]' instead. diff --git a/internal/runbits/reqop_runbit/update.go b/internal/runbits/reqop_runbit/update.go index 447cb85c5b..9964a5a7c9 100644 --- a/internal/runbits/reqop_runbit/update.go +++ b/internal/runbits/reqop_runbit/update.go @@ -52,7 +52,7 @@ type Requirement struct { Version []types.VersionRequirement } -func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit *buildplanner.Commit, commitMsg string) error { +func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit *buildplanner.Commit, commitMsg string, trigger trigger.Trigger) error { pj := prime.Project() out := prime.Output() cfg := prime.Config() @@ -96,7 +96,7 @@ func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit // Start runtime sourcing UI if !cfg.GetBool(constants.AsyncRuntimeConfig) { // refresh or install runtime - _, err := runtime_runbit.Update(prime, trigger.TriggerInstall, + _, err := runtime_runbit.Update(prime, trigger, runtime_runbit.WithCommit(newCommit), runtime_runbit.WithoutBuildscriptValidation(), ) diff --git a/internal/runbits/runtime/requirements/rationalize.go b/internal/runbits/runtime/requirements/rationalize.go deleted file mode 100644 index 962eebfcf4..0000000000 --- a/internal/runbits/runtime/requirements/rationalize.go +++ /dev/null @@ -1,91 +0,0 @@ -package requirements - -import ( - "errors" - - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/runbits/rationalize" - runtime_runbit "github.com/ActiveState/cli/internal/runbits/runtime" - bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" - "github.com/ActiveState/cli/pkg/platform/model" -) - -func (r *RequirementOperation) rationalizeError(err *error) { - var tooManyMatchesErr *model.ErrTooManyMatches - var noMatchesErr *ErrNoMatches - var buildPlannerErr *bpResp.BuildPlannerError - var resolveNamespaceErr *ResolveNamespaceError - - switch { - case err == nil: - return - - // Too many matches - case errors.As(*err, &tooManyMatchesErr): - *err = errs.WrapUserFacing(*err, - locale.Tr("err_searchingredient_toomany", tooManyMatchesErr.Query), - errs.SetInput()) - - // No matches, and no alternate suggestions - case errors.As(*err, &noMatchesErr) && noMatchesErr.Alternatives == nil: - *err = errs.WrapUserFacing(*err, - locale.Tr("package_ingredient_alternatives_nosuggest", noMatchesErr.Query), - errs.SetInput()) - - // No matches, but have alternate suggestions - case errors.As(*err, &noMatchesErr) && noMatchesErr.Alternatives != nil: - *err = errs.WrapUserFacing(*err, - locale.Tr("package_ingredient_alternatives", noMatchesErr.Query, *noMatchesErr.Alternatives), - errs.SetInput()) - - // We communicate buildplanner errors verbatim as the intend is that these are curated by the buildplanner - case errors.As(*err, &buildPlannerErr): - *err = errs.WrapUserFacing(*err, - buildPlannerErr.LocaleError(), - errs.SetIf(buildPlannerErr.InputError(), errs.SetInput())) - - // Headless - case errors.Is(*err, rationalize.ErrHeadless): - *err = errs.WrapUserFacing(*err, - locale.Tl( - "err_requirement_headless", - "Cannot update requirements for a headless project. Please visit {{.V0}} to convert your project and try again.", - r.Project.URL(), - ), - errs.SetInput()) - - case errors.Is(*err, errNoRequirements): - *err = errs.WrapUserFacing(*err, - locale.Tl("err_no_requirements", - "No requirements have been provided for this operation.", - ), - errs.SetInput(), - ) - - case errors.As(*err, &resolveNamespaceErr): - *err = errs.WrapUserFacing(*err, - locale.Tl("err_resolve_namespace", - "Could not resolve namespace for requirement '{{.V0}}'.", - resolveNamespaceErr.Name, - ), - errs.SetInput(), - ) - - case errors.Is(*err, errInitialNoRequirement): - *err = errs.WrapUserFacing(*err, - locale.T("err_initial_no_requirement"), - errs.SetInput(), - ) - - case errors.Is(*err, errNoLanguage): - *err = errs.WrapUserFacing(*err, - locale.Tl("err_no_language", "Could not determine which language namespace to search for packages in. Please supply the language flag."), - errs.SetInput(), - ) - - default: - runtime_runbit.RationalizeSolveError(r.Project, r.Auth, err) - - } -} diff --git a/internal/runbits/runtime/requirements/requirements.go b/internal/runbits/runtime/requirements/requirements.go deleted file mode 100644 index f7625b5e1d..0000000000 --- a/internal/runbits/runtime/requirements/requirements.go +++ /dev/null @@ -1,755 +0,0 @@ -package requirements - -import ( - "errors" - "fmt" - "regexp" - "strconv" - "strings" - "time" - - "github.com/ActiveState/cli/internal/analytics" - anaConsts "github.com/ActiveState/cli/internal/analytics/constants" - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/output" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/prompt" - "github.com/ActiveState/cli/internal/rtutils/ptr" - buildscript_runbit "github.com/ActiveState/cli/internal/runbits/buildscript" - "github.com/ActiveState/cli/internal/runbits/cves" - "github.com/ActiveState/cli/internal/runbits/dependencies" - "github.com/ActiveState/cli/internal/runbits/rationalize" - runtime_runbit "github.com/ActiveState/cli/internal/runbits/runtime" - "github.com/ActiveState/cli/internal/runbits/runtime/trigger" - "github.com/ActiveState/cli/pkg/buildscript" - "github.com/ActiveState/cli/pkg/localcommit" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" - medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" - "github.com/ActiveState/cli/pkg/platform/authentication" - "github.com/ActiveState/cli/pkg/platform/model" - bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" - "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/cli/pkg/runtime" - "github.com/ActiveState/cli/pkg/sysinfo" - "github.com/go-openapi/strfmt" - "github.com/thoas/go-funk" -) - -type PackageVersion struct { - captain.NameVersionValue -} - -func (pv *PackageVersion) Set(arg string) error { - err := pv.NameVersionValue.Set(arg) - if err != nil { - return locale.WrapInputError(err, "err_package_format", "The package and version provided is not formatting correctly. It must be in the form of @") - } - return nil -} - -type RequirementOperation struct { - prime primeable - // The remainder is redundant with the above. Refactoring this will follow in a later story so as not to blow - // up the one that necessitates adding the primer at this level. - // https://activestatef.atlassian.net/browse/DX-2869 - Output output.Outputer - Prompt prompt.Prompter - Project *project.Project - Auth *authentication.Auth - Config *config.Instance - Analytics analytics.Dispatcher - SvcModel *model.SvcModel -} - -type primeable interface { - primer.Outputer - primer.Prompter - primer.Projecter - primer.Auther - primer.Configurer - primer.Analyticer - primer.SvcModeler -} - -func NewRequirementOperation(prime primeable) *RequirementOperation { - return &RequirementOperation{ - prime, - prime.Output(), - prime.Prompt(), - prime.Project(), - prime.Auth(), - prime.Config(), - prime.Analytics(), - prime.SvcModel(), - } -} - -const latestVersion = "latest" - -type ErrNoMatches struct { - *locale.LocalizedError - Query string - Alternatives *string -} - -var errNoRequirements = errs.New("No requirements were provided") - -var errInitialNoRequirement = errs.New("Could not find compatible requirement for initial commit") - -var errNoLanguage = errs.New("No language") - -var versionRe = regexp.MustCompile(`^\d(\.\d+)*$`) - -// Requirement represents a package, language or platform requirement -// For now, be aware that you should never provide BOTH ns AND nsType, one or the other should always be nil, but never both. -// The refactor should clean this up. -type Requirement struct { - Name string - Version string - Revision *int - BitWidth int // Only needed for platform requirements - Namespace *model.Namespace - NamespaceType *model.NamespaceType - Operation types.Operation - - // The following fields are set during execution - langName string - langVersion string - validatePkg bool - appendVersionWildcard bool - originalRequirementName string - versionRequirements []types.VersionRequirement -} - -// ExecuteRequirementOperation executes the operation on the requirement -// This has become quite unwieldy, and is ripe for a refactor - https://activestatef.atlassian.net/browse/DX-1897 -func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requirements ...*Requirement) (rerr error) { - defer r.rationalizeError(&rerr) - - if len(requirements) == 0 { - return errNoRequirements - } - - out := r.Output - var pg *output.Spinner - defer func() { - if pg != nil { - // This is a bit awkward, but it would be even more awkward to manually address this for every error condition - pg.Stop(locale.T("progress_fail")) - } - }() - - if r.Project == nil { - return rationalize.ErrNoProject - } - if r.Project.IsHeadless() { - return rationalize.ErrHeadless - } - out.Notice(locale.Tr("operating_message", r.Project.NamespaceString(), r.Project.Dir())) - - if err := r.resolveNamespaces(ts, requirements...); err != nil { - return errs.Wrap(err, "Could not resolve namespaces") - } - - if err := r.validatePackages(requirements...); err != nil { - return errs.Wrap(err, "Could not validate packages") - } - - parentCommitID, err := localcommit.Get(r.Project.Dir()) - if err != nil { - return errs.Wrap(err, "Unable to get local commit") - } - hasParentCommit := parentCommitID != "" - - pg = output.StartSpinner(r.Output, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) - - if err := r.checkForUpdate(parentCommitID, requirements...); err != nil { - return locale.WrapError(err, "err_check_for_update", "Could not check for requirements updates") - } - - if !hasParentCommit { - // Use first requirement to extract language for initial commit - var requirement *Requirement - for _, r := range requirements { - if r.Namespace.Type() == model.NamespacePackage || r.Namespace.Type() == model.NamespaceBundle { - requirement = r - break - } - } - - if requirement == nil { - return errInitialNoRequirement - } - - languageFromNs := model.LanguageFromNamespace(requirement.Namespace.String()) - parentCommitID, err = model.CommitInitial(sysinfo.OS().String(), languageFromNs, requirement.langVersion, r.Auth) - if err != nil { - return locale.WrapError(err, "err_install_no_project_commit", "Could not create initial commit for new project") - } - } - - if err := r.resolveRequirements(requirements...); err != nil { - return locale.WrapError(err, "err_resolve_requirements", "Could not resolve one or more requirements") - } - - bp := bpModel.NewBuildPlannerModel(r.Auth) - script, err := r.prepareBuildScript(bp, parentCommitID, requirements, ts) - if err != nil { - return errs.Wrap(err, "Could not prepare build script") - } - - params := bpModel.StageCommitParams{ - Owner: r.Project.Owner(), - Project: r.Project.Name(), - ParentCommit: string(parentCommitID), - Description: commitMessage(requirements...), - Script: script, - } - - // Solve runtime - commit, err := bp.StageCommit(params) - if err != nil { - return errs.Wrap(err, "Could not stage commit") - } - - ns := requirements[0].Namespace - var trig trigger.Trigger - switch ns.Type() { - case model.NamespaceLanguage: - trig = trigger.TriggerLanguage - case model.NamespacePlatform: - trig = trigger.TriggerPlatform - default: - trig = trigger.TriggerPackage - } - - oldCommit, err := bp.FetchCommit(parentCommitID, r.Project.Owner(), r.Project.Name(), nil) - if err != nil { - return errs.Wrap(err, "Failed to fetch old build result") - } - - pg.Stop(locale.T("progress_success")) - pg = nil - - dependencies.OutputChangeSummary(r.prime.Output(), commit.BuildPlan(), oldCommit.BuildPlan()) - - // Report CVEs - if err := cves.NewCveReport(r.prime).Report(commit.BuildPlan(), oldCommit.BuildPlan()); err != nil { - return errs.Wrap(err, "Could not report CVEs") - } - - // Start runtime update UI - if !r.Config.GetBool(constants.AsyncRuntimeConfig) { - out.Notice("") - - // refresh or install runtime - _, err = runtime_runbit.Update(r.prime, trig, - runtime_runbit.WithCommit(commit), - runtime_runbit.WithoutBuildscriptValidation(), - ) - if err != nil { - if !IsBuildError(err) { - // If the error is not a build error we want to retain the changes - if err2 := r.updateCommitID(commit.CommitID); err2 != nil { - return errs.Pack(err, locale.WrapError(err2, "err_package_update_commit_id")) - } - } - return errs.Wrap(err, "Failed to refresh runtime") - } - } - - if err := r.updateCommitID(commit.CommitID); err != nil { - return locale.WrapError(err, "err_package_update_commit_id") - } - - if !hasParentCommit { - out.Notice(locale.Tr("install_initial_success", r.Project.Source().Path())) - } - - // Print the result - r.outputResults(requirements...) - - out.Notice(locale.T("operation_success_local")) - - return nil -} - -func (r *RequirementOperation) prepareBuildScript(bp *bpModel.BuildPlanner, parentCommit strfmt.UUID, requirements []*Requirement, ts *time.Time) (*buildscript.BuildScript, error) { - script, err := bp.GetBuildScript(string(parentCommit)) - if err != nil { - return nil, errs.Wrap(err, "Failed to get build expression") - } - - if ts != nil { - script.SetAtTime(*ts) - } else { - // If no atTime was provided then we need to ensure that the atTime in the script is updated to use - // the most recent, which is either the current value or the platform latest. - latest, err := model.FetchLatestTimeStamp(r.Auth) - if err != nil { - return nil, errs.Wrap(err, "Unable to fetch latest Platform timestamp") - } - atTime := script.AtTime() - if atTime == nil || latest.After(*atTime) { - script.SetAtTime(latest) - } - } - - for _, req := range requirements { - if req.Namespace.String() == types.NamespacePlatform { - err = script.UpdatePlatform(req.Operation, strfmt.UUID(req.Name)) - if err != nil { - return nil, errs.Wrap(err, "Failed to update build expression with platform") - } - } else { - requirement := types.Requirement{ - Namespace: req.Namespace.String(), - Name: req.Name, - VersionRequirement: req.versionRequirements, - Revision: req.Revision, - } - - err = script.UpdateRequirement(req.Operation, requirement) - if err != nil { - return nil, errs.Wrap(err, "Failed to update build expression with requirement") - } - } - } - - return script, nil -} - -type ResolveNamespaceError struct { - Name string -} - -func (e ResolveNamespaceError) Error() string { - return "unable to resolve namespace" -} - -func (r *RequirementOperation) resolveNamespaces(ts *time.Time, requirements ...*Requirement) error { - for _, requirement := range requirements { - if err := r.resolveNamespace(ts, requirement); err != nil { - if err != errNoLanguage { - err = errs.Pack(err, &ResolveNamespaceError{requirement.Name}) - } - return errs.Wrap(err, "Unable to resolve namespace") - } - } - return nil -} - -func (r *RequirementOperation) resolveNamespace(ts *time.Time, requirement *Requirement) error { - requirement.langName = "undetermined" - - if requirement.NamespaceType != nil { - switch *requirement.NamespaceType { - case model.NamespacePackage, model.NamespaceBundle: - commitID, err := localcommit.Get(r.Project.Dir()) - if err != nil { - return errs.Wrap(err, "Unable to get local commit") - } - - language, err := model.LanguageByCommit(commitID, r.Auth) - if err != nil { - logging.Debug("Could not get language from project: %v", err) - } - if language.Name == "" { - return errNoLanguage - } - requirement.langName = language.Name - requirement.Namespace = ptr.To(model.NewNamespacePkgOrBundle(requirement.langName, *requirement.NamespaceType)) - case model.NamespaceLanguage: - requirement.Namespace = ptr.To(model.NewNamespaceLanguage()) - case model.NamespacePlatform: - requirement.Namespace = ptr.To(model.NewNamespacePlatform()) - } - } - - ns := requirement.Namespace - nsType := requirement.NamespaceType - requirement.validatePkg = requirement.Operation == types.OperationAdded && ns != nil && (ns.Type() == model.NamespacePackage || ns.Type() == model.NamespaceBundle || ns.Type() == model.NamespaceLanguage) - if (ns == nil || !ns.IsValid()) && nsType != nil && (*nsType == model.NamespacePackage || *nsType == model.NamespaceBundle) { - pg := output.StartSpinner(r.Output, locale.Tr("progress_pkg_nolang", requirement.Name), constants.TerminalAnimationInterval) - - supported, err := model.FetchSupportedLanguages(sysinfo.OS().String()) - if err != nil { - return errs.Wrap(err, "Failed to retrieve the list of supported languages") - } - - var nsv model.Namespace - var supportedLang *medmodel.SupportedLanguage - requirement.Name, nsv, supportedLang, err = resolvePkgAndNamespace(r.Prompt, requirement.Name, *requirement.NamespaceType, supported, ts, r.Auth) - if err != nil { - return errs.Wrap(err, "Could not resolve pkg and namespace") - } - requirement.Namespace = &nsv - requirement.langVersion = supportedLang.DefaultVersion - requirement.langName = supportedLang.Name - - requirement.validatePkg = false - - pg.Stop(locale.T("progress_found")) - } - - if requirement.Namespace == nil { - return locale.NewError("err_package_invalid_namespace_detected", "No valid namespace could be detected") - } - - return nil -} - -func (r *RequirementOperation) validatePackages(requirements ...*Requirement) error { - var requirementsToValidate []*Requirement - for _, requirement := range requirements { - if !requirement.validatePkg { - continue - } - requirementsToValidate = append(requirementsToValidate, requirement) - } - - if len(requirementsToValidate) == 0 { - return nil - } - - pg := output.StartSpinner(r.Output, locale.Tr("progress_search", strings.Join(requirementNames(requirementsToValidate...), ", ")), constants.TerminalAnimationInterval) - for _, requirement := range requirementsToValidate { - if err := r.validatePackage(requirement); err != nil { - return errs.Wrap(err, "Could not validate package") - } - } - pg.Stop(locale.T("progress_found")) - - return nil -} - -func (r *RequirementOperation) validatePackage(requirement *Requirement) error { - if strings.ToLower(requirement.Version) == latestVersion { - requirement.Version = "" - } - - requirement.originalRequirementName = requirement.Name - normalized, err := model.FetchNormalizedName(*requirement.Namespace, requirement.Name, r.Auth) - if err != nil { - multilog.Error("Failed to normalize '%s': %v", requirement.Name, err) - } - - packages, err := model.SearchIngredientsStrict(requirement.Namespace.String(), normalized, false, false, nil, r.Auth) // ideally case-sensitive would be true (PB-4371) - if err != nil { - return locale.WrapError(err, "package_err_cannot_obtain_search_results") - } - - if len(packages) == 0 { - suggestions, err := getSuggestions(*requirement.Namespace, requirement.Name, r.Auth) - if err != nil { - multilog.Error("Failed to retrieve suggestions: %v", err) - } - - if len(suggestions) == 0 { - return &ErrNoMatches{ - locale.WrapExternalError(err, "package_ingredient_alternatives_nosuggest", "", requirement.Name), - requirement.Name, nil} - } - - return &ErrNoMatches{ - locale.WrapExternalError(err, "package_ingredient_alternatives", "", requirement.Name, strings.Join(suggestions, "\n")), - requirement.Name, ptr.To(strings.Join(suggestions, "\n"))} - } - - if normalized != "" && normalized != requirement.Name { - requirement.Name = normalized - } - - // If a bare version number was given, and if it is a partial version number (e.g. requests@2), - // we'll want to ultimately append a '.x' suffix. - if versionRe.MatchString(requirement.Version) { - for _, knownVersion := range packages[0].Versions { - if knownVersion.Version == requirement.Version { - break - } else if strings.HasPrefix(knownVersion.Version, requirement.Version) { - requirement.appendVersionWildcard = true - } - } - } - - return nil -} - -func (r *RequirementOperation) checkForUpdate(parentCommitID strfmt.UUID, requirements ...*Requirement) error { - for _, requirement := range requirements { - // Check if this is an addition or an update - if requirement.Operation == types.OperationAdded && parentCommitID != "" { - req, err := model.GetRequirement(parentCommitID, *requirement.Namespace, requirement.Name, r.Auth) - if err != nil { - return errs.Wrap(err, "Could not get requirement") - } - if req != nil { - requirement.Operation = types.OperationUpdated - } - } - - r.Analytics.EventWithLabel( - anaConsts.CatPackageOp, fmt.Sprintf("%s-%s", requirement.Operation, requirement.langName), requirement.Name, - ) - } - - return nil -} - -func (r *RequirementOperation) resolveRequirements(requirements ...*Requirement) error { - for _, requirement := range requirements { - if err := r.resolveRequirement(requirement); err != nil { - return errs.Wrap(err, "Could not resolve requirement") - } - } - return nil -} - -func (r *RequirementOperation) resolveRequirement(requirement *Requirement) error { - var err error - requirement.Name, requirement.Version, err = model.ResolveRequirementNameAndVersion(requirement.Name, requirement.Version, requirement.BitWidth, *requirement.Namespace, r.Auth) - if err != nil { - return errs.Wrap(err, "Could not resolve requirement name and version") - } - - versionString := requirement.Version - if requirement.appendVersionWildcard { - versionString += ".x" - } - - requirement.versionRequirements, err = bpModel.VersionStringToRequirements(versionString) - if err != nil { - return errs.Wrap(err, "Could not process version string into requirements") - } - - return nil -} - -func (r *RequirementOperation) updateCommitID(commitID strfmt.UUID) error { - if err := localcommit.Set(r.Project.Dir(), commitID.String()); err != nil { - return locale.WrapError(err, "err_package_update_commit_id") - } - - if r.Config.GetBool(constants.OptinBuildscriptsConfig) { - bp := bpModel.NewBuildPlannerModel(r.Auth) - script, err := bp.GetBuildScript(commitID.String()) - if err != nil { - return errs.Wrap(err, "Could not get remote build expr and time") - } - - err = buildscript_runbit.Update(r.Project, script) - if err != nil { - return locale.WrapError(err, "err_update_build_script") - } - } - - return nil -} - -func (r *RequirementOperation) outputResults(requirements ...*Requirement) { - for _, requirement := range requirements { - r.outputResult(requirement) - } -} - -func (r *RequirementOperation) outputResult(requirement *Requirement) { - // Print the result - message := locale.Tr(fmt.Sprintf("%s_version_%s", requirement.Namespace.Type(), requirement.Operation), requirement.Name, requirement.Version) - if requirement.Version == "" { - message = locale.Tr(fmt.Sprintf("%s_%s", requirement.Namespace.Type(), requirement.Operation), requirement.Name) - } - - r.Output.Print(output.Prepare( - message, - &struct { - Name string `json:"name"` - Version string `json:"version,omitempty"` - Type string `json:"type"` - Operation string `json:"operation"` - }{ - requirement.Name, - requirement.Version, - requirement.Namespace.Type().String(), - requirement.Operation.String(), - })) - - if requirement.originalRequirementName != requirement.Name && requirement.Operation != types.OperationRemoved { - r.Output.Notice(locale.Tl("package_version_differs", - "Note: the actual package name ({{.V0}}) is different from the requested package name ({{.V1}})", - requirement.Name, requirement.originalRequirementName)) - } -} - -func supportedLanguageByName(supported []medmodel.SupportedLanguage, langName string) medmodel.SupportedLanguage { - return funk.Find(supported, func(l medmodel.SupportedLanguage) bool { return l.Name == langName }).(medmodel.SupportedLanguage) -} - -func resolvePkgAndNamespace(prompt prompt.Prompter, packageName string, nsType model.NamespaceType, supported []medmodel.SupportedLanguage, ts *time.Time, auth *authentication.Auth) (string, model.Namespace, *medmodel.SupportedLanguage, error) { - ns := model.NewBlankNamespace() - - // Find ingredients that match the input query - ingredients, err := model.SearchIngredientsStrict("", packageName, false, false, ts, auth) - if err != nil { - return "", ns, nil, locale.WrapError(err, "err_pkgop_search_err", "Failed to check for ingredients.") - } - - ingredients, err = model.FilterSupportedIngredients(supported, ingredients) - if err != nil { - return "", ns, nil, errs.Wrap(err, "Failed to filter out unsupported packages") - } - - choices := []string{} - values := map[string][]string{} - for _, i := range ingredients { - language := model.LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) - - // Generate ingredient choices to present to the user - name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, language) - choices = append(choices, name) - values[name] = []string{*i.Ingredient.Name, language} - } - - if len(choices) == 0 { - return "", ns, nil, locale.WrapExternalError(err, "package_ingredient_alternatives_nolang", "", packageName) - } - - // If we only have one ingredient match we're done; return it. - if len(choices) == 1 { - language := values[choices[0]][1] - supportedLang := supportedLanguageByName(supported, language) - return values[choices[0]][0], model.NewNamespacePkgOrBundle(language, nsType), &supportedLang, nil - } - - // Prompt the user with the ingredient choices - choice, err := prompt.Select( - locale.Tl("prompt_pkgop_ingredient", "Multiple Matches"), - locale.Tl("prompt_pkgop_ingredient_msg", "Your query has multiple matches. Which one would you like to use?"), - choices, &choices[0], - ) - if err != nil { - return "", ns, nil, locale.WrapError(err, "err_pkgop_select", "Need a selection.") - } - - // Return the user selected ingredient - language := values[choice][1] - supportedLang := supportedLanguageByName(supported, language) - return values[choice][0], model.NewNamespacePkgOrBundle(language, nsType), &supportedLang, nil -} - -func getSuggestions(ns model.Namespace, name string, auth *authentication.Auth) ([]string, error) { - results, err := model.SearchIngredients(ns.String(), name, false, nil, auth) - if err != nil { - return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", name) - } - - maxResults := 5 - if len(results) > maxResults { - results = results[:maxResults] - } - - suggestions := make([]string, 0, maxResults+1) - for _, result := range results { - suggestions = append(suggestions, fmt.Sprintf(" - %s", *result.Ingredient.Name)) - } - - return suggestions, nil -} - -func commitMessage(requirements ...*Requirement) string { - switch len(requirements) { - case 0: - return "" - case 1: - return requirementCommitMessage(requirements[0]) - default: - return commitMessageMultiple(requirements...) - } -} - -func requirementCommitMessage(req *Requirement) string { - switch req.Namespace.Type() { - case model.NamespaceLanguage: - return languageCommitMessage(req.Operation, req.Name, req.Version) - case model.NamespacePlatform: - return platformCommitMessage(req.Operation, req.Name, req.Version, req.BitWidth) - case model.NamespacePackage, model.NamespaceBundle: - return packageCommitMessage(req.Operation, req.Name, req.Version) - } - return "" -} - -func languageCommitMessage(op types.Operation, name, version string) string { - var msgL10nKey string - switch op { - case types.OperationAdded: - msgL10nKey = "commit_message_added_language" - case types.OperationUpdated: - msgL10nKey = "commit_message_updated_language" - case types.OperationRemoved: - msgL10nKey = "commit_message_removed_language" - } - - return locale.Tr(msgL10nKey, name, version) -} - -func platformCommitMessage(op types.Operation, name, version string, word int) string { - var msgL10nKey string - switch op { - case types.OperationAdded: - msgL10nKey = "commit_message_added_platform" - case types.OperationUpdated: - msgL10nKey = "commit_message_updated_platform" - case types.OperationRemoved: - msgL10nKey = "commit_message_removed_platform" - } - - return locale.Tr(msgL10nKey, name, strconv.Itoa(word), version) -} - -func packageCommitMessage(op types.Operation, name, version string) string { - var msgL10nKey string - switch op { - case types.OperationAdded: - msgL10nKey = "commit_message_added_package" - case types.OperationUpdated: - msgL10nKey = "commit_message_updated_package" - case types.OperationRemoved: - msgL10nKey = "commit_message_removed_package" - } - - if version == "" { - version = locale.Tl("package_version_auto", "auto") - } - return locale.Tr(msgL10nKey, name, version) -} - -func commitMessageMultiple(requirements ...*Requirement) string { - var commitDetails []string - for _, req := range requirements { - commitDetails = append(commitDetails, requirementCommitMessage(req)) - } - - return locale.Tl("commit_message_multiple", "Committing changes to multiple requirements: {{.V0}}", strings.Join(commitDetails, ", ")) -} - -func requirementNames(requirements ...*Requirement) []string { - var names []string - for _, requirement := range requirements { - names = append(names, requirement.Name) - } - return names -} - -func IsBuildError(err error) bool { - var errBuild *runtime.BuildError - var errBuildPlanner *response.BuildPlannerError - - return errors.As(err, &errBuild) || errors.As(err, &errBuildPlanner) -} diff --git a/internal/runbits/runtime/trigger/trigger.go b/internal/runbits/runtime/trigger/trigger.go index a64f2aa1a5..80c752d9b7 100644 --- a/internal/runbits/runtime/trigger/trigger.go +++ b/internal/runbits/runtime/trigger/trigger.go @@ -11,25 +11,24 @@ func (t Trigger) String() string { } const ( - TriggerActivate Trigger = "activate" - TriggerScript Trigger = "script" - TriggerDeploy Trigger = "deploy" - TriggerExec Trigger = "exec-cmd" - TriggerExecutor Trigger = "exec" - TriggerSwitch Trigger = "switch" - TriggerImport Trigger = "import" - TriggerInit Trigger = "init" - TriggerPackage Trigger = "package" - TriggerLanguage Trigger = "language" - TriggerPlatform Trigger = "platform" - TriggerPull Trigger = "pull" - TriggerRefresh Trigger = "refresh" - TriggerReset Trigger = "reset" - TriggerRevert Trigger = "revert" - TriggerShell Trigger = "shell" - TriggerCheckout Trigger = "checkout" - TriggerUse Trigger = "use" - TriggerInstall Trigger = "install" + TriggerActivate Trigger = "activate" + TriggerScript Trigger = "script" + TriggerDeploy Trigger = "deploy" + TriggerExec Trigger = "exec-cmd" + TriggerExecutor Trigger = "exec" + TriggerSwitch Trigger = "switch" + TriggerImport Trigger = "import" + TriggerInit Trigger = "init" + TriggerPlatform Trigger = "platform" + TriggerPull Trigger = "pull" + TriggerRefresh Trigger = "refresh" + TriggerReset Trigger = "reset" + TriggerRevert Trigger = "revert" + TriggerShell Trigger = "shell" + TriggerCheckout Trigger = "checkout" + TriggerUse Trigger = "use" + TriggerInstall Trigger = "install" + TriggerUninstall Trigger = "uninstall" ) func NewExecTrigger(cmd string) Trigger { diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index d0aaa90432..c723e66057 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -17,6 +17,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/commits_runbit" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/internal/sliceutils" "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" @@ -149,7 +150,7 @@ func (i *Install) Run(params Params) (rerr error) { } // Update local checkout and source runtime changes - if err := reqop_runbit.UpdateAndReload(i.prime, script, oldCommit, locale.Tr("commit_message_added", reqs.String())); err != nil { + if err := reqop_runbit.UpdateAndReload(i.prime, script, oldCommit, locale.Tr("commit_message_added", reqs.String()), trigger.TriggerInstall); err != nil { return errs.Wrap(err, "Failed to update local checkout") } diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go index ecad652a7d..4f92736b7b 100644 --- a/internal/runners/install/rationalize.go +++ b/internal/runners/install/rationalize.go @@ -54,7 +54,7 @@ func (i *Install) rationalizeError(rerr *error) { func (i *Install) getSuggestions(req *requirement, languages []model.Language) ([]string, error) { ingredients, err := model.SearchIngredients(req.input.Namespace, req.input.Name, false, nil, i.prime.Auth()) if err != nil { - return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", req.input.Name) + return []string{}, locale.WrapError(err, "err_package_ingredient_search", "Failed to resolve ingredient named: {{.V0}}", req.input.Name) } // Filter out irrelevant ingredients diff --git a/internal/runners/languages/install.go b/internal/runners/languages/install.go deleted file mode 100644 index d2f208dae6..0000000000 --- a/internal/runners/languages/install.go +++ /dev/null @@ -1,101 +0,0 @@ -package languages - -import ( - "strings" - - "github.com/ActiveState/cli/internal/runbits/runtime/requirements" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" - - "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/primer" - "github.com/ActiveState/cli/internal/runbits/rationalize" - "github.com/ActiveState/cli/pkg/platform/authentication" - "github.com/ActiveState/cli/pkg/platform/model" - "github.com/ActiveState/cli/pkg/project" -) - -type Update struct { - prime primeable -} - -type primeable interface { - primer.Outputer - primer.Prompter - primer.Projecter - primer.Auther - primer.Configurer - primer.Analyticer - primer.SvcModeler -} - -func NewUpdate(prime primeable) *Update { - return &Update{ - prime: prime, - } -} - -type UpdateParams struct { - Language string -} - -func (u *Update) Run(params *UpdateParams) error { - lang, err := parseLanguage(params.Language) - if err != nil { - return err - } - - if u.prime.Project() == nil { - return rationalize.ErrNoProject - } - - err = ensureLanguageProject(lang, u.prime.Project(), u.prime.Auth()) - if err != nil { - return err - } - - op := requirements.NewRequirementOperation(u.prime) - return op.ExecuteRequirementOperation(nil, &requirements.Requirement{ - Name: lang.Name, - Version: lang.Version, - NamespaceType: &model.NamespaceLanguage, - Operation: types.OperationAdded, - }) -} - -func parseLanguage(langName string) (*model.Language, error) { - if !strings.Contains(langName, "@") { - return &model.Language{ - Name: langName, - Version: "", - }, nil - } - - split := strings.Split(langName, "@") - if len(split) != 2 { - return nil, locale.NewError("err_language_format") - } - name := split[0] - version := split[1] - - return &model.Language{ - Name: name, - Version: version, - }, nil -} - -func ensureLanguageProject(language *model.Language, project *project.Project, auth *authentication.Auth) error { - targetCommitID, err := model.BranchCommitID(project.Owner(), project.Name(), project.BranchName()) - if err != nil { - return err - } - - platformLanguage, err := model.FetchLanguageForCommit(*targetCommitID, auth) - if err != nil { - return err - } - - if platformLanguage.Name != language.Name { - return locale.NewInputError("err_language_mismatch") - } - return nil -} diff --git a/internal/runners/languages/install_test.go b/internal/runners/languages/install_test.go deleted file mode 100644 index b41d0f6aaa..0000000000 --- a/internal/runners/languages/install_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package languages - -import ( - "reflect" - "testing" - - "github.com/ActiveState/cli/pkg/platform/model" -) - -func Test_parseLanguage(t *testing.T) { - type args struct { - langName string - } - tests := []struct { - name string - args args - want *model.Language - wantErr bool - }{ - { - "Language with version", - args{"Python@2"}, - &model.Language{Name: "Python", Version: "2"}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseLanguage(tt.args.langName) - if (err != nil) != tt.wantErr { - t.Errorf("parseLanguage() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseLanguage() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/runners/languages/languages.go b/internal/runners/languages/languages.go index c890c2a3e5..47dbae1864 100644 --- a/internal/runners/languages/languages.go +++ b/internal/runners/languages/languages.go @@ -7,6 +7,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/pkg/buildplan" "github.com/ActiveState/cli/pkg/localcommit" @@ -16,6 +17,16 @@ import ( "github.com/ActiveState/cli/pkg/project" ) +type primeable interface { + primer.Outputer + primer.Prompter + primer.Projecter + primer.Auther + primer.Configurer + primer.Analyticer + primer.SvcModeler +} + // Languages manages the listing execution context. type Languages struct { out output.Outputer diff --git a/internal/runners/packages/uninstall.go b/internal/runners/packages/uninstall.go deleted file mode 100644 index 6deeccae73..0000000000 --- a/internal/runners/packages/uninstall.go +++ /dev/null @@ -1,60 +0,0 @@ -package packages - -import ( - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/internal/errs" - "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/ActiveState/cli/internal/runbits/commits_runbit" - "github.com/ActiveState/cli/internal/runbits/rationalize" - "github.com/ActiveState/cli/internal/runbits/runtime/requirements" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" - "github.com/ActiveState/cli/pkg/platform/model" -) - -// UninstallRunParams tracks the info required for running Uninstall. -type UninstallRunParams struct { - Packages captain.PackagesValueNoVersion -} - -// Uninstall manages the uninstalling execution context. -type Uninstall struct { - prime primeable -} - -// NewUninstall prepares an uninstallation execution context for use. -func NewUninstall(prime primeable) *Uninstall { - return &Uninstall{prime} -} - -// Run executes the uninstall behavior. -func (u *Uninstall) Run(params UninstallRunParams, nsType model.NamespaceType) (rerr error) { - defer rationalizeError(u.prime.Auth(), &rerr) - logging.Debug("ExecuteUninstall") - if u.prime.Project() == nil { - return rationalize.ErrNoProject - } - - var reqs []*requirements.Requirement - for _, p := range params.Packages { - req := &requirements.Requirement{ - Name: p.Name, - Operation: types.OperationRemoved, - } - - if p.Namespace != "" { - req.Namespace = ptr.To(model.NewNamespaceRaw(p.Namespace)) - } else { - req.NamespaceType = &nsType - } - - reqs = append(reqs, req) - } - - ts, err := commits_runbit.ExpandTimeForProject(&captain.TimeValue{}, u.prime.Auth(), u.prime.Project()) - if err != nil { - return errs.Wrap(err, "Unable to get timestamp from params") - } - - return requirements.NewRequirementOperation(u.prime).ExecuteRequirementOperation(&ts, reqs...) -} diff --git a/internal/runners/platforms/add.go b/internal/runners/platforms/add.go index 3f963ee00c..3cc02607eb 100644 --- a/internal/runners/platforms/add.go +++ b/internal/runners/platforms/add.go @@ -14,6 +14,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/rationalizers" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/pkg/localcommit" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/model" @@ -101,7 +102,7 @@ func (a *Add) Run(params AddRunParams) (rerr error) { script.AddPlatform(*platform.PlatformID) // Update local checkout and source runtime changes - if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", *platform.DisplayName)); err != nil { + if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", *platform.DisplayName), trigger.TriggerPlatform); err != nil { return errs.Wrap(err, "Failed to update local checkout") } diff --git a/internal/runners/platforms/remove.go b/internal/runners/platforms/remove.go index 070204744c..a4cd094e54 100644 --- a/internal/runners/platforms/remove.go +++ b/internal/runners/platforms/remove.go @@ -12,6 +12,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/rationalizers" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/pkg/localcommit" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/model" @@ -103,7 +104,7 @@ func (a *Remove) Run(params RemoveRunParams) (rerr error) { } // Update local checkout and source runtime changes - if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", params.Platform.String())); err != nil { + if err := reqop_runbit.UpdateAndReload(a.prime, script, oldCommit, locale.Tr("commit_message_added", params.Platform.String()), trigger.TriggerPlatform); err != nil { return errs.Wrap(err, "Failed to update local checkout") } diff --git a/internal/runners/uninstall/uninstall.go b/internal/runners/uninstall/uninstall.go index 731938070a..c443af1237 100644 --- a/internal/runners/uninstall/uninstall.go +++ b/internal/runners/uninstall/uninstall.go @@ -10,6 +10,7 @@ import ( "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/runbits/reqop_runbit" + "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/internal/sliceutils" "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" @@ -105,7 +106,7 @@ func (u *Uninstall) Run(params Params) (rerr error) { pg = nil // Update local checkout and source runtime changes - if err := reqop_runbit.UpdateAndReload(u.prime, script, oldCommit, locale.Tr("commit_message_added", params.Packages.String())); err != nil { + if err := reqop_runbit.UpdateAndReload(u.prime, script, oldCommit, locale.Tr("commit_message_added", params.Packages.String()), trigger.TriggerUninstall); err != nil { return errs.Wrap(err, "Failed to update local checkout") } From 6b825d75a9363074fec108fbdfa9f816d568fd3f Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 13:51:54 -0700 Subject: [PATCH 16/49] Add test for ParseNamespace --- pkg/platform/model/vcs.go | 2 +- pkg/platform/model/vcs_test.go | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 7aef7f2fb4..12b78d1941 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -83,7 +83,7 @@ const ( NamespaceCamelFlagsMatch = `^camel-flags$` // NamespaceOrgMatch is the namespace used for org specific requirements - NamespaceOrgMatch = `^org\/` + NamespaceOrgMatch = `^private\/` // NamespaceBuildFlagsMatch is the namespace used for passing build flags NamespaceBuildFlagsMatch = `^build-flags$` diff --git a/pkg/platform/model/vcs_test.go b/pkg/platform/model/vcs_test.go index 15483e8180..944d4f85a4 100644 --- a/pkg/platform/model/vcs_test.go +++ b/pkg/platform/model/vcs_test.go @@ -161,3 +161,42 @@ func (suite *VCSTestSuite) TestVersionStringToConstraints() { func TestVCSTestSuite(t *testing.T) { suite.Run(t, new(VCSTestSuite)) } + +func TestParseNamespace(t *testing.T) { + tests := []struct { + ns string + want NamespaceType + }{ + { + "language/python", + NamespacePackage, + }, + { + "bundles/python", + NamespaceBundle, + }, + { + "language", + NamespaceLanguage, + }, + { + "platform", + NamespacePlatform, + }, + { + "private/org", + NamespaceOrg, + }, + { + "raw/foo/bar", + NamespaceRaw, + }, + } + for _, tt := range tests { + t.Run(tt.ns, func(t *testing.T) { + if got := ParseNamespace(tt.ns); got.Type().name != tt.want.name { + t.Errorf("ParseNamespace() = %v, want %v", got.Type().name, tt.want.name) + } + }) + } +} From 05927a650fadd0058a5135b43a5a576cea7fad0a Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 14:47:14 -0700 Subject: [PATCH 17/49] Fix bundle uninstall not working --- cmd/state/internal/cmdtree/bundles.go | 7 ++++++- internal/runbits/rationalizers/commit.go | 2 +- pkg/buildscript/mutations.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cmd/state/internal/cmdtree/bundles.go b/cmd/state/internal/cmdtree/bundles.go index a04715ff88..e71be1e68c 100644 --- a/cmd/state/internal/cmdtree/bundles.go +++ b/cmd/state/internal/cmdtree/bundles.go @@ -93,7 +93,12 @@ func newBundleUninstallCommand(prime *primer.Values) *captain.Command { Required: true, }, }, - func(_ *captain.Command, _ []string) error { + func(_ *captain.Command, args []string) error { + for _, p := range args { + if _, err := params.Packages.Add(p); err != nil { + return locale.WrapInputError(err, "err_uninstall_packages_args", "Invalid package uninstall arguments") + } + } return runner.Run(params) }, ).SetSupportsStructuredOutput() diff --git a/internal/runbits/rationalizers/commit.go b/internal/runbits/rationalizers/commit.go index dfa26018b0..eff0a3a151 100644 --- a/internal/runbits/rationalizers/commit.go +++ b/internal/runbits/rationalizers/commit.go @@ -34,7 +34,7 @@ func HandleCommitErrors(rerr *error) { ) case types.NoChangeSinceLastCommitErrorType: *rerr = errs.WrapUserFacing(*rerr, - locale.Tl("err_packages_exist", "The requested package is already installed."), + locale.Tl("err_commit_nochanges", "There are no changes since the last commit."), errs.SetInput(), ) default: diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index de3bba9169..f1a6992d89 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -99,7 +99,7 @@ func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { break } } - if arg.Assignment.Key == requirementNamespaceKey { + if requirement.Namespace != "" && arg.Assignment.Key == requirementNamespaceKey { match = strValue(arg.Assignment.Value) == requirement.Namespace if !match { break From c8ef37f1934a58bc04ce0644313d47459bfc5b10 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Tue, 10 Sep 2024 14:47:28 -0700 Subject: [PATCH 18/49] Centralize commit error handling --- internal/runners/packages/rationalize.go | 32 ++---------------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/internal/runners/packages/rationalize.go b/internal/runners/packages/rationalize.go index b96ba8a285..ccf6503943 100644 --- a/internal/runners/packages/rationalize.go +++ b/internal/runners/packages/rationalize.go @@ -6,9 +6,9 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/runbits/rationalize" + "github.com/ActiveState/cli/internal/runbits/rationalizers" "github.com/ActiveState/cli/pkg/buildscript" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" - "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" "github.com/ActiveState/cli/pkg/platform/authentication" ) @@ -29,35 +29,7 @@ func rationalizeError(auth *authentication.Auth, err *error) { // Error staging a commit during install. case errors.As(*err, &commitError): - switch commitError.Type { - case types.NotFoundErrorType: - *err = errs.WrapUserFacing(*err, - locale.Tl("err_packages_not_found", "Could not make runtime changes because your project was not found."), - errs.SetInput(), - errs.SetTips(locale.T("tip_private_project_auth")), - ) - case types.ForbiddenErrorType: - *err = errs.WrapUserFacing(*err, - locale.Tl("err_packages_forbidden", "Could not make runtime changes because you do not have permission to do so."), - errs.SetInput(), - errs.SetTips(locale.T("tip_private_project_auth")), - ) - case types.HeadOnBranchMovedErrorType: - *err = errs.WrapUserFacing(*err, - locale.T("err_buildplanner_head_on_branch_moved"), - errs.SetInput(), - ) - case types.NoChangeSinceLastCommitErrorType: - *err = errs.WrapUserFacing(*err, - locale.Tl("err_packages_exist", "The requested package(s) is already installed."), - errs.SetInput(), - ) - default: - *err = errs.WrapUserFacing(*err, - locale.Tl("err_packages_buildplanner_error", "Could not make runtime changes due to the following error: {{.V0}}", commitError.Message), - errs.SetInput(), - ) - } + rationalizers.HandleCommitErrors(err) // Requirement not found for uninstall. case errors.As(*err, &requirementNotFoundErr): From 38b3f86a4fa7a862aa1b047fb47d1e0b921fb7de Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 10:08:29 -0700 Subject: [PATCH 19/49] Added JSON output for `state install` --- internal/captain/values.go | 6 +- internal/locale/locales/en-us.yaml | 8 ++ internal/runbits/reqop_runbit/update.go | 4 - internal/runners/install/install.go | 119 ++++++++++++------ internal/runners/install/rationalize.go | 8 +- pkg/platform/api/buildplanner/types/commit.go | 6 + .../api/buildplanner/types/requirement.go | 2 +- pkg/platform/model/vcs.go | 5 + 8 files changed, 106 insertions(+), 52 deletions(-) diff --git a/internal/captain/values.go b/internal/captain/values.go index a819291c3f..1abb4c464c 100644 --- a/internal/captain/values.go +++ b/internal/captain/values.go @@ -124,9 +124,9 @@ func (u *UsersValue) Type() string { // - / // - /@ type PackageValue struct { - Namespace string - Name string - Version string + Namespace string `json:"namespace"` + Name string `json:"name"` + Version string `json:"version"` } var _ FlagMarshaler = &PackageValue{} diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 31698718d7..b88e90e09e 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1580,3 +1580,11 @@ progress_requirements: other: "• Updating requirements" progress_platforms: other: "• Updating platforms" +prompt_pkgop_ingredient: + other: Multiple Matches +prompt_pkgop_ingredient_msg: + other: "Your query for [ACTIONABLE]{{.V0}}[/RESET] has multiple matches. Which one would you like to use?" +install_report_added: + other: "Added: [NOTICE]{{.V0}}[/RESET]" +install_report_updated: + other: "Updated: [NOTICE]{{.V0}}[/RESET]" diff --git a/internal/runbits/reqop_runbit/update.go b/internal/runbits/reqop_runbit/update.go index 9964a5a7c9..d56437339d 100644 --- a/internal/runbits/reqop_runbit/update.go +++ b/internal/runbits/reqop_runbit/update.go @@ -8,7 +8,6 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runbits/buildscript" @@ -66,9 +65,6 @@ func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit }() pg = output.StartSpinner(out, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval) - bsv, _ := script.Marshal() - logging.Debug("Buildscript: %s", string(bsv)) - commitParams := buildplanner.StageCommitParams{ Owner: pj.Owner(), Project: pj.Name(), diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index c723e66057..929a894495 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -1,6 +1,7 @@ package install import ( + "errors" "fmt" "strconv" "strings" @@ -42,11 +43,18 @@ type Params struct { Timestamp captain.TimeValue } +type resolvedRequirement struct { + types.Requirement + Version string `json:"version"` +} + type requirement struct { - input *captain.PackageValue - resolvedVersionReq []types.VersionRequirement - resolvedNamespace *model.Namespace - matchedIngredients []*model.IngredientAndVersion + Requested *captain.PackageValue `json:"requested"` + Resolved resolvedRequirement `json:"resolved"` + + // Remainder are for display purposes only + Type model.NamespaceType `json:"type"` + Operation types.Operation `json:"operation"` } type requirements []*requirement @@ -54,10 +62,10 @@ type requirements []*requirement func (r requirements) String() string { result := []string{} for _, req := range r { - if req.resolvedNamespace != nil { - result = append(result, fmt.Sprintf("%s/%s", req.resolvedNamespace.String(), req.input.Name)) + if req.Resolved.Namespace != "" { + result = append(result, fmt.Sprintf("%s/%s", req.Resolved.Namespace, req.Requested.Name)) } else { - result = append(result, req.input.Name) + result = append(result, req.Requested.Name) } } return strings.Join(result, ", ") @@ -154,6 +162,12 @@ func (i *Install) Run(params Params) (rerr error) { return errs.Wrap(err, "Failed to update local checkout") } + if out.Type().IsStructured() { + out.Print(output.Structured(reqs)) + } else { + i.renderUserFacing(reqs) + } + // All done out.Notice(locale.T("operation_success_local")) @@ -168,13 +182,14 @@ 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) { - var disambiguate []*requirement - var failed []*requirement + disambiguate := map[*requirement][]*model.IngredientAndVersion{} + failed := []*requirement{} reqs := []*requirement{} for _, pkg := range packages { - req := &requirement{input: pkg} + req := &requirement{Requested: pkg} if pkg.Namespace != "" { - req.resolvedNamespace = ptr.To(model.NewNamespaceRaw(pkg.Namespace)) + req.Resolved.Name = pkg.Name + req.Resolved.Namespace = pkg.Namespace } // Find ingredients that match the pkg query @@ -199,18 +214,13 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti return false }) } - req.matchedIngredients = ingredients // Validate that the ingredient is resolved, and prompt the user if multiple ingredients matched - if req.resolvedNamespace == nil { - len := len(ingredients) - switch { - case len == 1: - req.resolvedNamespace = ptr.To(model.ParseNamespace(*ingredients[0].Ingredient.PrimaryNamespace)) - case len > 1: - disambiguate = append(disambiguate, req) - case len == 0: + if req.Resolved.Namespace == "" { + if len(ingredients) == 0 { failed = append(failed, req) + } else { + disambiguate[req] = ingredients } } @@ -222,22 +232,31 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti return nil, errNoMatches{error: errs.New("Failed to resolve requirements"), requirements: failed, languages: languages} } - // Disambiguate requirements that match multiple ingredients + // Disambiguate requirements that had to be resolved with an ingredient lookup if len(disambiguate) > 0 { - for _, req := range disambiguate { - ingredient, err := i.promptForMatchingIngredient(req) - if err != nil { - return nil, errs.Wrap(err, "Prompting for namespace failed") + for req, ingredients := range disambiguate { + var ingredient *model.IngredientAndVersion + if len(ingredients) == 1 { + ingredient = ingredients[0] + } else { + var err error + ingredient, err = i.promptForMatchingIngredient(req, ingredients) + if err != nil { + return nil, errs.Wrap(err, "Prompting for namespace failed") + } } - req.matchedIngredients = []*model.IngredientAndVersion{ingredient} - req.resolvedNamespace = ptr.To(model.ParseNamespace(*ingredient.Ingredient.PrimaryNamespace)) + req.Resolved.Name = ingredient.Ingredient.NormalizedName + req.Resolved.Namespace = *ingredient.Ingredient.PrimaryNamespace } } - // Now that we have the ingredient resolved we can also resolve the version requirement + // Now that we have the ingredient resolved we can also resolve the version requirement. + // We can also set the type and operation, which are used for conveying what happened to the user. for _, req := range reqs { - version := req.input.Version - if req.input.Version == "" { + req.Type = model.ParseNamespace(req.Resolved.Namespace).Type() + version := req.Requested.Version + if req.Requested.Version == "" { + req.Resolved.Version = locale.T("constraint_auto") continue } if _, err := strconv.Atoi(version); err == nil { @@ -245,7 +264,8 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti version = fmt.Sprintf("%d.x", version) } var err error - req.resolvedVersionReq, err = bpModel.VersionStringToRequirements(version) + req.Resolved.Version = version + req.Resolved.VersionRequirement, err = bpModel.VersionStringToRequirements(version) if err != nil { return nil, errs.Wrap(err, "Could not process version string into requirements") } @@ -254,24 +274,24 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti return reqs, nil } -func (i *Install) promptForMatchingIngredient(req *requirement) (*model.IngredientAndVersion, error) { - if len(req.matchedIngredients) <= 1 { +func (i *Install) promptForMatchingIngredient(req *requirement, ingredients []*model.IngredientAndVersion) (*model.IngredientAndVersion, error) { + if len(ingredients) <= 1 { return nil, errs.New("promptForNamespace should never be called if there are no multiple ingredient matches") } choices := []string{} values := map[string]*model.IngredientAndVersion{} - for _, i := range req.matchedIngredients { + for _, i := range ingredients { // Generate ingredient choices to present to the user - name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, i.Ingredient.PrimaryNamespace) + name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, *i.Ingredient.PrimaryNamespace) choices = append(choices, name) values[name] = i } // Prompt the user with the ingredient choices choice, err := i.prime.Prompt().Select( - locale.Tl("prompt_pkgop_ingredient", "Multiple Matches"), - locale.Tl("prompt_pkgop_ingredient_msg", "Your query has multiple matches. Which one would you like to use?"), + locale.T("prompt_pkgop_ingredient"), + locale.Tr("prompt_pkgop_ingredient_msg", req.Requested.String()), choices, &choices[0], ) if err != nil { @@ -282,13 +302,32 @@ func (i *Install) promptForMatchingIngredient(req *requirement) (*model.Ingredie return values[choice], nil } +func (i *Install) renderUserFacing(reqs requirements) { + for _, req := range reqs { + l := "install_report_added" + if req.Operation == types.OperationUpdated { + l = "install_report_updated" + } + i.prime.Output().Notice(locale.Tr(l, fmt.Sprintf("%s/%s@%s", req.Resolved.Namespace, req.Resolved.Name, req.Resolved.Version))) + } + i.prime.Output().Notice("") +} + func prepareBuildScript(script *buildscript.BuildScript, requirements requirements, ts time.Time) error { script.SetAtTime(ts) for _, req := range requirements { requirement := types.Requirement{ - Namespace: req.resolvedNamespace.String(), - Name: req.input.Name, - VersionRequirement: req.resolvedVersionReq, + Namespace: req.Resolved.Namespace, + Name: req.Requested.Name, + VersionRequirement: req.Resolved.VersionRequirement, + } + + req.Operation = types.OperationUpdated + if err := script.RemoveRequirement(requirement); err != nil { + if !errors.As(err, ptr.To(&buildscript.RequirementNotFoundError{})) { + return errs.Wrap(err, "Could not remove requirement") + } + req.Operation = types.OperationAdded // If req could not be found it means this is an addition } err := script.AddRequirement(requirement) diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go index 4f92736b7b..2ef363548d 100644 --- a/internal/runners/install/rationalize.go +++ b/internal/runners/install/rationalize.go @@ -26,7 +26,7 @@ func (i *Install) rationalizeError(rerr *error) { case errors.As(*rerr, &noMatchErr): names := []string{} for _, r := range noMatchErr.requirements { - names = append(names, fmt.Sprintf(`[ACTIONABLE]%s[/RESET]`, r.input.Name)) + names = append(names, fmt.Sprintf(`[ACTIONABLE]%s[/RESET]`, r.Requested.Name)) } if len(noMatchErr.requirements) > 1 { *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_requirements_no_match", strings.Join(names, ", "))) @@ -52,13 +52,13 @@ func (i *Install) rationalizeError(rerr *error) { } func (i *Install) getSuggestions(req *requirement, languages []model.Language) ([]string, error) { - ingredients, err := model.SearchIngredients(req.input.Namespace, req.input.Name, false, nil, i.prime.Auth()) + ingredients, err := model.SearchIngredients(req.Requested.Namespace, req.Requested.Name, false, nil, i.prime.Auth()) if err != nil { - return []string{}, locale.WrapError(err, "err_package_ingredient_search", "Failed to resolve ingredient named: {{.V0}}", req.input.Name) + return []string{}, locale.WrapError(err, "err_package_ingredient_search", "Failed to resolve ingredient named: {{.V0}}", req.Requested.Name) } // Filter out irrelevant ingredients - if req.input.Namespace == "" { + if req.Requested.Namespace == "" { // Filter out ingredients that don't target one of the supported languages ingredients = sliceutils.Filter(ingredients, func(iv *model.IngredientAndVersion) bool { if !model.NamespaceMatch(*iv.Ingredient.PrimaryNamespace, i.nsType.Matchable()) { diff --git a/pkg/platform/api/buildplanner/types/commit.go b/pkg/platform/api/buildplanner/types/commit.go index f95da2a9df..cdba27da7d 100644 --- a/pkg/platform/api/buildplanner/types/commit.go +++ b/pkg/platform/api/buildplanner/types/commit.go @@ -1,6 +1,8 @@ package types import ( + "encoding/json" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" ) @@ -28,6 +30,10 @@ func (o Operation) String() string { } } +func (o *Operation) MarshalJSON() ([]byte, error) { + return json.Marshal(o.String()) +} + func (o *Operation) Unmarshal(v string) error { switch v { case mono_models.CommitChangeEditableOperationAdded: diff --git a/pkg/platform/api/buildplanner/types/requirement.go b/pkg/platform/api/buildplanner/types/requirement.go index b21a28cc09..33a7a367c5 100644 --- a/pkg/platform/api/buildplanner/types/requirement.go +++ b/pkg/platform/api/buildplanner/types/requirement.go @@ -3,7 +3,7 @@ package types type Requirement struct { Name string `json:"name"` Namespace string `json:"namespace"` - VersionRequirement []VersionRequirement `json:"version_requirements,omitempty"` + VersionRequirement []VersionRequirement `json:"-"` Revision *int `json:"revision,omitempty"` } diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 12b78d1941..ce79938f59 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -1,6 +1,7 @@ package model import ( + "encoding/json" "errors" "fmt" "regexp" @@ -143,6 +144,10 @@ func (t NamespaceType) String() string { return t.name } +func (t NamespaceType) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + func (t NamespaceType) Prefix() string { return t.prefix } From 48418db9ae0ad0505086cbc17b34e58535134d83 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 10:52:22 -0700 Subject: [PATCH 20/49] Add json output for uninstall --- internal/captain/values.go | 2 +- internal/locale/locales/en-us.yaml | 2 + internal/runners/uninstall/uninstall.go | 79 ++++++++++++++++++++----- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/internal/captain/values.go b/internal/captain/values.go index 1abb4c464c..5f772b13ee 100644 --- a/internal/captain/values.go +++ b/internal/captain/values.go @@ -126,7 +126,7 @@ func (u *UsersValue) Type() string { type PackageValue struct { Namespace string `json:"namespace"` Name string `json:"name"` - Version string `json:"version"` + Version string `json:"version,omitempty"` } var _ FlagMarshaler = &PackageValue{} diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index b88e90e09e..6827b515a5 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1588,3 +1588,5 @@ install_report_added: other: "Added: [NOTICE]{{.V0}}[/RESET]" install_report_updated: other: "Updated: [NOTICE]{{.V0}}[/RESET]" +install_report_removed: + other: "Removed: [NOTICE]{{.V0}}[/RESET]" diff --git a/internal/runners/uninstall/uninstall.go b/internal/runners/uninstall/uninstall.go index c443af1237..9a35e6661f 100644 --- a/internal/runners/uninstall/uninstall.go +++ b/internal/runners/uninstall/uninstall.go @@ -1,6 +1,9 @@ package uninstall import ( + "fmt" + "strings" + "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -34,6 +37,28 @@ type Params struct { Packages captain.PackagesValue } +type requirement struct { + Requested *captain.PackageValue `json:"requested"` + Resolved types.Requirement `json:"resolved"` + + // Remainder are for display purposes only + Type model.NamespaceType `json:"type"` +} + +type requirements []*requirement + +func (r requirements) String() string { + result := []string{} + for _, req := range r { + if req.Resolved.Namespace != "" { + result = append(result, fmt.Sprintf("%s/%s", req.Resolved.Namespace, req.Requested.Name)) + } else { + result = append(result, req.Requested.Name) + } + } + return strings.Join(result, ", ") +} + // Uninstall manages the installing execution context. type Uninstall struct { prime primeable @@ -97,8 +122,14 @@ func (u *Uninstall) Run(params Params) (rerr error) { // Update buildscript script := oldCommit.BuildScript() - if err := u.prepareBuildScript(script, params.Packages); err != nil { - return errs.Wrap(err, "Could not prepare build script") + reqs, err := u.resolveRequirements(script, params.Packages) + if err != nil { + return errs.Wrap(err, "Failed to resolve requirements") + } + for _, req := range reqs { + if err := script.RemoveRequirement(req.Resolved); err != nil { + return errs.Wrap(err, "Unable to remove requirement") + } } // Done updating requirements @@ -110,20 +141,36 @@ func (u *Uninstall) Run(params Params) (rerr error) { return errs.Wrap(err, "Failed to update local checkout") } + if out.Type().IsStructured() { + out.Print(output.Structured(reqs)) + } else { + u.renderUserFacing(reqs) + } + // All done out.Notice(locale.T("operation_success_local")) return nil } -func (u *Uninstall) prepareBuildScript(script *buildscript.BuildScript, pkgs captain.PackagesValue) error { +func (u *Uninstall) renderUserFacing(reqs requirements) { + u.prime.Output().Notice("") + for _, req := range reqs { + l := "install_report_removed" + u.prime.Output().Notice(locale.Tr(l, fmt.Sprintf("%s/%s", req.Resolved.Namespace, req.Resolved.Name))) + } + u.prime.Output().Notice("") +} + +func (u *Uninstall) resolveRequirements(script *buildscript.BuildScript, pkgs captain.PackagesValue) (requirements, error) { + result := requirements{} + reqs, err := script.DependencyRequirements() if err != nil { - return errs.Wrap(err, "Unable to get requirements") + return nil, errs.Wrap(err, "Unable to get requirements") } // Resolve requirements and check for errors - toRemove := []types.Requirement{} notFound := captain.PackagesValue{} multipleMatches := captain.PackagesValue{} for _, pkg := range pkgs { @@ -137,35 +184,35 @@ func (u *Uninstall) prepareBuildScript(script *buildscript.BuildScript, pkgs cap } return model.NamespaceMatch(req.Namespace, u.nsType.Matchable()) }) - toRemove = append(toRemove, matches...) // Check for duplicate matches if len(matches) > 1 { multipleMatches = append(multipleMatches, pkg) + continue } // Check for no matches if len(matches) == 0 { notFound = append(notFound, pkg) + continue } + + result = append(result, &requirement{ + Requested: pkg, + Resolved: matches[0], + Type: model.ParseNamespace(matches[0].Namespace).Type(), + }) } // Error out on duplicate matches if len(multipleMatches) > 0 { - return &errMultipleMatches{error: errs.New("Could not find all requested packages"), packages: multipleMatches} + return result, &errMultipleMatches{error: errs.New("Could not find all requested packages"), packages: multipleMatches} } // Error out on no matches if len(notFound) > 0 { - return &errNoMatches{error: errs.New("Could not find all requested packages"), packages: notFound} - } - - // Remove requirements - for _, req := range toRemove { - if err := script.RemoveRequirement(req); err != nil { - return errs.Wrap(err, "Unable to remove requirement") - } + return result, &errNoMatches{error: errs.New("Could not find all requested packages"), packages: notFound} } - return nil + return result, nil } From 6fef4538fc3ab3d76f6202e2b7d51560efe34ccc Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 10:53:17 -0700 Subject: [PATCH 21/49] Added structured output for platforms add/remove --- internal/runners/platforms/add.go | 4 ++++ internal/runners/platforms/remove.go | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/runners/platforms/add.go b/internal/runners/platforms/add.go index 3cc02607eb..cb84a58518 100644 --- a/internal/runners/platforms/add.go +++ b/internal/runners/platforms/add.go @@ -108,6 +108,10 @@ func (a *Add) Run(params AddRunParams) (rerr error) { out.Notice(locale.Tr("platform_added", *platform.DisplayName)) + if out.Type().IsStructured() { + out.Print(output.Structured(platform)) + } + return nil } diff --git a/internal/runners/platforms/remove.go b/internal/runners/platforms/remove.go index a4cd094e54..61369deb3d 100644 --- a/internal/runners/platforms/remove.go +++ b/internal/runners/platforms/remove.go @@ -17,7 +17,6 @@ import ( bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" "github.com/ActiveState/cli/pkg/platform/model" bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" - "github.com/go-openapi/strfmt" ) // RemoveRunParams tracks the info required for running Remove. @@ -82,14 +81,14 @@ func (a *Remove) Run(params RemoveRunParams) (rerr error) { if err != nil { return errs.Wrap(err, "Failed to get platforms") } - toRemove := []strfmt.UUID{} + toRemove := []*model.Platform{} for _, uid := range platforms { platform, err := model.FetchPlatformByUID(uid) if err != nil { return errs.Wrap(err, "Failed to get platform") } if model.IsPlatformMatch(platform, params.Platform.Name(), params.Platform.Version(), params.BitWidth) { - toRemove = append(toRemove, uid) + toRemove = append(toRemove, platform) } } if len(toRemove) == 0 { @@ -99,7 +98,7 @@ func (a *Remove) Run(params RemoveRunParams) (rerr error) { return errMultiMatch } - if err := script.RemovePlatform(toRemove[0]); err != nil { + if err := script.RemovePlatform(*toRemove[0].PlatformID); err != nil { return errs.Wrap(err, "Failed to remove platform") } @@ -110,6 +109,10 @@ func (a *Remove) Run(params RemoveRunParams) (rerr error) { out.Notice(locale.Tr("platform_added", params.Platform.String())) + if out.Type().IsStructured() { + out.Print(output.Structured(toRemove[0])) + } + return nil } From d3339da3c13bd20f923def70f4cedd2938a56dc7 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 11:00:57 -0700 Subject: [PATCH 22/49] Added standalone install test --- test/integration/package_int_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 77268712cb..dfd03764f6 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -431,6 +431,21 @@ scripts: ts.PrepareCommitIdFile("a9d0bc88-585a-49cf-89c1-6c07af781cff") } +func (suite *PackageIntegrationTestSuite) TestPackage_Install() { + suite.OnlyRunForTags(tagsuite.Package) + + ts := e2e.New(suite.T(), true) + defer ts.Close() + + ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") + cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") + cp.ExpectExitCode(0) + + cp = ts.Spawn("install", "requests") + cp.Expect("project has been updated") + cp.ExpectExitCode(0) +} + func (suite *PackageIntegrationTestSuite) TestPackage_Uninstall() { suite.OnlyRunForTags(tagsuite.Package) From 1277e45f8204009903dda3e0fd5a583673b2fc14 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 11:01:27 -0700 Subject: [PATCH 23/49] Drop redundant test code --- test/integration/bundle_int_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/integration/bundle_int_test.go b/test/integration/bundle_int_test.go index d5254d71a0..58ca8caa0a 100644 --- a/test/integration/bundle_int_test.go +++ b/test/integration/bundle_int_test.go @@ -91,16 +91,6 @@ func (suite *BundleIntegrationTestSuite) TestJSON() { cp = ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") cp.ExpectExitCode(0) - - cp = ts.Spawn("bundles", "install", "Testing", "--output", "json") - cp.Expect(`"name":"Testing"`) - cp.ExpectExitCode(0) - AssertValidJSON(suite.T(), cp) - - cp = ts.Spawn("bundles", "uninstall", "Testing", "-o", "editor") - cp.Expect(`"name":"Testing"`) - cp.ExpectExitCode(0) - AssertValidJSON(suite.T(), cp) } func TestBundleIntegrationTestSuite(t *testing.T) { From c3efc4200de4ffbea838fe330819f24fa62ab5a0 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 11:42:26 -0700 Subject: [PATCH 24/49] Fix assertions --- test/integration/languages_int_test.go | 4 ++-- test/integration/package_int_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/integration/languages_int_test.go b/test/integration/languages_int_test.go index d75f1b641d..c10aaf0107 100644 --- a/test/integration/languages_int_test.go +++ b/test/integration/languages_int_test.go @@ -125,7 +125,7 @@ func (suite *LanguagesIntegrationTestSuite) TestWildcards() { // Test explicit wildcard. cp = ts.Spawn("languages", "install", "python@3.9.x") - cp.Expect("Language updated: python@3.9.x") + cp.Expect("Updated: language/python@3.9.x") cp.ExpectExitCode(0) cp = ts.Spawn("history") cp.Expect("→ >=3.9,<3.10") @@ -137,7 +137,7 @@ func (suite *LanguagesIntegrationTestSuite) TestWildcards() { // Test implicit wildcard. cp = ts.Spawn("languages", "install", "python@3.9") - cp.Expect("Language updated: python@3.9") + cp.Expect("Updated: python@3.9") cp.ExpectExitCode(0) cp = ts.Spawn("history") cp.Expect("→ >=3.9,<3.10") diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index dfd03764f6..c72a8b1948 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -720,7 +720,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_NoPrompt() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "urllib3@2.0.2") - cp.Expect("Warning: Dependency has 2 known vulnerabilities", e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has \d+ known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -742,8 +742,8 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Prompt() { cp = ts.Spawn("config", "set", constants.SecurityPromptConfig, "true") cp.ExpectExitCode(0) - cp = ts.Spawn("install", "urllib3@2.0.2") - cp.Expect("Warning: Dependency has 2 known vulnerabilities") + cp = ts.Spawn("install", "urllib3@2.0.2", "--ts=2024-09-10T16:36:34.393Z") + cp.ExpectRe(`Warning: Dependency has \d+ known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) cp.Expect("Do you want to continue") cp.SendLine("y") cp.ExpectExitCode(0) @@ -764,8 +764,8 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Indirect() { cp = ts.Spawn("config", "set", constants.SecurityPromptConfig, "true") cp.ExpectExitCode(0) - cp = ts.Spawn("install", "private/ActiveState-CLI-Testing/language/python/django_dep", "--ts=now") - cp.ExpectRe(`Warning: Dependency has \d indirect known vulnerabilities`) + cp = ts.Spawn("install", "private/ActiveState-CLI-Testing/language/python/django_dep", "--ts=2024-09-10T16:36:34.393Z") + cp.ExpectRe(`Warning: Dependency has \d+ indirect known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) cp.Expect("Do you want to continue") cp.SendLine("n") cp.ExpectExitCode(1) From 63914e7757ca08b729aaf57b0ad76e881b454510 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 14:29:16 -0700 Subject: [PATCH 25/49] Ensure our default platforms are prioritized --- pkg/platform/model/inventory.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index 7cb0c806cd..780ff5bccb 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -456,13 +456,9 @@ func FetchPlatformByUID(uid strfmt.UUID) (*Platform, error) { var ErrPlatformNotFound = errors.New("could not find platform matching provided criteria") func FetchPlatformByDetails(name, version string, bitwidth int) (*Platform, error) { - // For backward compatibility we still want to raise ErrPlatformNotFound due to name ID matching - if version == "" && bitwidth == 0 { - var err error - _, err = PlatformNameToPlatformID(name) - if err != nil { - return nil, errs.Wrap(err, "platform id from name failed") - } + platformID, err := PlatformNameToPlatformID(name) + if err != nil { + return nil, errs.Wrap(err, "platform id from name failed") } runtimePlatforms, err := FetchPlatforms() @@ -470,6 +466,18 @@ func FetchPlatformByDetails(name, version string, bitwidth int) (*Platform, erro return nil, err } + // Prioritize the platform that we record as default + for _, rtPf := range runtimePlatforms { + if rtPf.PlatformID.String() != platformID { + continue + } + if IsPlatformMatch(rtPf, name, version, bitwidth) { + return rtPf, nil + } + break + } + + // Return the first platform whose criteria match for _, rtPf := range runtimePlatforms { if IsPlatformMatch(rtPf, name, version, bitwidth) { return rtPf, nil From 20e326dd080adc23ea724827e4ef380974c20990 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 14:29:35 -0700 Subject: [PATCH 26/49] Drop tests that are no longer relevant or redundant --- test/integration/package_int_test.go | 179 ++------------------------- 1 file changed, 13 insertions(+), 166 deletions(-) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index c72a8b1948..9880a4c50d 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -1,19 +1,12 @@ package integration import ( - "fmt" - "path/filepath" - "runtime" "strings" "testing" - "time" - - "github.com/ActiveState/cli/internal/testhelpers/suite" - "github.com/ActiveState/termtest" "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" + "github.com/ActiveState/cli/internal/testhelpers/suite" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" ) @@ -267,7 +260,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_info() { cp.ExpectExitCode(0) } -func (suite *PackageIntegrationTestSuite) TestPackage_detached_operation() { +func (suite *PackageIntegrationTestSuite) TestPackage_operation_multiple() { suite.OnlyRunForTags(tagsuite.Package) ts := e2e.New(suite.T(), false) defer ts.Close() @@ -277,122 +270,26 @@ func (suite *PackageIntegrationTestSuite) TestPackage_detached_operation() { cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") cp.ExpectExitCode(0) - suite.Run("install non-existing", func() { - cp := ts.Spawn("install", "json") - cp.Expect("No results found for search term") - cp.Expect("json2") - cp.Wait() - }) - - suite.Run("install", func() { - cp := ts.Spawn("install", "dateparser@0.7.2") - cp.ExpectRe("(?:Package added|being built)", termtest.OptExpectTimeout(30*time.Second)) - cp.Wait() - }) - - suite.Run("install (update)", func() { - cp := ts.Spawn("install", "dateparser@0.7.6") - cp.ExpectRe("(?:Package updated|being built)", termtest.OptExpectTimeout(50*time.Second)) - cp.Wait() - }) - - suite.Run("uninstall", func() { - cp := ts.Spawn("uninstall", "dateparser") - cp.ExpectRe("(?:Package uninstalled|being built)", termtest.OptExpectTimeout(30*time.Second)) - cp.Wait() - }) -} - -func (suite *PackageIntegrationTestSuite) TestPackage_operation() { - suite.OnlyRunForTags(tagsuite.Package) - if runtime.GOOS == "darwin" { - suite.T().Skip("Skipping mac for now as the builds are still too unreliable") - return - } - ts := e2e.New(suite.T(), false) - defer ts.Close() - - user := ts.CreateNewUser() - namespace := fmt.Sprintf("%s/%s", user.Username, "python3-pkgtest") - - cp := ts.Spawn("fork", "ActiveState-CLI/Packages", "--org", user.Username, "--name", "python3-pkgtest") - cp.ExpectExitCode(0) - - cp = ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") - cp.ExpectExitCode(0) - - cp = ts.Spawn("checkout", namespace, ".") - cp.Expect("Checked out project") - cp.ExpectExitCode(0) - - cp = ts.Spawn("history", "--output=json") - cp.ExpectExitCode(0) - - suite.Run("install", func() { - cp := ts.Spawn("install", "urllib3@1.25.6") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package added|being built)", termtest.OptExpectTimeout(30*time.Second)) - cp.Wait() - }) - - suite.Run("install (update)", func() { - cp := ts.Spawn("install", "urllib3@1.25.8") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package updated|being built)", termtest.OptExpectTimeout(30*time.Second)) - cp.Wait() - }) - - suite.Run("uninstall", func() { - cp := ts.Spawn("uninstall", "urllib3") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package uninstalled|being built)", termtest.OptExpectTimeout(30*time.Second)) - cp.Wait() - }) -} - -func (suite *PackageIntegrationTestSuite) TestPackage_operation_multiple() { - suite.OnlyRunForTags(tagsuite.Package) - if runtime.GOOS == "darwin" { - suite.T().Skip("Skipping mac for now as the builds are still too unreliable") - return - } - ts := e2e.New(suite.T(), false) - defer ts.Close() - - user := ts.CreateNewUser() - namespace := fmt.Sprintf("%s/%s", user.Username, "python3-pkgtest") - - cp := ts.Spawn("fork", "ActiveState-CLI/Packages", "--org", user.Username, "--name", "python3-pkgtest") - cp.ExpectExitCode(0) - - cp = ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") - cp.ExpectExitCode(0) - - cp = ts.Spawn("checkout", namespace, ".") - cp.Expect("Checked out project") - cp.ExpectExitCode(0) - - cp = ts.Spawn("history", "--output=json") - cp.ExpectExitCode(0) - suite.Run("install", func() { cp := ts.Spawn("install", "requests", "urllib3@1.25.6") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package added|being built)", termtest.OptExpectTimeout(30*time.Second)) + cp.Expect("Operating on project ActiveState-CLI/small-python") + cp.Expect("Added: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Added: language/python/urllib3") cp.Wait() }) suite.Run("install (update)", func() { cp := ts.Spawn("install", "urllib3@1.25.8") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package updated|being built)", termtest.OptExpectTimeout(30*time.Second)) + cp.Expect("Operating on project ActiveState-CLI/small-python") + cp.Expect("Updated: language/python/urllib3", e2e.RuntimeSourcingTimeoutOpt) cp.Wait() }) suite.Run("uninstall", func() { cp := ts.Spawn("uninstall", "requests", "urllib3") - cp.Expect(fmt.Sprintf("Operating on project %s/python3-pkgtest", user.Username)) - cp.ExpectRe("(?:Package uninstalled|being built)", termtest.OptExpectTimeout(30*time.Second)) + cp.Expect("Operating on project ActiveState-CLI/small-python") + cp.Expect("Removed: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Removed: language/python/urllib3") cp.Wait() }) } @@ -409,7 +306,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_Duplicate() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "shared/zlib") // install again - cp.Expect("already installed") + cp.Expect(" no changes") cp.ExpectNotExitCode(0) ts.IgnoreLogErrors() @@ -534,56 +431,6 @@ func (suite *PackageIntegrationTestSuite) TestJSON() { AssertValidJSON(suite.T(), cp) } -func (suite *PackageIntegrationTestSuite) TestNormalize() { - suite.OnlyRunForTags(tagsuite.Package) - if runtime.GOOS == "darwin" { - suite.T().Skip("Skipping mac for now as the builds are still too unreliable") - return - } - ts := e2e.New(suite.T(), false) - defer ts.Close() - - cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") - cp.ExpectExitCode(0) - - dir := filepath.Join(ts.Dirs.Work, "normalized") - suite.Require().NoError(fileutils.Mkdir(dir)) - cp = ts.SpawnWithOpts( - e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "."), - e2e.OptWD(dir), - ) - cp.Expect("Checked out project") - cp.ExpectExitCode(0) - - cp = ts.SpawnWithOpts( - e2e.OptArgs("install", "Charset_normalizer"), - e2e.OptWD(dir), - ) - // Even though we are not sourcing a runtime it can still take time to resolve - // the dependencies and create the commit - cp.Expect("charset-normalizer", e2e.RuntimeSourcingTimeoutOpt) - cp.Expect("is different") - cp.Expect("Charset_normalizer") - cp.ExpectExitCode(0) - - anotherDir := filepath.Join(ts.Dirs.Work, "not-normalized") - suite.Require().NoError(fileutils.Mkdir(anotherDir)) - cp = ts.SpawnWithOpts( - e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "."), - e2e.OptWD(anotherDir), - ) - cp.Expect("Checked out project") - cp.ExpectExitCode(0) - - cp = ts.SpawnWithOpts( - e2e.OptArgs("install", "charset-normalizer"), - e2e.OptWD(anotherDir), - ) - cp.Expect("charset-normalizer", e2e.RuntimeSourcingTimeoutOpt) - cp.ExpectExitCode(0, e2e.RuntimeSourcingTimeoutOpt) - suite.NotContains(cp.Output(), "is different") -} - func (suite *PackageIntegrationTestSuite) TestInstall_InvalidVersion() { suite.OnlyRunForTags(tagsuite.Package) ts := e2e.New(suite.T(), false) @@ -743,7 +590,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Prompt() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "urllib3@2.0.2", "--ts=2024-09-10T16:36:34.393Z") - cp.ExpectRe(`Warning: Dependency has \d+ known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) cp.Expect("Do you want to continue") cp.SendLine("y") cp.ExpectExitCode(0) @@ -790,7 +637,7 @@ func (suite *PackageIntegrationTestSuite) TestChangeSummary() { cp.Expect("├─ ") cp.Expect("├─ ") cp.Expect("└─ ") - cp.Expect("Package added: requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Added: requests", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } From 98aca2e0240f5509da78cb1d687359aab2067894 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Wed, 11 Sep 2024 14:29:44 -0700 Subject: [PATCH 27/49] Fix assertion --- test/integration/revert_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/revert_int_test.go b/test/integration/revert_int_test.go index dcb8028bfe..7b619531af 100644 --- a/test/integration/revert_int_test.go +++ b/test/integration/revert_int_test.go @@ -65,7 +65,7 @@ func (suite *RevertIntegrationTestSuite) TestRevertRemote() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "requests") - cp.Expect("Package added") + cp.Expect("Added: requests") cp.ExpectExitCode(0) cp = ts.Spawn("revert", "REMOTE", "--non-interactive") From 24cc190e9e42fd1458087b9849476ebe37627094 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 09:24:00 -0700 Subject: [PATCH 28/49] Fix assertions --- test/integration/package_int_test.go | 6 +++--- test/integration/runtime_int_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 9880a4c50d..33764bf230 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -637,7 +637,7 @@ func (suite *PackageIntegrationTestSuite) TestChangeSummary() { cp.Expect("├─ ") cp.Expect("├─ ") cp.Expect("└─ ") - cp.Expect("Added: requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Added: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } @@ -653,13 +653,13 @@ func (suite *PackageIntegrationTestSuite) TestChangeSummaryShowsAddedForUpdate() ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") cp = ts.Spawn("install", "jinja2@2.0") - cp.Expect("Package added: jinja2") + cp.Expect("Added: language/python/jinja2") cp.ExpectExitCode(0) cp = ts.Spawn("install", "jinja2@3.1.4") cp.Expect("Installing jinja2@3.1.4 includes 1 direct dep") cp.Expect("└─ markupsafe@2.1.5") - cp.Expect("Package updated: jinja2") + cp.Expect("Updated: language/python/jinja2") cp.ExpectExitCode(0) } diff --git a/test/integration/runtime_int_test.go b/test/integration/runtime_int_test.go index 83ab55cfa4..2e444ece84 100644 --- a/test/integration/runtime_int_test.go +++ b/test/integration/runtime_int_test.go @@ -171,7 +171,7 @@ func (suite *RuntimeIntegrationTestSuite) TestBuildInProgress() { cp.Expect("Build Log") cp.Expect("Building") cp.Expect("All dependencies have been installed and verified", e2e.RuntimeBuildSourcingTimeoutOpt) - cp.Expect("Package added: hello-world") + cp.Expect("Added: private/" + e2e.PersistentUsername + "/hello-world") cp.ExpectExitCode(0) cp = ts.Spawn("exec", "main") From 8f7920ad927e0d63b6dc2abde16594a4a7bd560d Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 09:24:08 -0700 Subject: [PATCH 29/49] Remove unused locale --- internal/locale/locales/en-us.yaml | 44 ------------------------------ 1 file changed, 44 deletions(-) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 6827b515a5..97b13b1c4d 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -628,56 +628,12 @@ package_name: other: Name package_version: other: Version -package_added: - other: "Package added: [NOTICE]{{.V0}}[/RESET]" -package_version_added: - other: "Package added: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -bundle_added: - other: "[SUCCESS]✔ {{.V0}} Bundle successfully installed![/RESET]" -bundle_version_added: - other: "[SUCCESS]✔ {{.V0}} Bundle @ version {{.V1}} successfully installed![/RESET]" -language_updated: - other: "Language updated: [NOTICE]{{.V0}}[/RESET]" -language_version_updated: - other: "Language updated: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -err_package_updated: - other: Failed to update package -err_bundle_updated: - other: Failed to update bundle -package_updated: - other: "Package updated: [NOTICE]{{.V0}}[/RESET]" -package_version_updated: - other: "Package updated: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -bundle_updated: - other: "Bundle updated: [NOTICE]{{.V0}}[/RESET]" -bundle_version_updated: - other: "Bundle updated: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -err_package_removed: - other: Failed to remove package err_remove_requirement_not_found: other: Could not remove requirement '[ACTIONABLE]{{.V0}}[/RESET]', because it does not exist. err_remove_platform_not_found: other: Could not remove platform '[ACTIONABLE]{{.V0}}[/RESET]', because it does not exist. -err_bundle_removed: - other: Failed to remove bundle -err_packages_removed: - other: Failed to remove packages -package_removed: - other: "Package uninstalled: [NOTICE]{{.V0}}[/RESET]" -bundle_removed: - other: "Bundle uninstalled: [NOTICE]{{.V0}}[/RESET]" err_update_build_script: other: Could not update runtime build script -raw_version_added: - other: "Package added: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -raw_added: - other: "Package added: [NOTICE]{{.V0}}[/RESET]" -raw_version_updated: - other: "Package updated: [NOTICE]{{.V0}}@{{.V1}}[/RESET]" -raw_updated: - other: "Package updated: [NOTICE]{{.V0}}[/RESET]" -raw_removed: - other: "Package removed: [NOTICE]{{.V0}}[/RESET]" notice_commit_build_script: other: | Your local build script has changes that should be committed. Please run '[ACTIONABLE]state commit[/RESET]' to do so. From d35e45b53ba349e8ac9e76879a981c472b7302ae Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 09:24:28 -0700 Subject: [PATCH 30/49] Make all our scripts standalone --- activestate.yaml | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/activestate.yaml b/activestate.yaml index 518f404ffb..88dd7f8c29 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -48,6 +48,7 @@ constants: scripts: - name: install-deps-dev language: bash + standalone: true if: ne .Shell "cmd" value: | if ! type "go" &> /dev/null; then @@ -73,11 +74,13 @@ scripts: fi - name: install-deps-os language: bash + standalone: true if: ne .OS.Name "Linux" description: Install OS specific deps value: "" - name: install-deps-ci language: bash + standalone: true if: ne .Shell "cmd" value: | if { [[ "$GOOS" == "windows" ]] || [[ "$OS" == "Windows_NT" ]]; } && ! type "goversioninfo" &> /dev/null; then @@ -86,6 +89,7 @@ scripts: fi - name: preprocess language: bash + standalone: true description: Generates assets required by the project that aren't just specific to the build value: | set -e @@ -97,6 +101,7 @@ scripts: fi - name: build language: bash + standalone: true description: Builds the project with the host OS as the target OS. value: | set -e @@ -111,6 +116,7 @@ scripts: go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_TARGET $constants.CLI_BUILDFLAGS $constants.CLI_PKGS - name: build-for language: bash + standalone: true description: Builds the project with the specified OS as the target OS. (valid values darwin, linux, windows) value: | set -e @@ -121,6 +127,7 @@ scripts: go build -tags "internal $GO_BUILD_TAGS" -o ${2} $constants.CLI_BUILDFLAGS $constants.CLI_PKGS - name: build-svc language: bash + standalone: true description: Builds the state-svc daemon value: | set -e @@ -136,6 +143,7 @@ scripts: - name: build-exec description: Builds the State Executor application language: bash + standalone: true value: | set -e $constants.SET_ENV @@ -143,8 +151,8 @@ scripts: go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET $constants.CLI_BUILDFLAGS $constants.EXECUTOR_PKGS - name: build-all description: Builds all our tools - standalone: true language: bash + standalone: true value: | set -e echo "Building State Tool" @@ -159,6 +167,7 @@ scripts: $scripts.build-exec.path() - name: build-installer language: bash + standalone: true description: Builds the state-installer value: | set -e @@ -167,6 +176,7 @@ scripts: go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_INSTALLER_TARGET $constants.INSTALLER_PKGS - name: build-remote-installer language: bash + standalone: true description: Builds the state-remote-installer value: | set -e @@ -181,12 +191,14 @@ scripts: go build -tags "$GO_BUILD_TAGS" -o ../../$BUILD_TARGET_DIR/$TARGET . - name: install language: bash + standalone: true description: Installs the current HEAD version into GOBIN value: | $constants.SET_ENV go install $constants.CLI_BUILDFLAGS $CLI_PKGS - name: deploy-updates language: bash + standalone: true description: Deploys update files to S3. This steps is automated by CI and should never be ran manually unless you KNOW WHAT YOU'RE DOING. value: | set -e @@ -232,17 +244,20 @@ scripts: cp installers/stop${constants.SCRIPT_EXT} $INSTALLERS_DIR/stop${constants.SCRIPT_EXT} - name: deploy-installers language: bash + standalone: true description: Deploys update files to S3. This steps is automated by CI and should never be ran manually unless you KNOW WHAT YOU'RE DOING. value: | go run scripts/ci/s3-deployer/main.go build/installers us-east-1 state-tool update/state - name: deploy-remote-installer language: bash + standalone: true value: | set -e $constants.SET_ENV go run scripts/ci/s3-deployer/main.go $BUILD_TARGET_DIR/remote-installer us-east-1 state-tool remote-installer - name: build-workflow-assets language: bash + standalone: true description: Generates our github workflows value: | $scripts.build-for.path() "windows" ./.github/deps/Windows/bin/state.exe @@ -254,6 +269,7 @@ scripts: GOOS=darwin go build -o .github/deps/macOS/bin/parallelize github.com/ActiveState/cli/scripts/ci/parallelize/ - name: update-workflow-assets language: bash + standalone: true description: Generates our github workflows value: | [ -z "${2}" ] && >&2 echo "Usage: update-workflow-assets [branch] [version]" && exit 1 @@ -286,12 +302,13 @@ scripts: rm -Rf $tmpDir - name: test language: bash + standalone: true description: Runs unit tests (not integration tests) value: | go test -v `go list ./... | grep -v integration | grep -v automation | grep -v expect | grep -v state-svc | grep -v state-offline` $@ - standalone: true - name: integration-tests language: bash + standalone: true description: Runs integration tests. value: | unset ACTIVESTATE_ACTIVATED @@ -300,6 +317,7 @@ scripts: go test `go list ./... | grep "${INTEGRATION_TEST_REGEX}"` -v "${@:1}" -timeout 20m - name: integration-tests-build-check language: bash + standalone: true description: Builds integration tests and removes the executable artifact(s). value: | out="x.test" @@ -307,28 +325,31 @@ scripts: [ -f $out ] && rm $out - name: clean language: bash + standalone: true description: Cleans out the build dir. value: | go clean rm -Rf build - name: run language: bash + standalone: true description: Builds the State Tool and runs it with `--help` value: | $scripts.build.path() build/state --help - name: debug language: bash - description: "Runs a remote debugger that can be hooked into from your IDE. Example usage: `state run debug activate` (will debug `state activate`)" standalone: true + description: "Runs a remote debugger that can be hooked into from your IDE. Example usage: `state run debug activate` (will debug `state activate`)" value: dlv debug --headless --listen=:2346 --api-version=2 github.com/ActiveState/cli/cmd/state -- $@ - name: scripted language: bash - description: "Runs a command via 'go run'" standalone: true + description: "Runs a command via 'go run'" value: go run github.com/ActiveState/cli/cmd/state $@ - name: story-cleanup language: bash + standalone: true description: "Runs Python script to move old stories from primary project to storage project" value: | export PT_API_TOKEN=$secrets.project.PT_API_TOKEN @@ -338,12 +359,14 @@ scripts: python3 ./scripts/story-cleanup/story-cleanup.py - name: lint language: bash + standalone: true description: "Runs linting for untracked and unstaged changes (if any), or staged changes" value: | golangci-lint run --new actionlint - name: lint-staged language: bash + standalone: true description: "Runs linting for staged changes (skipping untracked and unstaged-only files)" value: | golangci-lint run \ @@ -356,12 +379,14 @@ scripts: actionlint $actionfiles - name: lint-all language: bash + standalone: true description: "Runs linting for all files" value: | golangci-lint run --no-config actionlint - name: check-format language: bash + standalone: true description: "Checks if the code is formatted correctly" value: | set -e @@ -392,6 +417,7 @@ scripts: fi - name: grab-mergecommits language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -399,6 +425,7 @@ scripts: go run $project.path()/scripts/grab-mergecommits/main.go $1 - name: target-version-pr language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -406,6 +433,7 @@ scripts: go run $project.path()/scripts/ci/target-version-pr/main.go $1 - name: create-version-pr language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -413,6 +441,7 @@ scripts: go run $project.path()/scripts/create-version-pr/main.go $1 - name: propagate-pr language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -420,6 +449,7 @@ scripts: go run $project.path()/scripts/ci/propagate-pr/main.go $1 - name: verify-pr language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -427,6 +457,7 @@ scripts: go run $project.path()/scripts/ci/verify-pr/main.go $1 - name: start-story language: bash + standalone: true value: | export JIRA_USERNAME=${secrets.user.JIRA_USERNAME} export JIRA_TOKEN=${secrets.user.JIRA_TOKEN} @@ -434,6 +465,7 @@ scripts: go run $project.path()/scripts/start-story/main.go "$@" - name: ghapi language: bash + standalone: true value: | curl \ -H "Accept: application/vnd.github+json" \ @@ -442,6 +474,7 @@ scripts: - name: benchmark-exec if: eq .OS.Name "Linux" language: bash + standalone: true description: "Benchmarks executable leveraging highly sensitive/accurate tooling" value: | # example usage: From 0021f0390533bd64b500286411a4d0771d57cdee Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 10:15:37 -0700 Subject: [PATCH 31/49] Add back auto wildcarding --- internal/runners/install/install.go | 124 +++++++++++++++---------- pkg/platform/model/vcs.go | 13 --- test/integration/languages_int_test.go | 2 +- 3 files changed, 77 insertions(+), 62 deletions(-) diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 929a894495..722e79afc3 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -3,7 +3,7 @@ package install import ( "errors" "fmt" - "strconv" + "regexp" "strings" "time" @@ -45,7 +45,8 @@ type Params struct { type resolvedRequirement struct { types.Requirement - Version string `json:"version"` + VersionLocale string `json:"version"` // VersionLocale represents the version as we want to show it to the user + ingredient *model.IngredientAndVersion } type requirement struct { @@ -182,7 +183,6 @@ 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) { - disambiguate := map[*requirement][]*model.IngredientAndVersion{} failed := []*requirement{} reqs := []*requirement{} for _, pkg := range packages { @@ -198,31 +198,48 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti return nil, locale.WrapError(err, "err_pkgop_search_err", "Failed to check for ingredients.") } - // Resolve matched ingredients + // Filter out ingredients that don't target one of the supported languages if pkg.Namespace == "" { - // Filter out ingredients that don't target one of the supported languages ingredients = sliceutils.Filter(ingredients, func(iv *model.IngredientAndVersion) bool { + // Ensure namespace type matches if !model.NamespaceMatch(*iv.Ingredient.PrimaryNamespace, i.nsType.Matchable()) { return false } - il := model.LanguageFromNamespace(*iv.Ingredient.PrimaryNamespace) - for _, l := range languages { - if l.Name == il { - return true + + // Ensure that this is namespace covers one of the languages in our project + // But only if we're aiming to install a package or bundle, because otherwise the namespace is not + // guaranteed to hold the language. + if i.nsType == model.NamespacePackage || i.nsType == model.NamespaceBundle { + il := model.LanguageFromNamespace(*iv.Ingredient.PrimaryNamespace) + for _, l := range languages { + if l.Name == il { + return true + } } + return false } - return false + return true }) } - // Validate that the ingredient is resolved, and prompt the user if multiple ingredients matched - if req.Resolved.Namespace == "" { - if len(ingredients) == 0 { - failed = append(failed, req) - } else { - disambiguate[req] = ingredients + // Resolve matched ingredients + var ingredient *model.IngredientAndVersion + if len(ingredients) == 1 { + ingredient = ingredients[0] + } else if len(ingredients) > 1 { // This wouldn't ever trigger if namespace was provided as that should guarantee a single result + var err error + ingredient, err = i.promptForMatchingIngredient(req, ingredients) + if err != nil { + return nil, errs.Wrap(err, "Prompting for namespace failed") } } + if ingredient == nil { + failed = append(failed, req) + } else { + req.Resolved.Name = ingredient.Ingredient.NormalizedName + req.Resolved.Namespace = *ingredient.Ingredient.PrimaryNamespace + req.Resolved.ingredient = ingredient + } reqs = append(reqs, req) } @@ -232,46 +249,57 @@ func (i *Install) resolveRequirements(packages captain.PackagesValue, ts time.Ti return nil, errNoMatches{error: errs.New("Failed to resolve requirements"), requirements: failed, languages: languages} } - // Disambiguate requirements that had to be resolved with an ingredient lookup - if len(disambiguate) > 0 { - for req, ingredients := range disambiguate { - var ingredient *model.IngredientAndVersion - if len(ingredients) == 1 { - ingredient = ingredients[0] - } else { - var err error - ingredient, err = i.promptForMatchingIngredient(req, ingredients) - if err != nil { - return nil, errs.Wrap(err, "Prompting for namespace failed") - } - } - req.Resolved.Name = ingredient.Ingredient.NormalizedName - req.Resolved.Namespace = *ingredient.Ingredient.PrimaryNamespace - } - } - // Now that we have the ingredient resolved we can also resolve the version requirement. // We can also set the type and operation, which are used for conveying what happened to the user. for _, req := range reqs { + // Set requirement type req.Type = model.ParseNamespace(req.Resolved.Namespace).Type() - version := req.Requested.Version - if req.Requested.Version == "" { - req.Resolved.Version = locale.T("constraint_auto") - continue + + if err := resolveVersion(req); err != nil { + return nil, errs.Wrap(err, "Could not resolve version") } - if _, err := strconv.Atoi(version); err == nil { - // If the version number provided is a straight up integer (no dots or dashes) then assume it's a wildcard - version = fmt.Sprintf("%d.x", version) + } + + return reqs, nil +} + +var versionRe = regexp.MustCompile(`^\d(\.\d+)*$`) + +func resolveVersion(req *requirement) error { + version := req.Requested.Version + + // An empty version means "Auto" + if req.Requested.Version == "" { + req.Resolved.VersionLocale = locale.T("constraint_auto") + return nil + } + + // Verify that the version provided can be resolved + if versionRe.MatchString(version) { + match := false + for _, knownVersion := range req.Resolved.ingredient.Versions { + if knownVersion.Version == version { + match = true + break + } } - var err error - req.Resolved.Version = version - req.Resolved.VersionRequirement, err = bpModel.VersionStringToRequirements(version) - if err != nil { - return nil, errs.Wrap(err, "Could not process version string into requirements") + if !match { + for _, knownVersion := range req.Resolved.ingredient.Versions { + if strings.HasPrefix(knownVersion.Version, version) { + version = version + ".x" // The user supplied a partial version, resolve it as a wildcard + } + } } } - return reqs, nil + var err error + req.Resolved.VersionLocale = version + req.Resolved.VersionRequirement, err = bpModel.VersionStringToRequirements(version) + if err != nil { + return errs.Wrap(err, "Could not process version string into requirements") + } + + return nil } func (i *Install) promptForMatchingIngredient(req *requirement, ingredients []*model.IngredientAndVersion) (*model.IngredientAndVersion, error) { @@ -308,7 +336,7 @@ func (i *Install) renderUserFacing(reqs requirements) { if req.Operation == types.OperationUpdated { l = "install_report_updated" } - i.prime.Output().Notice(locale.Tr(l, fmt.Sprintf("%s/%s@%s", req.Resolved.Namespace, req.Resolved.Name, req.Resolved.Version))) + i.prime.Output().Notice(locale.Tr(l, fmt.Sprintf("%s/%s@%s", req.Resolved.Namespace, req.Resolved.Name, req.Resolved.VersionLocale))) } i.prime.Output().Notice("") } diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index ce79938f59..65e9db352e 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -671,19 +671,6 @@ func (cs indexedCommits) countBetween(first, last string) (int, error) { return ct, nil } -func ResolveRequirementNameAndVersion(name, version string, word int, namespace Namespace, auth *authentication.Auth) (string, string, error) { - if namespace.Type() == NamespacePlatform { - platform, err := FetchPlatformByDetails(name, version, word) - if err != nil { - return "", "", errs.Wrap(err, "Could not fetch platform") - } - name = platform.PlatformID.String() - version = "" - } - - return name, version, nil -} - func ChangesetFromRequirements(op Operation, reqs []*gqlModel.Requirement) Changeset { var changeset Changeset diff --git a/test/integration/languages_int_test.go b/test/integration/languages_int_test.go index c10aaf0107..9103fd9231 100644 --- a/test/integration/languages_int_test.go +++ b/test/integration/languages_int_test.go @@ -137,7 +137,7 @@ func (suite *LanguagesIntegrationTestSuite) TestWildcards() { // Test implicit wildcard. cp = ts.Spawn("languages", "install", "python@3.9") - cp.Expect("Updated: python@3.9") + cp.Expect("Updated: language/python@3.9.x") cp.ExpectExitCode(0) cp = ts.Spawn("history") cp.Expect("→ >=3.9,<3.10") From e12c61583999724c23104f245ffa074d26477c81 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 10:26:43 -0700 Subject: [PATCH 32/49] Reduce API requests --- internal/runbits/commits_runbit/time.go | 17 +++++++++++++++++ internal/runners/install/install.go | 16 ++++++++-------- pkg/platform/model/checkpoints.go | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/internal/runbits/commits_runbit/time.go b/internal/runbits/commits_runbit/time.go index ac9afb9717..af7f2e33e9 100644 --- a/internal/runbits/commits_runbit/time.go +++ b/internal/runbits/commits_runbit/time.go @@ -5,6 +5,7 @@ import ( "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -63,3 +64,19 @@ func ExpandTimeForProject(ts *captain.TimeValue, auth *authentication.Auth, proj return timestamp, nil } + +// ExpandTimeForBuildScript is the same as ExpandTimeForProject except that it works off of a buildscript, allowing for +// fewer API round trips. +func ExpandTimeForBuildScript(ts *captain.TimeValue, auth *authentication.Auth, script *buildscript.BuildScript) (time.Time, error) { + timestamp, err := ExpandTime(ts, auth) + if err != nil { + return time.Time{}, errs.Wrap(err, "Unable to expand time") + } + + atTime := script.AtTime() + if atTime.After(timestamp) { + return *atTime, nil + } + + return timestamp, nil +} diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index 722e79afc3..db12bb874c 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -118,13 +118,6 @@ func (i *Install) Run(params Params) (rerr error) { { pg = output.StartSpinner(out, locale.T("progress_search"), constants.TerminalAnimationInterval) - // Resolve timestamp, commit and languages used for current project. - // This will be used to resolve the requirements. - ts, err = commits_runbit.ExpandTimeForProject(¶ms.Timestamp, i.prime.Auth(), i.prime.Project()) - if err != nil { - return errs.Wrap(err, "Unable to get timestamp from params") - } - // Grab local commit info localCommitID, err := localcommit.Get(i.prime.Project().Dir()) if err != nil { @@ -135,8 +128,15 @@ func (i *Install) Run(params Params) (rerr error) { return errs.Wrap(err, "Failed to fetch old build result") } + // Resolve timestamp, commit and languages used for current project. + // This will be used to resolve the requirements. + ts, err = commits_runbit.ExpandTimeForBuildScript(¶ms.Timestamp, i.prime.Auth(), oldCommit.BuildScript()) + if err != nil { + return errs.Wrap(err, "Unable to get timestamp from params") + } + // Get languages used in current project - languages, err := model.FetchLanguagesForCommit(localCommitID, i.prime.Auth()) + languages, err := model.FetchLanguagesForBuildScript(oldCommit.BuildScript()) if err != nil { logging.Debug("Could not get language from project: %v", err) } diff --git a/pkg/platform/model/checkpoints.go b/pkg/platform/model/checkpoints.go index f7beccbc5d..87f491a940 100644 --- a/pkg/platform/model/checkpoints.go +++ b/pkg/platform/model/checkpoints.go @@ -3,6 +3,7 @@ package model import ( "strings" + "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/go-openapi/strfmt" @@ -72,6 +73,27 @@ func FetchLanguagesForCommit(commitID strfmt.UUID, auth *authentication.Auth) ([ return languages, nil } +// FetchLanguagesForBuildScript fetches a list of language names for the given buildscript +func FetchLanguagesForBuildScript(script *buildscript.BuildScript) ([]Language, error) { + languages := []Language{} + reqs, err := script.DependencyRequirements() + if err != nil { + return nil, errs.Wrap(err, "failed to get dependency requirements") + } + + for _, requirement := range reqs { + if NamespaceMatch(requirement.Namespace, NamespaceLanguageMatch) { + lang := Language{ + Name: requirement.Name, + Version: BuildPlannerVersionConstraintsToString(requirement.VersionRequirement), + } + languages = append(languages, lang) + } + } + + return languages, nil +} + // FetchCheckpointForCommit fetches the checkpoint for the given commit func FetchCheckpointForCommit(commitID strfmt.UUID, auth *authentication.Auth) ([]*gqlModel.Requirement, strfmt.DateTime, error) { logging.Debug("fetching checkpoint (%s)", commitID.String()) From 74e7e6ab289ae0483a9aa447a09d8d621da01e23 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 12 Sep 2024 13:17:09 -0700 Subject: [PATCH 33/49] Remove unused var --- internal/runners/install/install.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/runners/install/install.go b/internal/runners/install/install.go index db12bb874c..25837289d5 100644 --- a/internal/runners/install/install.go +++ b/internal/runners/install/install.go @@ -111,7 +111,6 @@ func (i *Install) Run(params Params) (rerr error) { }() // Start process of resolving requirements - var err error var oldCommit *bpModel.Commit var reqs requirements var ts time.Time From 6d344d916bb931c631e0b725893483aa8bd9db44 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 09:04:56 -0700 Subject: [PATCH 34/49] Fix assertion --- test/integration/package_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 33764bf230..8e63340c10 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -567,7 +567,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_NoPrompt() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "urllib3@2.0.2") - cp.ExpectRe(`Warning: Dependency has \d+ known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) cp.ExpectExitCode(0) } From 252009849ba303004d3d77c932c9e7697e85710e Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 09:07:56 -0700 Subject: [PATCH 35/49] Mark input errors --- internal/runners/install/rationalize.go | 15 ++++++++++++--- internal/runners/uninstall/rationalize.go | 10 ++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/runners/install/rationalize.go b/internal/runners/install/rationalize.go index 2ef363548d..be4a6d3a5d 100644 --- a/internal/runners/install/rationalize.go +++ b/internal/runners/install/rationalize.go @@ -29,7 +29,10 @@ func (i *Install) rationalizeError(rerr *error) { names = append(names, fmt.Sprintf(`[ACTIONABLE]%s[/RESET]`, r.Requested.Name)) } if len(noMatchErr.requirements) > 1 { - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_requirements_no_match", strings.Join(names, ", "))) + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("package_requirements_no_match", strings.Join(names, ", ")), + errs.SetInput()) return } suggestions, err := i.getSuggestions(noMatchErr.requirements[0], noMatchErr.languages) @@ -38,11 +41,17 @@ func (i *Install) rationalizeError(rerr *error) { } if len(suggestions) == 0 { - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives_nosuggest", strings.Join(names, ", "))) + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("package_ingredient_alternatives_nosuggest", strings.Join(names, ", ")), + errs.SetInput()) return } - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "), strings.Join(suggestions, "\n"))) + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("package_ingredient_alternatives", strings.Join(names, ", "), strings.Join(suggestions, "\n")), + errs.SetInput()) // Error staging a commit during install. case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): diff --git a/internal/runners/uninstall/rationalize.go b/internal/runners/uninstall/rationalize.go index 767b1c3136..387c9869a2 100644 --- a/internal/runners/uninstall/rationalize.go +++ b/internal/runners/uninstall/rationalize.go @@ -19,10 +19,16 @@ func (u *Uninstall) rationalizeError(rerr *error) { return case errors.As(*rerr, &noMatchesErr): - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("err_uninstall_nomatch", noMatchesErr.packages.String())) + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("err_uninstall_nomatch", noMatchesErr.packages.String()), + errs.SetInput()) case errors.As(*rerr, &multipleMatchesErr): - *rerr = errs.WrapUserFacing(*rerr, locale.Tr("err_uninstall_multimatch", multipleMatchesErr.packages.String())) + *rerr = errs.WrapUserFacing( + *rerr, + locale.Tr("err_uninstall_multimatch", multipleMatchesErr.packages.String()), + errs.SetInput()) // Error staging a commit during install. case errors.As(*rerr, ptr.To(&bpResp.CommitError{})): From 02ba52766043b37a4fa9ebca874a7d8b9bf054ff Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 09:30:59 -0700 Subject: [PATCH 36/49] Increase search timeout --- pkg/platform/model/inventory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index 780ff5bccb..aa4176e151 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -205,6 +205,7 @@ func searchIngredientsNamespace(ns string, name string, includeVersions bool, ex } params.SetLimit(&limit) params.SetHTTPClient(api.NewHTTPClient()) + params.WithTimeout(60 * time.Second) if ts != nil { dt := strfmt.DateTime(*ts) From d43b31a671faec2e7972adcb508a6c42971ef1c8 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:01:37 -0700 Subject: [PATCH 37/49] Add comment explaining async config condition --- internal/runbits/reqop_runbit/update.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/runbits/reqop_runbit/update.go b/internal/runbits/reqop_runbit/update.go index d56437339d..99333a0a90 100644 --- a/internal/runbits/reqop_runbit/update.go +++ b/internal/runbits/reqop_runbit/update.go @@ -90,6 +90,9 @@ func UpdateAndReload(prime primeable, script *buildscript.BuildScript, oldCommit } // Start runtime sourcing UI + // Note normally we'd defer to Update's logic of async runtimes, but the reason we do this is to allow for solve + // errors to still be relayed even when using async. In this particular case the solving logic already happened + // when we created the commit, so running it again doesn't provide any value and only would slow things down. if !cfg.GetBool(constants.AsyncRuntimeConfig) { // refresh or install runtime _, err := runtime_runbit.Update(prime, trigger, From 0fa03bf4997d07ef471f9d6bc51cf87cf4786af5 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:05:02 -0700 Subject: [PATCH 38/49] Drop badly worded but also useless comments --- internal/runners/platforms/remove.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/runners/platforms/remove.go b/internal/runners/platforms/remove.go index 61369deb3d..7aab41b28a 100644 --- a/internal/runners/platforms/remove.go +++ b/internal/runners/platforms/remove.go @@ -19,17 +19,14 @@ import ( bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" ) -// RemoveRunParams tracks the info required for running Remove. type RemoveRunParams struct { Params } -// Remove manages the adding execution context. type Remove struct { prime primeable } -// NewRemove prepares an add execution context for use. func NewRemove(prime primeable) *Remove { return &Remove{ prime: prime, @@ -39,7 +36,6 @@ func NewRemove(prime primeable) *Remove { var errNoMatch = errors.New("no platform matched the search criteria") var errMultiMatch = errors.New("multiple platforms matched the search criteria") -// Run executes the add behavior. func (a *Remove) Run(params RemoveRunParams) (rerr error) { defer rationalizeRemovePlatformError(&rerr) From 9d7a94ac04e3362503a1843977a63d9d08ffbb99 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:08:41 -0700 Subject: [PATCH 39/49] Add comment for `SpawnDebuggerWithOpts` --- internal/testhelpers/e2e/session.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index 5c780a145a..ca6c19217a 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -210,6 +210,12 @@ func (s *Session) Spawn(args ...string) *SpawnedCmd { return s.SpawnCmdWithOpts(s.Exe, OptArgs(args...)) } +// SpawnDebuggerWithOpts will spawn a state tool command with the dlv debugger in remote debugging port. +// It uses the default dlv port of `2345`. It has been tested in Goland (intellij), see instructions here: +// https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#step-3-create-the-remote-run-debug-configuration-on-the-client-computer +// Note remote debugging seems a bit unreliable. I've found it works best to start the test code first, and once it is +// running then start the remote debugger. When I launch the remote debugger first it often doesn't take. But even +// when using this trickery it may at times not work; try restarting goland, your machine, or dlv. func (s *Session) SpawnDebuggerWithOpts(opts ...SpawnOptSetter) *SpawnedCmd { spawnOpts := s.newSpawnOpts(opts...) args := slices.Clone(spawnOpts.Args) From b4ada371b5e4287d6a25c7545ab08986be46f867 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:12:33 -0700 Subject: [PATCH 40/49] Remove unused function --- pkg/buildscript/mutations.go | 16 ---------------- pkg/platform/model/buildplanner/project.go | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index f1a6992d89..6c23b7a0d3 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -125,22 +125,6 @@ func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { return nil } -func (b *BuildScript) UpdatePlatform(operation types.Operation, platformID strfmt.UUID) error { - var err error - switch operation { - case types.OperationAdded: - err = b.AddPlatform(platformID) - case types.OperationRemoved: - err = b.RemovePlatform(platformID) - default: - return errs.New("Unsupported operation") - } - if err != nil { - return errs.Wrap(err, "Could not update BuildScript's platform") - } - return nil -} - func (b *BuildScript) AddPlatform(platformID strfmt.UUID) error { platformsNode, err := b.getPlatformsNode() if err != nil { diff --git a/pkg/platform/model/buildplanner/project.go b/pkg/platform/model/buildplanner/project.go index 6f52dcc06d..c9e84647b2 100644 --- a/pkg/platform/model/buildplanner/project.go +++ b/pkg/platform/model/buildplanner/project.go @@ -39,7 +39,7 @@ func (b *BuildPlanner) CreateProject(params *CreateProjectParams) (strfmt.UUID, } // Add the platform. - if err := script.UpdatePlatform(types.OperationAdded, params.PlatformID); err != nil { + if err := script.AddPlatform(params.PlatformID); err != nil { return "", errs.Wrap(err, "Unable to add platform") } From 70a6f307afa2fe8d00a407b76c0786e696009ce9 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:13:00 -0700 Subject: [PATCH 41/49] Rename method to be consistent --- internal/runners/publish/publish.go | 2 +- pkg/platform/model/vcs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index d75084e25d..a02ff27620 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -125,7 +125,7 @@ func (r *Runner) Run(params *Params) error { if params.Namespace != "" { reqVars.Namespace = params.Namespace } else if reqVars.Namespace == "" && r.project != nil && r.project.Owner() != "" { - reqVars.Namespace = model.NewOrgNamespace(r.project.Owner()).String() + reqVars.Namespace = model.NewNamespaceOrg(r.project.Owner()).String() } // Name diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 65e9db352e..4efaf98749 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -221,7 +221,7 @@ func NewNamespacePlatform() Namespace { return Namespace{NamespacePlatform, "platform"} } -func NewOrgNamespace(orgName string) Namespace { +func NewNamespaceOrg(orgName string) Namespace { return Namespace{ nsType: NamespaceOrg, value: NamespaceOrg.prefix + "/" + orgName, From 60bc05993bc66ea89cfdbd0823c953459ce1f259 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:14:18 -0700 Subject: [PATCH 42/49] Expectation can use full word --- test/integration/install_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go index 3adbf3939c..f75de95ffa 100644 --- a/test/integration/install_int_test.go +++ b/test/integration/install_int_test.go @@ -42,7 +42,7 @@ func (suite *InstallIntegrationTestSuite) TestInstallSuggest() { cp = ts.Spawn("install", "djang") cp.Expect("No results found") cp.Expect("Did you mean") - cp.Expect("language/python/djang") + cp.Expect("language/python/django") cp.ExpectExitCode(1) } From 1ee70c93b7250f56a501d4ee7ab17f5f6454d7f3 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:17:47 -0700 Subject: [PATCH 43/49] Differentiate between solving and sourcing runtime timeouts --- internal/testhelpers/e2e/session.go | 6 ++++++ internal/testhelpers/e2e/session_unix.go | 15 --------------- internal/testhelpers/e2e/session_windows.go | 15 --------------- test/integration/package_int_test.go | 14 +++++++------- 4 files changed, 13 insertions(+), 37 deletions(-) delete mode 100644 internal/testhelpers/e2e/session_unix.go delete mode 100644 internal/testhelpers/e2e/session_windows.go diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index ca6c19217a..a0c02d6184 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -44,6 +44,12 @@ import ( "github.com/stretchr/testify/require" ) +var ( + RuntimeSolvingTimeoutOpt = termtest.OptExpectTimeout(1 * time.Minute) + RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute) + RuntimeBuildSourcingTimeoutOpt = termtest.OptExpectTimeout(6 * time.Minute) +) + // Session represents an end-to-end testing session during which several console process can be spawned and tested // It provides a consistent environment (environment variables and temporary // directories) that is shared by processes spawned during this session. diff --git a/internal/testhelpers/e2e/session_unix.go b/internal/testhelpers/e2e/session_unix.go deleted file mode 100644 index 970a8fbec0..0000000000 --- a/internal/testhelpers/e2e/session_unix.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !windows -// +build !windows - -package e2e - -import ( - "time" - - "github.com/ActiveState/termtest" -) - -var ( - RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute) - RuntimeBuildSourcingTimeoutOpt = termtest.OptExpectTimeout(6 * time.Minute) -) diff --git a/internal/testhelpers/e2e/session_windows.go b/internal/testhelpers/e2e/session_windows.go deleted file mode 100644 index 6c1830eadd..0000000000 --- a/internal/testhelpers/e2e/session_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build windows -// +build windows - -package e2e - -import ( - "time" - - "github.com/ActiveState/termtest" -) - -var ( - RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute) - RuntimeBuildSourcingTimeoutOpt = termtest.OptExpectTimeout(6 * time.Minute) -) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 8e63340c10..1b4ea761c3 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -273,7 +273,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_operation_multiple() { suite.Run("install", func() { cp := ts.Spawn("install", "requests", "urllib3@1.25.6") cp.Expect("Operating on project ActiveState-CLI/small-python") - cp.Expect("Added: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Added: language/python/requests", e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Added: language/python/urllib3") cp.Wait() }) @@ -281,14 +281,14 @@ func (suite *PackageIntegrationTestSuite) TestPackage_operation_multiple() { suite.Run("install (update)", func() { cp := ts.Spawn("install", "urllib3@1.25.8") cp.Expect("Operating on project ActiveState-CLI/small-python") - cp.Expect("Updated: language/python/urllib3", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Updated: language/python/urllib3", e2e.RuntimeSolvingTimeoutOpt) cp.Wait() }) suite.Run("uninstall", func() { cp := ts.Spawn("uninstall", "requests", "urllib3") cp.Expect("Operating on project ActiveState-CLI/small-python") - cp.Expect("Removed: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Removed: language/python/requests", e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Removed: language/python/urllib3") cp.Wait() }) @@ -567,7 +567,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_NoPrompt() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "urllib3@2.0.2") - cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSolvingTimeoutOpt) cp.ExpectExitCode(0) } @@ -590,7 +590,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Prompt() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "urllib3@2.0.2", "--ts=2024-09-10T16:36:34.393Z") - cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has .* vulnerabilities`, e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Do you want to continue") cp.SendLine("y") cp.ExpectExitCode(0) @@ -612,7 +612,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Indirect() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "private/ActiveState-CLI-Testing/language/python/django_dep", "--ts=2024-09-10T16:36:34.393Z") - cp.ExpectRe(`Warning: Dependency has \d+ indirect known vulnerabilities`, e2e.RuntimeSourcingTimeoutOpt) + cp.ExpectRe(`Warning: Dependency has \d+ indirect known vulnerabilities`, e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Do you want to continue") cp.SendLine("n") cp.ExpectExitCode(1) @@ -637,7 +637,7 @@ func (suite *PackageIntegrationTestSuite) TestChangeSummary() { cp.Expect("├─ ") cp.Expect("├─ ") cp.Expect("└─ ") - cp.Expect("Added: language/python/requests", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Added: language/python/requests", e2e.RuntimeSolvingTimeoutOpt) cp.ExpectExitCode(0) } From 2f767aa759c9cefaeb8db7e80ffeceedd3d0bf7e Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 14:19:07 -0700 Subject: [PATCH 44/49] Retaining dirs is by definition something we ought to only do for debugging --- test/integration/exec_int_test.go | 2 +- test/integration/executor_int_test.go | 2 +- test/integration/package_int_test.go | 8 ++++---- test/integration/prepare_int_test.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/integration/exec_int_test.go b/test/integration/exec_int_test.go index f3cfff58d7..1bc3009ac5 100644 --- a/test/integration/exec_int_test.go +++ b/test/integration/exec_int_test.go @@ -161,7 +161,7 @@ func (suite *ExecIntegrationTestSuite) TestExeBatArguments() { suite.T().Skip("This test is only for windows") } - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") diff --git a/test/integration/executor_int_test.go b/test/integration/executor_int_test.go index 3ff24dd223..96a46a3dfe 100644 --- a/test/integration/executor_int_test.go +++ b/test/integration/executor_int_test.go @@ -96,7 +96,7 @@ func (suite *ExecutorIntegrationTestSuite) TestExecutorBatArguments() { suite.T().Skip("This test is only for windows") } - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() root := environment.GetRootPathUnsafe() diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index 1b4ea761c3..7cafcc21e3 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -331,7 +331,7 @@ scripts: func (suite *PackageIntegrationTestSuite) TestPackage_Install() { suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() ts.PrepareProject("ActiveState-CLI/small-python", "5a1e49e5-8ceb-4a09-b605-ed334474855b") @@ -346,7 +346,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_Install() { func (suite *PackageIntegrationTestSuite) TestPackage_Uninstall() { suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() ts.PrepareProject("ActiveState-CLI-Testing/small-python-with-pkg", "a2115792-2620-4217-89ed-b596c8c11ce3") @@ -361,7 +361,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_Uninstall() { func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() suite.PrepareActiveStateYAML(ts) @@ -379,7 +379,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDoesNotExist() { func (suite *PackageIntegrationTestSuite) TestPackage_UninstallDupeMatch() { suite.OnlyRunForTags(tagsuite.Package) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) defer ts.Close() ts.PrepareProject("ActiveState-CLI-Testing/duplicate-pkg-name", "e5a15d59-9192-446a-a133-9f4c2ebe0898") diff --git a/test/integration/prepare_int_test.go b/test/integration/prepare_int_test.go index 900e2ae7c5..0b68b9822e 100644 --- a/test/integration/prepare_int_test.go +++ b/test/integration/prepare_int_test.go @@ -124,7 +124,7 @@ func (suite *PrepareIntegrationTestSuite) AssertConfig(target string) { func (suite *PrepareIntegrationTestSuite) TestResetExecutors() { suite.OnlyRunForTags(tagsuite.Prepare) - ts := e2e.New(suite.T(), true) + ts := e2e.New(suite.T(), false) err := ts.ClearCache() suite.Require().NoError(err) defer ts.Close() From 05e069ccaea5c1e4c00fb4041b20be90451b155c Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Fri, 13 Sep 2024 15:01:51 -0700 Subject: [PATCH 45/49] Add increased timeout --- test/integration/install_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go index f75de95ffa..602631e592 100644 --- a/test/integration/install_int_test.go +++ b/test/integration/install_int_test.go @@ -40,7 +40,7 @@ func (suite *InstallIntegrationTestSuite) TestInstallSuggest() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "djang") - cp.Expect("No results found") + cp.Expect("No results found", e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Did you mean") cp.Expect("language/python/django") cp.ExpectExitCode(1) From 7a0c29daf63770dd70e89af6840997df42f1fb80 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 19 Sep 2024 09:15:17 -0700 Subject: [PATCH 46/49] Fix test; not guaranteed to return django --- test/integration/install_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/install_int_test.go b/test/integration/install_int_test.go index 602631e592..c47d2c2d84 100644 --- a/test/integration/install_int_test.go +++ b/test/integration/install_int_test.go @@ -42,7 +42,7 @@ func (suite *InstallIntegrationTestSuite) TestInstallSuggest() { cp = ts.Spawn("install", "djang") cp.Expect("No results found", e2e.RuntimeSolvingTimeoutOpt) cp.Expect("Did you mean") - cp.Expect("language/python/django") + cp.Expect("language/python/djang") cp.ExpectExitCode(1) } From ed949e5a3ebcc0356a9fad975f7a302868cbd7be Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 19 Sep 2024 09:39:01 -0700 Subject: [PATCH 47/49] Satisfy newline checker which is apparently bugged cause I didn't touch this file --- internal/assets/contents/shells/zshrc_global.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/assets/contents/shells/zshrc_global.sh b/internal/assets/contents/shells/zshrc_global.sh index 9f19cbfbe8..c2cee91028 100644 --- a/internal/assets/contents/shells/zshrc_global.sh +++ b/internal/assets/contents/shells/zshrc_global.sh @@ -8,4 +8,4 @@ export {{$K}}="{{$V}}:$PATH" {{- else}} export {{$K}}="{{$V}}" {{- end}} -{{- end}} \ No newline at end of file +{{- end}} From 4b0cca855aae984421c97e5fe64713fe34b204d7 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 19 Sep 2024 10:54:53 -0700 Subject: [PATCH 48/49] Fix unit test using removed function --- pkg/buildscript/mutations_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/buildscript/mutations_test.go b/pkg/buildscript/mutations_test.go index b0dc20479b..f3cff0996f 100644 --- a/pkg/buildscript/mutations_test.go +++ b/pkg/buildscript/mutations_test.go @@ -347,7 +347,11 @@ func TestUpdatePlatform(t *testing.T) { script, err := UnmarshalBuildExpression(data, nil) assert.NoError(t, err) - err = script.UpdatePlatform(tt.args.operation, tt.args.platform) + if tt.args.operation == types.OperationAdded { + err = script.AddPlatform(tt.args.platform) + } else { + err = script.RemovePlatform(tt.args.platform) + } if err != nil { if tt.wantErr { return From 2ae56e031c5f9a4cfdda781417e1de4598dbdfe1 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Thu, 19 Sep 2024 10:55:15 -0700 Subject: [PATCH 49/49] Give more time for double solve --- test/integration/package_int_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index e50de9db0a..9660ab1891 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -3,11 +3,13 @@ package integration import ( "strings" "testing" + "time" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/suite" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/ActiveState/termtest" ) type PackageIntegrationTestSuite struct { @@ -273,7 +275,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_operation_multiple() { suite.Run("install", func() { cp := ts.Spawn("install", "requests", "urllib3@1.25.6") cp.Expect("Operating on project ActiveState-CLI/small-python") - cp.Expect("Added: language/python/requests", e2e.RuntimeSolvingTimeoutOpt) + cp.Expect("Added: language/python/requests", termtest.OptExpectTimeout(2*time.Minute)) // Extra time because 2 packages cp.Expect("Added: language/python/urllib3") cp.Wait() })