package libbuildpack import ( "crypto/sha256" "encoding/hex" "fmt" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/Masterminds/semver" ) type Installer struct { manifest *Manifest appCacheDir string filesInAppCache map[string]interface{} versionLine *map[string]string retryTimeLimit time.Duration retryTimeInitialInterval time.Duration } func NewInstaller(manifest *Manifest) *Installer { return &Installer{manifest, "", make(map[string]interface{}), &map[string]string{}, 1 * time.Minute, 1 * time.Second} } func (i *Installer) SetAppCacheDir(appCacheDir string) (err error) { i.appCacheDir, err = filepath.Abs(filepath.Join(appCacheDir, "dependencies")) return } func (i *Installer) InstallDependency(dep Dependency, outputDir string) error { i.manifest.log.BeginStep("Installing %s %s", dep.Name, dep.Version) tmpDir, err := ioutil.TempDir("", "downloads") if err != nil { return err } defer os.RemoveAll(tmpDir) tmpFile := filepath.Join(tmpDir, "archive") entry, err := i.manifest.GetEntry(dep) if err != nil { return err } err = i.FetchDependency(dep, tmpFile) if err != nil { return err } err = i.warnNewerPatch(dep) if err != nil { return err } err = i.warnEndOfLife(dep) if err != nil { return err } if strings.HasSuffix(entry.URI, ".sh") { return os.Rename(tmpFile, outputDir) } err = os.MkdirAll(outputDir, 0755) if err != nil { return err } if strings.HasSuffix(entry.URI, ".zip") { return ExtractZip(tmpFile, outputDir) } if strings.HasSuffix(entry.URI, ".tar.xz") { return ExtractTarXz(tmpFile, outputDir) } if strings.HasSuffix(entry.URI, ".tar.gz") || strings.HasSuffix(entry.URI, ".tgz") { return ExtractTarGz(tmpFile, outputDir) } basename := filepath.Base(entry.URI) return CopyFile(tmpFile, filepath.Join(outputDir, basename)) } func (i *Installer) warnNewerPatch(dep Dependency) error { v, err := semver.NewVersion(dep.Version) if err != nil { return nil } if v.Prerelease() != "" { i.manifest.log.Warning("You are using the pre-release version %s of %s", dep.Version, dep.Name) return nil } versions := i.manifest.AllDependencyVersions(dep.Name) minor := fmt.Sprintf("%v", v.Minor()) versionLine := *i.GetVersionLine() if versionLine[dep.Name] == "minor" { minor = "x" } constraint := fmt.Sprintf("%d.%s.x", v.Major(), minor) latest, err := FindMatchingVersion(constraint, versions) if err != nil { return err } if latest != dep.Version { i.manifest.log.Warning(outdatedDependencyWarning(dep, latest)) } return nil } func (i *Installer) warnEndOfLife(dep Dependency) error { matchVersion := func(versionLine, depVersion string) bool { return versionLine == depVersion } v, err := semver.NewVersion(dep.Version) if err == nil { matchVersion = func(versionLine, depVersion string) bool { constraint, err := semver.NewConstraint(versionLine) if err != nil { return false } return constraint.Check(v) } } for _, deprecation := range i.manifest.Deprecations { if deprecation.Name != dep.Name { continue } if !matchVersion(deprecation.VersionLine, dep.Version) { continue } eolTime, err := time.Parse(dateFormat, deprecation.Date) if err != nil { return err } if eolTime.Sub(i.manifest.currentTime) < thirtyDays { i.manifest.log.Warning(endOfLifeWarning(dep.Name, deprecation.VersionLine, deprecation.Date, deprecation.Link)) } } return nil } func (i *Installer) FetchDependency(dep Dependency, outputFile string) error { entry, err := i.manifest.GetEntry(dep) if err != nil { return err } if entry.File != "" { // this file is cached by the buildpack return fetchCachedBuildpackDependency(entry, outputFile, i.manifest.manifestRootDir, i.manifest.log) } if i.appCacheDir != "" { // this buildpack caches dependencies in the app cache return i.fetchAppCachedBuildpackDependency(entry, outputFile) } return downloadDependency(entry, outputFile, i.manifest.log, i.retryTimeLimit, i.retryTimeInitialInterval) } func (i *Installer) CleanupAppCache() error { pathsToDelete := []string{} if err := filepath.Walk(i.appCacheDir, func(path string, info os.FileInfo, err error) error { if info == nil || info.IsDir() { return nil } if err != nil { return fmt.Errorf("Failed while cleaning up app cache; couldn't look at %s because: %v", path, err) } if path == i.appCacheDir { return nil } if _, ok := i.filesInAppCache[path]; !ok { pathsToDelete = append(pathsToDelete, path) } return nil }); err != nil { return err } for _, path := range pathsToDelete { i.manifest.log.Debug("Deleting cached file: %s", path) if err := os.RemoveAll(path); err != nil { return fmt.Errorf("Failed while cleaning up app cache; couldn't delete %s because: %v", path, err) } } return nil } func (i *Installer) InstallOnlyVersion(depName string, installDir string) error { depVersions := i.manifest.AllDependencyVersions(depName) if len(depVersions) > 1 { return fmt.Errorf("more than one version of %s found", depName) } else if len(depVersions) == 0 { return fmt.Errorf("no versions of %s found", depName) } dep := Dependency{Name: depName, Version: depVersions[0]} return i.InstallDependency(dep, installDir) } func (i *Installer) fetchAppCachedBuildpackDependency(entry *ManifestEntry, outputFile string) error { shaURI := sha256.Sum256([]byte(entry.URI)) cacheFile := filepath.Join(i.appCacheDir, hex.EncodeToString(shaURI[:]), filepath.Base(entry.URI)) i.filesInAppCache[cacheFile] = true i.filesInAppCache[filepath.Dir(cacheFile)] = true foundCacheFile, err := FileExists(cacheFile) if err != nil { return err } if foundCacheFile { i.manifest.log.Info("Copy [%s]", cacheFile) if err := CopyFile(cacheFile, outputFile); err != nil { return err } return deleteBadFile(entry, outputFile) } if err := downloadDependency(entry, outputFile, i.manifest.log, i.retryTimeLimit, i.retryTimeInitialInterval); err != nil { return err } if err := CopyFile(outputFile, cacheFile); err != nil { return err } return nil } func (i *Installer) SetVersionLine(depName string, line string) { (*i.versionLine)[depName] = line } func (i *Installer) GetVersionLine() *map[string]string { return i.versionLine } func (i *Installer) SetRetryTimeLimit(duration time.Duration) { i.retryTimeLimit = duration return } func (i *Installer) SetRetryTimeInitialInterval(duration time.Duration) { i.retryTimeInitialInterval = duration return }