github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/setup/setup.go (about)

     1  package setup
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  	rt "runtime"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/ActiveState/cli/internal/analytics"
    17  	anaConsts "github.com/ActiveState/cli/internal/analytics/constants"
    18  	"github.com/ActiveState/cli/internal/analytics/dimensions"
    19  	"github.com/ActiveState/cli/internal/condition"
    20  	"github.com/ActiveState/cli/internal/constants"
    21  	"github.com/ActiveState/cli/internal/errs"
    22  	"github.com/ActiveState/cli/internal/fileutils"
    23  	"github.com/ActiveState/cli/internal/graph"
    24  	"github.com/ActiveState/cli/internal/httputil"
    25  	"github.com/ActiveState/cli/internal/locale"
    26  	"github.com/ActiveState/cli/internal/logging"
    27  	"github.com/ActiveState/cli/internal/multilog"
    28  	"github.com/ActiveState/cli/internal/output"
    29  	"github.com/ActiveState/cli/internal/proxyreader"
    30  	"github.com/ActiveState/cli/internal/rollbar"
    31  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    32  	"github.com/ActiveState/cli/internal/runbits/dependencies"
    33  	"github.com/ActiveState/cli/internal/sliceutils"
    34  	"github.com/ActiveState/cli/internal/svcctl"
    35  	"github.com/ActiveState/cli/internal/unarchiver"
    36  	"github.com/ActiveState/cli/pkg/buildplan"
    37  	"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
    38  	"github.com/ActiveState/cli/pkg/platform/authentication"
    39  	"github.com/ActiveState/cli/pkg/platform/model"
    40  	bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner"
    41  	"github.com/ActiveState/cli/pkg/platform/runtime/artifactcache"
    42  	"github.com/ActiveState/cli/pkg/platform/runtime/buildexpression"
    43  	"github.com/ActiveState/cli/pkg/platform/runtime/buildscript"
    44  	"github.com/ActiveState/cli/pkg/platform/runtime/envdef"
    45  	"github.com/ActiveState/cli/pkg/platform/runtime/executors"
    46  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/buildlog"
    47  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/events"
    48  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/events/progress"
    49  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/implementations/alternative"
    50  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/implementations/camel"
    51  	"github.com/ActiveState/cli/pkg/platform/runtime/store"
    52  	"github.com/ActiveState/cli/pkg/platform/runtime/target"
    53  	"github.com/ActiveState/cli/pkg/platform/runtime/validate"
    54  	"github.com/ActiveState/cli/pkg/sysinfo"
    55  	"github.com/faiface/mainthread"
    56  	"github.com/gammazero/workerpool"
    57  	"github.com/go-openapi/strfmt"
    58  )
    59  
    60  // MaxConcurrency is maximum number of parallel artifact installations
    61  const MaxConcurrency = 5
    62  
    63  // NotInstalledError is an error returned when the runtime is not completely installed yet.
    64  var NotInstalledError = errs.New("Runtime is not completely installed.")
    65  
    66  // BuildError designates a recipe build error.
    67  type BuildError struct {
    68  	*locale.LocalizedError
    69  }
    70  
    71  // ArtifactDownloadError designates an error downloading an artifact.
    72  type ArtifactDownloadError struct {
    73  	*errs.WrapperError
    74  }
    75  
    76  // ArtifactCachedBuildFailed designates an error due to a build for an artifact that failed and has been cached
    77  type ArtifactCachedBuildFailed struct {
    78  	*errs.WrapperError
    79  	Artifact *buildplan.Artifact
    80  }
    81  
    82  // ArtifactInstallError designates an error installing a downloaded artifact.
    83  type ArtifactInstallError struct {
    84  	*errs.WrapperError
    85  }
    86  
    87  // ArtifactSetupErrors combines all errors that can happen while installing artifacts in parallel
    88  type ArtifactSetupErrors struct {
    89  	errs []error
    90  }
    91  
    92  type ExecutorSetupError struct {
    93  	*errs.WrapperError
    94  }
    95  
    96  func (a *ArtifactSetupErrors) Error() string {
    97  	var errors []string
    98  	for _, err := range a.errs {
    99  		errors = append(errors, errs.JoinMessage(err))
   100  	}
   101  	return "Not all artifacts could be installed, errors:\n" + strings.Join(errors, "\n")
   102  }
   103  
   104  func (a *ArtifactSetupErrors) Unwrap() []error {
   105  	return a.errs
   106  }
   107  
   108  // Errors returns the individual error messages collected from all failing artifact installations
   109  func (a *ArtifactSetupErrors) Errors() []error {
   110  	return a.errs
   111  }
   112  
   113  // UserError returns a message including all user-facing sub-error messages
   114  func (a *ArtifactSetupErrors) LocalizedError() string {
   115  	var errStrings []string
   116  	for _, err := range a.errs {
   117  		errStrings = append(errStrings, locale.JoinedErrorMessage(err))
   118  	}
   119  	return locale.Tl("setup_artifacts_err", "Not all artifacts could be installed:\n{{.V0}}", strings.Join(errStrings, "\n"))
   120  }
   121  
   122  // ProgressReportError designates an error in the event handler for reporting progress.
   123  type ProgressReportError struct {
   124  	*errs.WrapperError
   125  }
   126  
   127  type RuntimeInUseError struct {
   128  	*locale.LocalizedError
   129  	Processes []*graph.ProcessInfo
   130  }
   131  
   132  type Targeter interface {
   133  	CommitUUID() strfmt.UUID
   134  	Name() string
   135  	Owner() string
   136  	Dir() string
   137  	Trigger() target.Trigger
   138  	ProjectDir() string
   139  
   140  	// ReadOnly communicates that this target should only use cached runtime information (ie. don't check for updates)
   141  	ReadOnly() bool
   142  	// InstallFromDir communicates that this target should only install artifacts from the given directory (i.e. offline installer)
   143  	InstallFromDir() *string
   144  }
   145  
   146  type Configurable interface {
   147  	GetString(key string) string
   148  	GetBool(key string) bool
   149  }
   150  
   151  type Setup struct {
   152  	auth          *authentication.Auth
   153  	target        Targeter
   154  	eventHandler  events.Handler
   155  	store         *store.Store
   156  	analytics     analytics.Dispatcher
   157  	artifactCache *artifactcache.ArtifactCache
   158  	cfg           Configurable
   159  	out           output.Outputer
   160  	svcm          *model.SvcModel
   161  }
   162  
   163  type Setuper interface {
   164  	// DeleteOutdatedArtifacts deletes outdated artifact as best as it can
   165  	DeleteOutdatedArtifacts(*buildplan.ArtifactChangeset, store.StoredArtifactMap, store.StoredArtifactMap) error
   166  }
   167  
   168  // ArtifactSetuper is the interface for an implementation of artifact setup functions
   169  // These need to be specialized for each BuildEngine type
   170  type ArtifactSetuper interface {
   171  	EnvDef(tmpInstallDir string) (*envdef.EnvironmentDefinition, error)
   172  	Unarchiver() unarchiver.Unarchiver
   173  }
   174  
   175  type ArtifactResolver interface {
   176  	ResolveArtifactName(strfmt.UUID) string
   177  }
   178  
   179  type artifactInstaller func(strfmt.UUID, string, ArtifactSetuper) error
   180  type artifactUninstaller func() error
   181  
   182  // New returns a new Setup instance that can install a Runtime locally on the machine.
   183  func New(target Targeter, eventHandler events.Handler, auth *authentication.Auth, an analytics.Dispatcher, cfg Configurable, out output.Outputer, svcm *model.SvcModel) *Setup {
   184  	cache, err := artifactcache.New()
   185  	if err != nil {
   186  		multilog.Error("Could not create artifact cache: %v", err)
   187  	}
   188  	return &Setup{auth, target, eventHandler, store.New(target.Dir()), an, cache, cfg, out, svcm}
   189  }
   190  
   191  func (s *Setup) Solve() (*bpModel.Commit, error) {
   192  	defer func() {
   193  		s.solveUpdateRecover(recover())
   194  	}()
   195  
   196  	if s.target.InstallFromDir() != nil {
   197  		return nil, nil
   198  	}
   199  
   200  	if err := s.handleEvent(events.SolveStart{}); err != nil {
   201  		return nil, errs.Wrap(err, "Could not handle SolveStart event")
   202  	}
   203  
   204  	bpm := bpModel.NewBuildPlannerModel(s.auth)
   205  	commit, err := bpm.FetchCommit(s.target.CommitUUID(), s.target.Owner(), s.target.Name(), nil)
   206  	if err != nil {
   207  		return nil, errs.Wrap(err, "Failed to fetch build result")
   208  	}
   209  
   210  	if err := s.eventHandler.Handle(events.SolveSuccess{}); err != nil {
   211  		return nil, errs.Wrap(err, "Could not handle SolveSuccess event")
   212  	}
   213  
   214  	return commit, nil
   215  }
   216  
   217  func (s *Setup) Update(commit *bpModel.Commit) (rerr error) {
   218  	defer func() {
   219  		s.solveUpdateRecover(recover())
   220  	}()
   221  	defer func() {
   222  		var ev events.Eventer = events.Success{}
   223  		if rerr != nil {
   224  			ev = events.Failure{}
   225  		}
   226  
   227  		err := s.handleEvent(ev)
   228  		if err != nil {
   229  			multilog.Error("Could not handle Success/Failure event: %s", errs.JoinMessage(err))
   230  		}
   231  	}()
   232  
   233  	bp := commit.BuildPlan()
   234  
   235  	// Do not allow users to deploy runtimes to the root directory (this can easily happen in docker
   236  	// images). Note that runtime targets are fully resolved via fileutils.ResolveUniquePath(), so
   237  	// paths like "/." and "/opt/.." resolve to simply "/" at this time.
   238  	if rt.GOOS != "windows" && s.target.Dir() == "/" {
   239  		return locale.NewInputError("err_runtime_setup_root", "Cannot set up a runtime in the root directory. Please specify or run from a user-writable directory.")
   240  	}
   241  
   242  	// Determine if this runtime is currently in use.
   243  	ctx, cancel := context.WithTimeout(context.Background(), model.SvcTimeoutMinimal)
   244  	defer cancel()
   245  	if procs, err := s.svcm.GetProcessesInUse(ctx, ExecDir(s.target.Dir())); err == nil {
   246  		if len(procs) > 0 {
   247  			list := []string{}
   248  			for _, proc := range procs {
   249  				list = append(list, fmt.Sprintf("   - %s (process: %d)", proc.Exe, proc.Pid))
   250  			}
   251  			return &RuntimeInUseError{locale.NewInputError("runtime_setup_in_use_err", "", strings.Join(list, "\n")), procs}
   252  		}
   253  	} else {
   254  		multilog.Error("Unable to determine if runtime is in use: %v", errs.JoinMessage(err))
   255  	}
   256  
   257  	// Update all the runtime artifacts
   258  	artifacts, err := s.updateArtifacts(bp)
   259  	if err != nil {
   260  		return errs.Wrap(err, "Failed to update artifacts")
   261  	}
   262  
   263  	if err := s.store.StoreBuildPlan(bp); err != nil {
   264  		return errs.Wrap(err, "Could not save recipe file.")
   265  	}
   266  
   267  	expression, err := buildexpression.New(commit.Expression)
   268  	if err != nil {
   269  		return errs.Wrap(err, "failed to parse build expression")
   270  	}
   271  
   272  	script, err := buildscript.NewFromBuildExpression(&commit.AtTime, expression)
   273  	if err != nil {
   274  		return errs.Wrap(err, "Could not convert to buildscript")
   275  	}
   276  
   277  	if err := s.store.StoreBuildScript(script); err != nil {
   278  		return errs.Wrap(err, "Could not store buildscript file.")
   279  	}
   280  
   281  	if s.target.ProjectDir() != "" && s.cfg.GetBool(constants.OptinBuildscriptsConfig) {
   282  		if err := buildscript.Update(s.target, &commit.AtTime, expression); err != nil {
   283  			return errs.Wrap(err, "Could not update build script")
   284  		}
   285  	}
   286  
   287  	// Update executors
   288  	if err := s.updateExecutors(artifacts); err != nil {
   289  		return ExecutorSetupError{errs.Wrap(err, "Failed to update executors")}
   290  	}
   291  
   292  	// Mark installation as completed
   293  	if err := s.store.MarkInstallationComplete(s.target.CommitUUID(), fmt.Sprintf("%s/%s", s.target.Owner(), s.target.Name())); err != nil {
   294  		return errs.Wrap(err, "Could not mark install as complete.")
   295  	}
   296  
   297  	return nil
   298  }
   299  
   300  // Panics are serious, and reproducing them in the runtime package is HARD. To help with this we dump
   301  // the build plan when a panic occurs so we have something more to go on.
   302  func (s *Setup) solveUpdateRecover(r interface{}) {
   303  	if r == nil {
   304  		return
   305  	}
   306  
   307  	multilog.Critical("Panic during runtime update: %s", r)
   308  	panic(r) // We're just logging the panic while we have context, we're not meant to handle it here
   309  }
   310  
   311  func (s *Setup) updateArtifacts(bp *buildplan.BuildPlan) ([]strfmt.UUID, error) {
   312  	mutex := &sync.Mutex{}
   313  	var installArtifactFuncs []func() error
   314  
   315  	// Fetch and install each runtime artifact.
   316  	// Note: despite the name, we are "pre-installing" the artifacts to a temporary location.
   317  	// Once all artifacts are fetched, unpacked, and prepared, final installation occurs.
   318  	artifacts, uninstallFunc, err := s.fetchAndInstallArtifacts(bp, func(a strfmt.UUID, archivePath string, as ArtifactSetuper) (rerr error) {
   319  		defer func() {
   320  			if rerr != nil {
   321  				rerr = &ArtifactInstallError{errs.Wrap(rerr, "Unable to install artifact")}
   322  				if err := s.handleEvent(events.ArtifactInstallFailure{a, rerr}); err != nil {
   323  					rerr = errs.Wrap(rerr, "Could not handle ArtifactInstallFailure event")
   324  					return
   325  				}
   326  			}
   327  			if err := s.handleEvent(events.ArtifactInstallSuccess{a}); err != nil {
   328  				rerr = errs.Wrap(rerr, "Could not handle ArtifactInstallSuccess event")
   329  				return
   330  			}
   331  		}()
   332  
   333  		// Set up target and unpack directories
   334  		targetDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory)
   335  		if err := fileutils.MkdirUnlessExists(targetDir); err != nil {
   336  			return errs.Wrap(err, "Could not create temp runtime dir")
   337  		}
   338  		unpackedDir := filepath.Join(targetDir, a.String())
   339  
   340  		logging.Debug("Unarchiving %s to %s", archivePath, unpackedDir)
   341  
   342  		// ensure that the unpack dir is empty
   343  		err := os.RemoveAll(unpackedDir)
   344  		if err != nil {
   345  			return errs.Wrap(err, "Could not remove previous temporary installation directory.")
   346  		}
   347  
   348  		// Unpack artifact archive
   349  		numFiles, err := s.unpackArtifact(as.Unarchiver(), archivePath, unpackedDir, &progress.Report{
   350  			ReportSizeCb: func(size int) error {
   351  				if err := s.handleEvent(events.ArtifactInstallStarted{a, size}); err != nil {
   352  					return errs.Wrap(err, "Could not handle ArtifactInstallStarted event")
   353  				}
   354  				return nil
   355  			},
   356  			ReportIncrementCb: func(inc int) error {
   357  				if err := s.handleEvent(events.ArtifactInstallProgress{a, inc}); err != nil {
   358  					return errs.Wrap(err, "Could not handle ArtifactInstallProgress event")
   359  				}
   360  				return nil
   361  			},
   362  		})
   363  		if err != nil {
   364  			err := errs.Wrap(err, "Could not unpack artifact %s", archivePath)
   365  			return err
   366  		}
   367  
   368  		// Set up constants used to expand environment definitions
   369  		cnst, err := envdef.NewConstants(s.store.InstallPath())
   370  		if err != nil {
   371  			return errs.Wrap(err, "Could not get new environment constants")
   372  		}
   373  
   374  		// Retrieve environment definitions for artifact
   375  		envDef, err := as.EnvDef(unpackedDir)
   376  		if err != nil {
   377  			return errs.Wrap(err, "Could not collect env info for artifact")
   378  		}
   379  
   380  		// Expand environment definitions using constants
   381  		envDef = envDef.ExpandVariables(cnst)
   382  		err = envDef.ApplyFileTransforms(filepath.Join(unpackedDir, envDef.InstallDir), cnst)
   383  		if err != nil {
   384  			return locale.WrapError(err, "runtime_alternative_file_transforms_err", "", "Could not apply necessary file transformations after unpacking")
   385  		}
   386  
   387  		mutex.Lock()
   388  		installArtifactFuncs = append(installArtifactFuncs, func() error {
   389  			return s.moveToInstallPath(a, unpackedDir, envDef, numFiles)
   390  		})
   391  		mutex.Unlock()
   392  
   393  		return nil
   394  	})
   395  	if err != nil {
   396  		return artifacts, locale.WrapError(err, "err_runtime_setup")
   397  	}
   398  
   399  	if os.Getenv(constants.RuntimeSetupWaitEnvVarName) != "" && (condition.OnCI() || condition.BuiltOnDevMachine()) {
   400  		// This code block is for integration testing purposes only.
   401  		// Under normal conditions, we should never access fmt or os.Stdin from this context.
   402  		fmt.Printf("Waiting for input because %s was set\n", constants.RuntimeSetupWaitEnvVarName)
   403  		ch := make([]byte, 1)
   404  		_, err = os.Stdin.Read(ch) // block until input is sent
   405  		if err != nil {
   406  			return artifacts, locale.WrapError(err, "err_runtime_setup")
   407  		}
   408  	}
   409  
   410  	// Uninstall outdated artifacts.
   411  	// This must come before calling any installArtifactFuncs or else the runtime may become corrupt.
   412  	if uninstallFunc != nil {
   413  		err := uninstallFunc()
   414  		if err != nil {
   415  			return artifacts, locale.WrapError(err, "err_runtime_setup")
   416  		}
   417  	}
   418  
   419  	// Move files to final installation path after successful download and unpack.
   420  	for _, f := range installArtifactFuncs {
   421  		err := f()
   422  		if err != nil {
   423  			return artifacts, locale.WrapError(err, "err_runtime_setup")
   424  		}
   425  	}
   426  
   427  	// Clean up temp directory.
   428  	tempDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory)
   429  	err = os.RemoveAll(tempDir)
   430  	if err != nil {
   431  		multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Failed to remove temporary installation directory %s: %v", tempDir, err)
   432  	}
   433  
   434  	return artifacts, nil
   435  }
   436  
   437  func (s *Setup) updateExecutors(artifacts []strfmt.UUID) error {
   438  	execPath := ExecDir(s.target.Dir())
   439  	if err := fileutils.MkdirUnlessExists(execPath); err != nil {
   440  		return locale.WrapError(err, "err_deploy_execpath", "Could not create exec directory.")
   441  	}
   442  
   443  	edGlobal, err := s.store.UpdateEnviron(artifacts)
   444  	if err != nil {
   445  		return errs.Wrap(err, "Could not save combined environment file")
   446  	}
   447  
   448  	exePaths, err := edGlobal.ExecutablePaths()
   449  	if err != nil {
   450  		return locale.WrapError(err, "err_deploy_execpaths", "Could not retrieve runtime executable paths")
   451  	}
   452  
   453  	env, err := s.store.Environ(false)
   454  	if err != nil {
   455  		return locale.WrapError(err, "err_setup_get_runtime_env", "Could not retrieve runtime environment")
   456  	}
   457  
   458  	execInit := executors.New(execPath)
   459  	if err := execInit.Apply(svcctl.NewIPCSockPathFromGlobals().String(), s.target, env, exePaths); err != nil {
   460  		return locale.WrapError(err, "err_deploy_executors", "Could not create executors")
   461  	}
   462  
   463  	return nil
   464  }
   465  
   466  // fetchAndInstallArtifacts returns all artifacts needed by the runtime, even if some or
   467  // all of them were already installed.
   468  // It may also return an artifact uninstaller function that should be run prior to final
   469  // installation.
   470  func (s *Setup) fetchAndInstallArtifacts(bp *buildplan.BuildPlan, installFunc artifactInstaller) ([]strfmt.UUID, artifactUninstaller, error) {
   471  	if s.target.InstallFromDir() != nil {
   472  		artifacts, err := s.fetchAndInstallArtifactsFromDir(installFunc)
   473  		return artifacts, nil, err
   474  	}
   475  	return s.fetchAndInstallArtifactsFromBuildPlan(bp, installFunc)
   476  }
   477  
   478  func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(bp *buildplan.BuildPlan, installFunc artifactInstaller) ([]strfmt.UUID, artifactUninstaller, error) {
   479  	// If the build is not ready or if we are installing the buildtime closure
   480  	// then we need to include the buildtime closure in the changed artifacts
   481  	// and the progress reporting.
   482  	includeBuildtimeClosure := strings.EqualFold(os.Getenv(constants.InstallBuildDependencies), "true") || !bp.IsBuildReady()
   483  
   484  	platformID, err := model.FilterCurrentPlatform(sysinfo.OS().String(), bp.Platforms(), s.cfg)
   485  	if err != nil {
   486  		return nil, nil, locale.WrapError(err, "err_filter_current_platform")
   487  	}
   488  
   489  	artifactFilters := []buildplan.FilterArtifact{
   490  		buildplan.FilterStateArtifacts(),
   491  		buildplan.FilterPlatformArtifacts(platformID),
   492  	}
   493  
   494  	// Compute and handle the change summary
   495  	allArtifacts := bp.Artifacts(artifactFilters...)
   496  
   497  	// Detect failed artifacts early
   498  	for _, a := range allArtifacts {
   499  		var aErr error
   500  		if a.Status == types.ArtifactFailedPermanently || a.Status == types.ArtifactFailedTransiently {
   501  			errV := &ArtifactCachedBuildFailed{errs.New("artifact failed, status: %s", a.Status), a}
   502  			if aErr == nil {
   503  				aErr = errV
   504  			} else {
   505  				aErr = errs.Pack(aErr, errV)
   506  			}
   507  		}
   508  		if aErr != nil {
   509  			return nil, nil, aErr
   510  		}
   511  	}
   512  
   513  	if len(allArtifacts) == 0 {
   514  		v, err := json.Marshal(bp.Artifacts())
   515  		if err != nil {
   516  			return nil, nil, err
   517  		}
   518  		return nil, nil, errs.New("did not find any artifacts that match our platform (%s), full artifacts list: %s", platformID, v)
   519  	}
   520  
   521  	resolver, err := selectArtifactResolver(bp)
   522  	if err != nil {
   523  		return nil, nil, errs.Wrap(err, "Failed to select artifact resolver")
   524  	}
   525  
   526  	// build results don't have namespace info and will happily report internal only artifacts
   527  	downloadablePrebuiltArtifacts := sliceutils.Filter(allArtifacts, func(a *buildplan.Artifact) bool {
   528  		return a.Status == types.ArtifactSucceeded && a.URL != ""
   529  	})
   530  
   531  	// Analytics data to send.
   532  	dimensions := &dimensions.Values{
   533  		CommitID: ptr.To(s.target.CommitUUID().String()),
   534  	}
   535  
   536  	// send analytics build event, if a new runtime has to be built in the cloud
   537  	if bp.IsBuildInProgress() {
   538  		s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeBuild, dimensions)
   539  	}
   540  
   541  	oldBuildPlan, err := s.store.BuildPlan()
   542  	if err != nil && !errors.As(err, ptr.To(&store.ErrVersionMarker{})) {
   543  		return nil, nil, errs.Wrap(err, "could not load existing build plan")
   544  	}
   545  
   546  	var oldBuildPlanArtifacts buildplan.Artifacts
   547  	var changedArtifacts *buildplan.ArtifactChangeset
   548  	if oldBuildPlan != nil {
   549  		oldBuildPlanArtifacts = oldBuildPlan.Artifacts(artifactFilters...)
   550  		changedArtifacts = ptr.To(bp.DiffArtifacts(oldBuildPlan, true))
   551  	}
   552  
   553  	storedArtifacts, err := s.store.Artifacts()
   554  	if err != nil {
   555  		return nil, nil, locale.WrapError(err, "err_stored_artifacts")
   556  	}
   557  
   558  	alreadyInstalled := reusableArtifacts(allArtifacts, storedArtifacts)
   559  
   560  	artifactNamesList := []string{}
   561  	for _, a := range allArtifacts {
   562  		artifactNamesList = append(artifactNamesList, a.Name())
   563  	}
   564  	installedList := []string{}
   565  	for _, a := range alreadyInstalled {
   566  		installedList = append(installedList, resolver.ResolveArtifactName(a.ArtifactID))
   567  	}
   568  	downloadList := []string{}
   569  	for _, a := range downloadablePrebuiltArtifacts {
   570  		downloadList = append(downloadList, resolver.ResolveArtifactName(a.ArtifactID))
   571  	}
   572  	logging.Debug(
   573  		"Parsed artifacts.\nBuild ready: %v\nArtifact names: %v\nAlready installed: %v\nTo Download: %v",
   574  		bp.IsBuildReady(), artifactNamesList, installedList, downloadList,
   575  	)
   576  
   577  	filterNeedsInstall := func(a *buildplan.Artifact) bool {
   578  		_, alreadyInstalled := alreadyInstalled[a.ArtifactID]
   579  		return !alreadyInstalled
   580  	}
   581  	filters := []buildplan.FilterArtifact{filterNeedsInstall}
   582  	if !includeBuildtimeClosure {
   583  		filters = append(filters, buildplan.FilterRuntimeArtifacts())
   584  	}
   585  	artifactsToInstall := allArtifacts.Filter(filters...)
   586  	if err != nil {
   587  		return nil, nil, errs.Wrap(err, "Failed to compute artifacts to build")
   588  	}
   589  
   590  	// Output a dependency summary if applicable.
   591  	if s.target.Trigger() == target.TriggerCheckout {
   592  		dependencies.OutputSummary(s.out, bp.RequestedArtifacts())
   593  	} else if s.target.Trigger() == target.TriggerInit {
   594  		artifacts := bp.Artifacts().Filter(buildplan.FilterStateArtifacts(), buildplan.FilterRuntimeArtifacts())
   595  		dependencies.OutputSummary(s.out, artifacts)
   596  	} else if len(oldBuildPlanArtifacts) > 0 && changedArtifacts != nil {
   597  		dependencies.OutputChangeSummary(s.out, changedArtifacts, oldBuildPlanArtifacts)
   598  	}
   599  
   600  	// The log file we want to use for builds
   601  	logFilePath := logging.FilePathFor(fmt.Sprintf("build-%s.log", s.target.CommitUUID().String()+"-"+time.Now().Format("20060102150405")))
   602  
   603  	recipeID, err := bp.RecipeID()
   604  	if err != nil {
   605  		return nil, nil, errs.Wrap(err, "Could not get recipe ID from build plan")
   606  	}
   607  
   608  	artifactNameMap := map[strfmt.UUID]string{}
   609  	for _, a := range allArtifacts {
   610  		artifactNameMap[a.ArtifactID] = a.Name()
   611  	}
   612  
   613  	if err := s.eventHandler.Handle(events.Start{
   614  		RecipeID:         recipeID,
   615  		RequiresBuild:    bp.IsBuildInProgress(),
   616  		Artifacts:        artifactNameMap,
   617  		LogFilePath:      logFilePath,
   618  		ArtifactsToBuild: allArtifacts.ToIDSlice(),
   619  		// Yes these have the same value; this is intentional.
   620  		// Separating these out just allows us to be more explicit and intentional in our event handling logic.
   621  		ArtifactsToDownload: artifactsToInstall.ToIDSlice(),
   622  		ArtifactsToInstall:  artifactsToInstall.ToIDSlice(),
   623  	}); err != nil {
   624  		return nil, nil, errs.Wrap(err, "Could not handle Start event")
   625  	}
   626  
   627  	var uninstallArtifacts artifactUninstaller = func() error {
   628  		setup, err := s.selectSetupImplementation(bp.Engine())
   629  		if err != nil {
   630  			return errs.Wrap(err, "Failed to select setup implementation")
   631  		}
   632  		return s.deleteOutdatedArtifacts(setup, changedArtifacts, alreadyInstalled)
   633  	}
   634  
   635  	// only send the download analytics event, if we have to install artifacts that are not yet installed
   636  	if len(artifactsToInstall) > 0 {
   637  		// if we get here, we download artifacts
   638  		s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeDownload, dimensions)
   639  	}
   640  
   641  	err = s.installArtifactsFromBuild(bp.IsBuildReady(), bp.Engine(), recipeID, artifactsToInstall, installFunc, logFilePath)
   642  	if err != nil {
   643  		return nil, nil, err
   644  	}
   645  	err = s.artifactCache.Save()
   646  	if err != nil {
   647  		multilog.Error("Could not save artifact cache updates: %v", err)
   648  	}
   649  
   650  	artifactIDs := allArtifacts.ToIDSlice()
   651  	logging.Debug("Returning artifacts: %v", artifactIDs)
   652  	return artifactIDs, uninstallArtifacts, nil
   653  }
   654  
   655  func aggregateErrors() (chan<- error, <-chan error) {
   656  	aggErr := make(chan error)
   657  	bgErrs := make(chan error)
   658  	go func() {
   659  		var errs []error
   660  		for err := range bgErrs {
   661  			errs = append(errs, err)
   662  		}
   663  
   664  		if len(errs) > 0 {
   665  			aggErr <- &ArtifactSetupErrors{errs}
   666  		} else {
   667  			aggErr <- nil
   668  		}
   669  	}()
   670  
   671  	return bgErrs, aggErr
   672  }
   673  
   674  func (s *Setup) installArtifactsFromBuild(isReady bool, engine types.BuildEngine, recipeID strfmt.UUID, artifacts buildplan.Artifacts, installFunc artifactInstaller, logFilePath string) error {
   675  	// Artifacts are installed in two stages
   676  	// - The first stage runs concurrently in MaxConcurrency worker threads (download, unpacking, relocation)
   677  	// - The second stage moves all files into its final destination is running in a single thread (using the mainthread library) to avoid file conflicts
   678  
   679  	var err error
   680  	if isReady {
   681  		logging.Debug("Installing via build result")
   682  		if err := s.handleEvent(events.BuildSkipped{}); err != nil {
   683  			return errs.Wrap(err, "Could not handle BuildSkipped event")
   684  		}
   685  		err = s.installFromBuildResult(engine, artifacts, installFunc)
   686  		if err != nil {
   687  			err = errs.Wrap(err, "Installing via build result failed")
   688  		}
   689  	} else {
   690  		logging.Debug("Installing via buildlog streamer")
   691  		err = s.installFromBuildLog(engine, recipeID, artifacts, installFunc, logFilePath)
   692  		if err != nil {
   693  			err = errs.Wrap(err, "Installing via buildlog streamer failed")
   694  		}
   695  	}
   696  
   697  	return err
   698  }
   699  
   700  // setupArtifactSubmitFunction returns a function that sets up an artifact and can be submitted to a workerpool
   701  func (s *Setup) setupArtifactSubmitFunction(
   702  	engine types.BuildEngine,
   703  	ar *buildplan.Artifact,
   704  	installFunc artifactInstaller,
   705  	errors chan<- error,
   706  ) func() {
   707  	return func() {
   708  		as, err := s.selectArtifactSetupImplementation(engine, ar.ArtifactID)
   709  		if err != nil {
   710  			errors <- errs.Wrap(err, "Failed to select artifact setup implementation")
   711  			return
   712  		}
   713  
   714  		unarchiver := as.Unarchiver()
   715  		archivePath, err := s.obtainArtifact(ar, unarchiver.Ext())
   716  		if err != nil {
   717  			errors <- locale.WrapError(err, "artifact_download_failed", "", ar.Name(), ar.ArtifactID.String())
   718  			return
   719  		}
   720  
   721  		err = installFunc(ar.ArtifactID, archivePath, as)
   722  		if err != nil {
   723  			errors <- locale.WrapError(err, "artifact_setup_failed", "", ar.Name(), ar.ArtifactID.String())
   724  			return
   725  		}
   726  	}
   727  }
   728  
   729  func (s *Setup) installFromBuildResult(engine types.BuildEngine, artifacts buildplan.Artifacts, installFunc artifactInstaller) error {
   730  	logging.Debug("Installing artifacts from build result")
   731  	errs, aggregatedErr := aggregateErrors()
   732  	mainthread.Run(func() {
   733  		defer close(errs)
   734  		wp := workerpool.New(MaxConcurrency)
   735  		for _, a := range artifacts {
   736  			wp.Submit(s.setupArtifactSubmitFunction(engine, a, installFunc, errs))
   737  		}
   738  
   739  		wp.StopWait()
   740  	})
   741  
   742  	return <-aggregatedErr
   743  }
   744  
   745  func (s *Setup) installFromBuildLog(engine types.BuildEngine, recipeID strfmt.UUID, artifacts buildplan.Artifacts, installFunc artifactInstaller, logFilePath string) error {
   746  	ctx, cancel := context.WithCancel(context.Background())
   747  	defer cancel()
   748  
   749  	buildLog, err := buildlog.New(ctx, artifacts.ToIDMap(), s.eventHandler, recipeID, logFilePath)
   750  	if err != nil {
   751  		return errs.Wrap(err, "Cannot establish connection with BuildLog")
   752  	}
   753  	defer func() {
   754  		if err := buildLog.Close(); err != nil {
   755  			logging.Debug("Failed to close build log: %v", errs.JoinMessage(err))
   756  		}
   757  	}()
   758  
   759  	errs, aggregatedErr := aggregateErrors()
   760  
   761  	mainthread.Run(func() {
   762  		defer close(errs)
   763  
   764  		var wg sync.WaitGroup
   765  		defer wg.Wait()
   766  		wg.Add(1)
   767  		go func() {
   768  			// wp.StopWait needs to be run in this go-routine after ALL tasks are scheduled, hence we need to add an extra wait group
   769  			defer wg.Done()
   770  			wp := workerpool.New(MaxConcurrency)
   771  			defer wp.StopWait()
   772  
   773  			for a := range buildLog.BuiltArtifactsChannel() {
   774  				wp.Submit(s.setupArtifactSubmitFunction(engine, a, installFunc, errs))
   775  			}
   776  		}()
   777  
   778  		if err = buildLog.Wait(); err != nil {
   779  			errs <- err
   780  		}
   781  	})
   782  
   783  	return <-aggregatedErr
   784  }
   785  
   786  func (s *Setup) moveToInstallPath(a strfmt.UUID, unpackedDir string, envDef *envdef.EnvironmentDefinition, numFiles int) error {
   787  	// clean up the unpacked dir
   788  	defer os.RemoveAll(unpackedDir)
   789  
   790  	var files []string
   791  	var dirs []string
   792  	onMoveFile := func(fromPath, toPath string) {
   793  		if fileutils.IsDir(toPath) {
   794  			dirs = append(dirs, toPath)
   795  		} else {
   796  			files = append(files, toPath)
   797  		}
   798  	}
   799  	err := fileutils.MoveAllFilesRecursively(
   800  		filepath.Join(unpackedDir, envDef.InstallDir),
   801  		s.store.InstallPath(), onMoveFile,
   802  	)
   803  	if err != nil {
   804  		err := errs.Wrap(err, "Move artifact failed")
   805  		return err
   806  	}
   807  
   808  	if err := s.store.StoreArtifact(store.NewStoredArtifact(a, files, dirs, envDef)); err != nil {
   809  		return errs.Wrap(err, "Could not store artifact meta info")
   810  	}
   811  
   812  	return nil
   813  }
   814  
   815  // downloadArtifact downloads the given artifact
   816  func (s *Setup) downloadArtifact(a *buildplan.Artifact, targetFile string) (rerr error) {
   817  	defer func() {
   818  		if rerr != nil {
   819  			if !errs.Matches(rerr, &ProgressReportError{}) {
   820  				rerr = &ArtifactDownloadError{errs.Wrap(rerr, "Unable to download artifact")}
   821  			}
   822  
   823  			if err := s.handleEvent(events.ArtifactDownloadFailure{a.ArtifactID, rerr}); err != nil {
   824  				rerr = errs.Wrap(rerr, "Could not handle ArtifactDownloadFailure event")
   825  				return
   826  			}
   827  		}
   828  
   829  		if err := s.handleEvent(events.ArtifactDownloadSuccess{a.ArtifactID}); err != nil {
   830  			rerr = errs.Wrap(rerr, "Could not handle ArtifactDownloadSuccess event")
   831  			return
   832  		}
   833  	}()
   834  
   835  	if a.URL == "" {
   836  		return errs.New("Artifact URL is empty: %+v", a)
   837  	}
   838  
   839  	artifactURL, err := url.Parse(a.URL)
   840  	if err != nil {
   841  		return errs.Wrap(err, "Could not parse artifact URL %s.", a.URL)
   842  	}
   843  
   844  	b, err := httputil.GetWithProgress(artifactURL.String(), &progress.Report{
   845  		ReportSizeCb: func(size int) error {
   846  			if err := s.handleEvent(events.ArtifactDownloadStarted{a.ArtifactID, size}); err != nil {
   847  				return ProgressReportError{errs.Wrap(err, "Could not handle ArtifactDownloadStarted event")}
   848  			}
   849  			return nil
   850  		},
   851  		ReportIncrementCb: func(inc int) error {
   852  			if err := s.handleEvent(events.ArtifactDownloadProgress{a.ArtifactID, inc}); err != nil {
   853  				return errs.Wrap(err, "Could not handle ArtifactDownloadProgress event")
   854  			}
   855  			return nil
   856  		},
   857  	})
   858  	if err != nil {
   859  		return errs.Wrap(err, "Download %s failed", artifactURL.String())
   860  	}
   861  
   862  	if err := fileutils.WriteFile(targetFile, b); err != nil {
   863  		return errs.Wrap(err, "Writing download to target file %s failed", targetFile)
   864  	}
   865  
   866  	return nil
   867  }
   868  
   869  // verifyArtifact verifies the checksum of the downloaded artifact matches the checksum given by the
   870  // platform, and returns an error if the verification fails.
   871  func (s *Setup) verifyArtifact(archivePath string, a *buildplan.Artifact) error {
   872  	return validate.Checksum(archivePath, a.Checksum)
   873  }
   874  
   875  // obtainArtifact obtains an artifact and returns the local path to that artifact's archive.
   876  func (s *Setup) obtainArtifact(a *buildplan.Artifact, extension string) (string, error) {
   877  	if cachedPath, found := s.artifactCache.Get(a.ArtifactID); found {
   878  		if err := s.verifyArtifact(cachedPath, a); err == nil {
   879  			if err := s.handleEvent(events.ArtifactDownloadSkipped{a.ArtifactID}); err != nil {
   880  				return "", errs.Wrap(err, "Could not handle ArtifactDownloadSkipped event")
   881  			}
   882  			return cachedPath, nil
   883  		}
   884  		// otherwise re-download it; do not return an error
   885  	}
   886  
   887  	targetDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory)
   888  	if err := fileutils.MkdirUnlessExists(targetDir); err != nil {
   889  		return "", errs.Wrap(err, "Could not create temp runtime dir")
   890  	}
   891  
   892  	archivePath := filepath.Join(targetDir, a.ArtifactID.String()+extension)
   893  	if err := s.downloadArtifact(a, archivePath); err != nil {
   894  		return "", errs.Wrap(err, "Could not download artifact %s", a.URL)
   895  	}
   896  
   897  	err := s.verifyArtifact(archivePath, a)
   898  	if err != nil {
   899  		return "", errs.Wrap(err, "Artifact checksum validation failed")
   900  	}
   901  
   902  	err = s.artifactCache.Store(a.ArtifactID, archivePath)
   903  	if err != nil {
   904  		multilog.Error("Could not store artifact in cache: %v", err)
   905  	}
   906  
   907  	return archivePath, nil
   908  }
   909  
   910  func (s *Setup) unpackArtifact(ua unarchiver.Unarchiver, tarballPath string, targetDir string, progress progress.Reporter) (int, error) {
   911  	f, i, err := ua.PrepareUnpacking(tarballPath, targetDir)
   912  	if err != nil {
   913  		return 0, errs.Wrap(err, "Prepare for unpacking failed")
   914  	}
   915  	defer f.Close()
   916  
   917  	if err := progress.ReportSize(int(i)); err != nil {
   918  		return 0, errs.Wrap(err, "Could not report size")
   919  	}
   920  
   921  	var numUnpackedFiles int
   922  	ua.SetNotifier(func(_ string, _ int64, isDir bool) {
   923  		if !isDir {
   924  			numUnpackedFiles++
   925  		}
   926  	})
   927  	proxy := proxyreader.NewProxyReader(progress, f)
   928  	return numUnpackedFiles, ua.Unarchive(proxy, i, targetDir)
   929  }
   930  
   931  func (s *Setup) selectSetupImplementation(buildEngine types.BuildEngine) (Setuper, error) {
   932  	switch buildEngine {
   933  	case types.Alternative:
   934  		return alternative.NewSetup(s.store), nil
   935  	case types.Camel:
   936  		return camel.NewSetup(s.store), nil
   937  	default:
   938  		return nil, errs.New("Unknown build engine: %s", buildEngine)
   939  	}
   940  }
   941  
   942  func selectArtifactResolver(bp *buildplan.BuildPlan) (ArtifactResolver, error) {
   943  	switch bp.Engine() {
   944  	case types.Alternative:
   945  		return alternative.NewResolver(bp.Artifacts().ToIDMap()), nil
   946  	case types.Camel:
   947  		return camel.NewResolver(), nil
   948  	default:
   949  		return nil, errs.New("Unknown build engine: %s", bp.Engine())
   950  	}
   951  }
   952  
   953  func (s *Setup) selectArtifactSetupImplementation(buildEngine types.BuildEngine, a strfmt.UUID) (ArtifactSetuper, error) {
   954  	switch buildEngine {
   955  	case types.Alternative:
   956  		return alternative.NewArtifactSetup(a, s.store), nil
   957  	case types.Camel:
   958  		return camel.NewArtifactSetup(a, s.store), nil
   959  	default:
   960  		return nil, errs.New("Unknown build engine: %s", buildEngine)
   961  	}
   962  }
   963  
   964  func ExecDir(targetDir string) string {
   965  	return filepath.Join(targetDir, "exec")
   966  }
   967  
   968  func reusableArtifacts(requestedArtifacts []*buildplan.Artifact, storedArtifacts store.StoredArtifactMap) store.StoredArtifactMap {
   969  	keep := make(store.StoredArtifactMap)
   970  
   971  	for _, a := range requestedArtifacts {
   972  		if v, ok := storedArtifacts[a.ArtifactID]; ok {
   973  			keep[a.ArtifactID] = v
   974  		}
   975  	}
   976  	return keep
   977  }
   978  
   979  func (s *Setup) fetchAndInstallArtifactsFromDir(installFunc artifactInstaller) ([]strfmt.UUID, error) {
   980  	artifactsDir := s.target.InstallFromDir()
   981  	if artifactsDir == nil {
   982  		return nil, errs.New("Cannot install from a directory that is nil")
   983  	}
   984  
   985  	artifacts, err := fileutils.ListDir(*artifactsDir, false)
   986  	if err != nil {
   987  		return nil, errs.Wrap(err, "Cannot read from directory to install from")
   988  	}
   989  	logging.Debug("Found %d artifacts to install from '%s'", len(artifacts), *artifactsDir)
   990  
   991  	installedArtifacts := make([]strfmt.UUID, len(artifacts))
   992  
   993  	errors, aggregatedErr := aggregateErrors()
   994  	mainthread.Run(func() {
   995  		defer close(errors)
   996  
   997  		wp := workerpool.New(MaxConcurrency)
   998  
   999  		for i, a := range artifacts {
  1000  			// Each artifact is of the form artifactID.tar.gz, so extract the artifactID from the name.
  1001  			filename := a.Path()
  1002  			basename := filepath.Base(filename)
  1003  			extIndex := strings.Index(basename, ".")
  1004  			if extIndex == -1 {
  1005  				extIndex = len(basename)
  1006  			}
  1007  			artifactID := strfmt.UUID(basename[0:extIndex])
  1008  			installedArtifacts[i] = artifactID
  1009  
  1010  			// Submit the artifact for setup and install.
  1011  			func(filename string, artifactID strfmt.UUID) {
  1012  				wp.Submit(func() {
  1013  					as := alternative.NewArtifactSetup(artifactID, s.store) // offline installer artifacts are in this format
  1014  					err = installFunc(artifactID, filename, as)
  1015  					if err != nil {
  1016  						errors <- locale.WrapError(err, "artifact_setup_failed", "", artifactID.String(), "")
  1017  					}
  1018  				})
  1019  			}(filename, artifactID) // avoid referencing loop variables inside goroutine closures
  1020  		}
  1021  
  1022  		wp.StopWait()
  1023  	})
  1024  
  1025  	return installedArtifacts, <-aggregatedErr
  1026  }
  1027  
  1028  func (s *Setup) handleEvent(ev events.Eventer) error {
  1029  	err := s.eventHandler.Handle(ev)
  1030  	if err != nil {
  1031  		return &ProgressReportError{errs.Wrap(err, "Error handling event: %v", errs.JoinMessage(err))}
  1032  	}
  1033  	return nil
  1034  }
  1035  
  1036  func (s *Setup) deleteOutdatedArtifacts(setup Setuper, changedArtifacts *buildplan.ArtifactChangeset, alreadyInstalled store.StoredArtifactMap) error {
  1037  	storedArtifacts, err := s.store.Artifacts()
  1038  	if err != nil {
  1039  		return locale.WrapError(err, "err_stored_artifacts")
  1040  	}
  1041  
  1042  	err = setup.DeleteOutdatedArtifacts(changedArtifacts, storedArtifacts, alreadyInstalled)
  1043  	if err != nil {
  1044  		// This multilog is technically redundant and may be dropped after we can collect data on this error for a while as rollbar is not surfacing the returned error
  1045  		// https://github.com/ActiveState/cli/pull/2620#discussion_r1256103647
  1046  		multilog.Error("Could not delete outdated artifacts: %s", errs.JoinMessage(err))
  1047  		return errs.Wrap(err, "Could not delete outdated artifacts")
  1048  	}
  1049  	return nil
  1050  }