Skip to content

Ecosystem implementation #3683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions pkg/runtime/depot.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ type deployment struct {
type deploymentType string

const (
deploymentTypeLink deploymentType = "link"
deploymentTypeCopy = "copy"
deploymentTypeLink deploymentType = "link"
deploymentTypeCopy = "copy"
deploymentTypeEcosystem = "ecosystem"
)

type artifactInfo struct {
Expand Down Expand Up @@ -212,16 +213,12 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
}

// Record deployment to config
if _, ok := d.config.Deployments[id]; !ok {
d.config.Deployments[id] = []deployment{}
}
d.config.Deployments[id] = append(d.config.Deployments[id], deployment{
err = d.Track(id, &deployment{
Type: deploymentTypeLink,
Path: absoluteDest,
Files: files.RelativePaths(),
RelativeSrc: relativeSrc,
})
err = d.recordUse(id)
if err != nil {
return errs.Wrap(err, "Could not record artifact use")
}
Expand Down Expand Up @@ -273,24 +270,31 @@ func (d *depot) DeployViaCopy(id strfmt.UUID, relativeSrc, absoluteDest string)
}

// Record deployment to config
if _, ok := d.config.Deployments[id]; !ok {
d.config.Deployments[id] = []deployment{}
}
d.config.Deployments[id] = append(d.config.Deployments[id], deployment{
err = d.Track(id, &deployment{
Type: deploymentTypeCopy,
Path: absoluteDest,
Files: files.RelativePaths(),
RelativeSrc: relativeSrc,
})
err = d.recordUse(id)
if err != nil {
return errs.Wrap(err, "Could not record artifact use")
}

return nil
}

func (d *depot) recordUse(id strfmt.UUID) error {
// Track will record an artifact deployment.
// This is automatically called by `DeployVia*()` functions.
// This should be called for ecosystems that handle installation of artifacts.
func (d *depot) Track(id strfmt.UUID, deploy *deployment) error {
// Record deployment of this artifact.
if _, ok := d.config.Deployments[id]; !ok {
d.config.Deployments[id] = []deployment{}
}
if deploy != nil {
d.config.Deployments[id] = append(d.config.Deployments[id], *deploy)
}

// Ensure a cache entry for this artifact exists and then update its last access time.
if _, exists := d.config.Cache[id]; !exists {
size, err := fileutils.GetDirSize(d.Path(id))
Expand All @@ -304,6 +308,17 @@ func (d *depot) recordUse(id strfmt.UUID) error {
return nil
}

// Untrack will remove an artifact deployment.
// It does not actually delete files; it just tells the depot a previously tracked artifact should
// no longer be tracked.
// This is automatically called by the `Undeploy()` function.
// This should be called for ecosystems that handle uninstallation of artifacts.
func (d *depot) Untrack(id strfmt.UUID, path string) {
if _, ok := d.config.Deployments[id]; ok {
d.config.Deployments[id] = sliceutils.Filter(d.config.Deployments[id], func(d deployment) bool { return d.Path != path })
}
}

func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
d.fsMutex.Lock()
defer d.fsMutex.Unlock()
Expand Down Expand Up @@ -357,7 +372,7 @@ func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
}

// Write changes to config
d.config.Deployments[id] = sliceutils.Filter(d.config.Deployments[id], func(d deployment) bool { return d.Path != path })
d.Untrack(id, path)

return nil
}
Expand Down Expand Up @@ -427,7 +442,7 @@ func (d *depot) Save() error {
for id := range d.artifacts {
if deployments, ok := d.config.Deployments[id]; !ok || len(deployments) == 0 {
if _, exists := d.config.Cache[id]; !exists {
err := d.recordUse(id)
err := d.Track(id, nil) // create cache entry for previously used artifact
if err != nil {
return errs.Wrap(err, "Could not update depot cache with previously used artifact")
}
Expand Down
60 changes: 60 additions & 0 deletions pkg/runtime/ecosystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package runtime

import "github.com/ActiveState/cli/pkg/buildplan"

// eg.
// availableEcosystems = []func() (ecosystem, error){
// func() (ecosystem, error) {
// return &python.Ecosystem{}, nil
// },
// }
var availableEcosystems []func() ecosystem

type ecosystem interface {
Init(runtimePath string, buildplan *buildplan.BuildPlan) error
Namespaces() []string
Add(artifact *buildplan.Artifact, artifactSrcPath string) ([]string, error)
Remove(artifact *buildplan.Artifact) error
Apply() error
}

func artifactMatchesEcosystem(a *buildplan.Artifact, e ecosystem) bool {
for _, namespace := range e.Namespaces() {
for _, i := range a.Ingredients {
if i.Namespace == namespace {
return true
}
}
}
return false
}

func namespacesMatchesEcosystem(namespaces []string, e ecosystem) bool {
for _, namespace := range e.Namespaces() {
for _, n := range namespaces {
if n == namespace {
return true
}
}
}
return false
}

func filterEcosystemMatchingArtifact(artifact *buildplan.Artifact, ecosystems []ecosystem) ecosystem {
for _, ecosystem := range ecosystems {
if artifactMatchesEcosystem(artifact, ecosystem) {
return ecosystem
}
}
return nil
}

func filterEcosystemsMatchingNamespaces(ecosystems []ecosystem, namespaces []string) []ecosystem {
result := []ecosystem{}
for _, ecosystem := range ecosystems {
if namespacesMatchesEcosystem(namespaces, ecosystem) {
result = append(result, ecosystem)
}
}
return result
}
62 changes: 57 additions & 5 deletions pkg/runtime/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type setup struct {
path string
opts *Opts
depot *depot
ecosystems []ecosystem
supportsHardLinks bool
env *envdef.Collection
buildplan *buildplan.BuildPlan
Expand All @@ -90,7 +91,7 @@ type setup struct {
toInstall buildplan.ArtifactIDMap

// toUninstall encompasses all artifacts that will need to be uninstalled for this runtime.
toUninstall map[strfmt.UUID]struct{}
toUninstall map[strfmt.UUID]bool
Copy link
Contributor

@mitchell-as mitchell-as Jul 15, 2025

Choose a reason for hiding this comment

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

The depot does not have enough information to construct a buildplan.Artifact object. The depot only tracks artifact IDs, and we infer what artifacts to uninstall by IDs that do not exist in the incoming buildplan. I am changing from struct{} to bool to better reflect this for now, as it was a source of confusion.

CP-956, which is to implement Remove() and Untrack() should hopefully figure out how best to accomplish what we want.

}

func newSetup(path string, bp *buildplan.BuildPlan, env *envdef.Collection, depot *depot, opts *Opts) (*setup, error) {
Expand Down Expand Up @@ -127,10 +128,10 @@ func newSetup(path string, bp *buildplan.BuildPlan, env *envdef.Collection, depo

// Identify which artifacts we can uninstall
installableArtifactsMap := installableArtifacts.ToIDMap()
artifactsToUninstall := map[strfmt.UUID]struct{}{}
artifactsToUninstall := map[strfmt.UUID]bool{}
for id := range installedArtifacts {
if _, required := installableArtifactsMap[id]; !required {
artifactsToUninstall[id] = struct{}{}
artifactsToUninstall[id] = true
}
}

Expand Down Expand Up @@ -167,6 +168,16 @@ func newSetup(path string, bp *buildplan.BuildPlan, env *envdef.Collection, depo
}
}

// Load all ecosystems
var ecosystems []ecosystem
for _, e := range availableEcosystems {
ecosystem := e()
if err := ecosystem.Init(path, bp); err != nil {
return nil, errs.Wrap(err, "Could not create ecosystem")
}
ecosystems = append(ecosystems, ecosystem)
}

return &setup{
path: path,
opts: opts,
Expand All @@ -179,6 +190,7 @@ func newSetup(path string, bp *buildplan.BuildPlan, env *envdef.Collection, depo
toUnpack: artifactsToUnpack.ToIDMap(),
toInstall: artifactsToInstall.ToIDMap(),
toUninstall: artifactsToUninstall,
ecosystems: ecosystems,
}, nil
}

Expand Down Expand Up @@ -269,11 +281,18 @@ func (s *setup) update() error {
}
}

// Tell applicable ecosystems to perform any final uninstall actions.
Copy link
Contributor

@mitchell-as mitchell-as Jul 15, 2025

Choose a reason for hiding this comment

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

I'm not convinced we can have Apply() do both uninstall and install at the same time, so I'm calling Apply() here after all of the Remove() calls, and then again later in this file after all of the Add() calls.

Feel free to correct me on this.

for _, e := range s.ecosystems {
if err := e.Apply(); err != nil {
return errs.Wrap(err, "Could not apply ecosystem changes")
}
}

// Install artifacts
wp = workerpool.New(maxConcurrency)
for _, a := range s.toInstall {
wp.Submit(func() error {
if err := s.install(a.ArtifactID); err != nil {
if err := s.install(a); err != nil {
return errs.Wrap(err, "Could not install artifact")
}
return nil
Expand All @@ -285,6 +304,13 @@ func (s *setup) update() error {
return errs.Wrap(err, "errors occurred during install")
}

// Tell applicable ecosystems to perform any final install actions.
for _, e := range s.ecosystems {
if err := e.Apply(); err != nil {
return errs.Wrap(err, "Could not apply ecosystem changes")
}
}

if err := s.postProcess(); err != nil {
return errs.Wrap(err, "Postprocessing failed")
}
Expand Down Expand Up @@ -447,7 +473,8 @@ func (s *setup) updateExecutors() error {
return nil
}

func (s *setup) install(id strfmt.UUID) (rerr error) {
func (s *setup) install(artifact *buildplan.Artifact) (rerr error) {
id := artifact.ArtifactID
defer func() {
if rerr == nil {
if err := s.fireEvent(events.ArtifactInstallSuccess{id}); err != nil {
Expand All @@ -465,6 +492,20 @@ func (s *setup) install(id strfmt.UUID) (rerr error) {
}

artifactDepotPath := s.depot.Path(id)

if ecosys := filterEcosystemMatchingArtifact(artifact, s.ecosystems); ecosys != nil {
files, err := ecosys.Add(artifact, artifactDepotPath)
if err != nil {
return errs.Wrap(err, "Ecosystem unable to add artifact")
}
s.depot.Track(id, &deployment{
Type: deploymentTypeEcosystem,
Path: filepath.Join(s.path, artifact.ArtifactID.String()), // dummy path for uniqueness
Files: files,
})
return nil
}

envDef, err := s.env.Load(artifactDepotPath)
if err != nil {
return errs.Wrap(err, "Could not get env")
Expand Down Expand Up @@ -506,6 +547,17 @@ func (s *setup) uninstall(id strfmt.UUID) (rerr error) {
}

artifactDepotPath := s.depot.Path(id)

// TODO: CP-956
//if ecosys := filterEcosystemMatchingArtifact(artifact, s.ecosystems); ecosys != nil {
// err := ecosys.Remove(artifact)
Copy link
Contributor

Choose a reason for hiding this comment

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

The depot does not have enough information to construct an artifact (buildplan.Artifact), so CP-956 will have to figure out what approach to take when re-enabling this block of code.

// if err != nil {
// return errs.Wrap(err, "Ecosystem unable to remove artifact")
// }
// s.depot.Untrack(id, filepath.Join(s.path, artifact.ArtifactID.String()))
// return nil
//}

envDef, err := s.env.Load(artifactDepotPath)
if err != nil {
return errs.Wrap(err, "Could not get env")
Expand Down
Loading