blob: 0ce5797ba4616ffa72868ecb31ce3bf649936b78 [file] [log] [blame] [edit]
package action
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/Masterminds/glide/cfg"
"github.com/Masterminds/glide/dependency"
"github.com/Masterminds/glide/msg"
gpath "github.com/Masterminds/glide/path"
"github.com/Masterminds/glide/repo"
"github.com/sdboyer/gps"
)
// Install installs a vendor directory based on an existing Glide configuration.
func Install(installer *repo.Installer, io, so, sv bool) {
base := "."
// Ensure GOPATH
EnsureGopath()
EnsureVendorDir()
conf := EnsureConfig()
// TODO might need a better way for discovering the root
vend, err := gpath.Vendor()
if err != nil {
msg.Die("Could not find the vendor dir: %s", err)
}
// Create the SourceManager for this run
sm, err := gps.NewSourceManager(dependency.Analyzer{}, filepath.Join(installer.Home, "cache"))
defer sm.Release()
if err != nil {
msg.Err(err.Error())
return
}
rd := filepath.Dir(vend)
rt, err := gps.ListPackages(rd, conf.Name)
if err != nil {
msg.Die("Error while scanning project: %s", err)
}
params := gps.SolveParameters{
RootDir: rd,
RootPackageTree: rt,
Manifest: conf,
Trace: true,
TraceLogger: log.New(os.Stdout, "", 0),
}
var s gps.Solver
if gpath.HasLock(base) {
var legacy bool
params.Lock, legacy, err = loadLockfile(base, conf)
if legacy {
msg.Warn("glide.lock was in a legacy format. An attempt will be made to automatically update it.")
}
if err != nil {
msg.Err("Could not load lockfile.")
return
}
s, err = gps.Prepare(params, sm)
if err != nil {
msg.Err("Could not set up solver: %s", err)
return
}
digest := s.HashInputs()
// Check if digests match, and warn if they don't
if bytes.Equal(digest, params.Lock.InputHash()) {
if so {
msg.Err("glide.yaml is out of sync with glide.lock")
return
} else {
msg.Warn("glide.yaml is out of sync with glide.lock!")
}
}
gw := safeGroupWriter{
resultLock: params.Lock,
vendor: vend,
sm: sm,
stripVendor: sv,
}
err = gw.writeAllSafe()
if err != nil {
msg.Err(err.Error())
return
}
} else if io || so {
msg.Err("No glide.lock file could be found.")
return
} else {
// There is no lock, so we have to solve first
s, err = gps.Prepare(params, sm)
if err != nil {
msg.Err("Could not set up solver: %s", err)
return
}
r, err := s.Solve()
if err != nil {
// TODO better error handling
msg.Err(err.Error())
return
}
gw := safeGroupWriter{
resultLock: r,
vendor: vend,
sm: sm,
stripVendor: sv,
}
err = gw.writeAllSafe()
if err != nil {
msg.Err(err.Error())
return
}
}
}
// locksAreEquivalent compares the fingerprints between two locks to determine
// if they're equivalent.
//
// If the either of the locks are nil, the input hashes are different, the
// fingerprints are different, or any error is returned from fingerprinting,
// this function returns false.
func locksAreEquivalent(l1, l2 *cfg.Lockfile) bool {
if l1 != nil && l2 != nil {
if l1.Hash != l2.Hash {
return false
}
f1, err := l1.Fingerprint()
f2, err2 := l2.Fingerprint()
if err == nil && err2 == nil && f1 == f2 {
return true
}
}
return false
}
// safeGroupWriter provides a slipshod-but-better-than-nothing approach to
// grouping together yaml, lock, and vendor dir writes.
type safeGroupWriter struct {
conf *cfg.Config
lock *cfg.Lockfile
resultLock gps.Lock
sm gps.SourceManager
glidefile, vendor string
stripVendor bool
}
// writeAllSafe writes out some combination of config yaml, lock, and a vendor
// tree, to a temp dir, then moves them into place if and only if all the write
// operations succeeded. It also does its best to roll back if any moves fail.
//
// This helps to ensure glide doesn't exit with a partial write, resulting in an
// undefined disk state.
//
// - If a gw.conf is provided, it will be written to gw.glidefile
// - If gw.lock is provided without a gw.resultLock, it will be written to
// `glide.lock` in the parent dir of gw.vendor
// - If gw.lock and gw.resultLock are both provided and are not equivalent,
// the resultLock will be written to the same location as above, and a vendor
// tree will be written to gw.vendor
// - If gw.resultLock is provided and gw.lock is not, it will write both a lock
// and vendor dir in the same way
//
// Any of the conf, lock, or result can be omitted; the grouped write operation
// will continue for whichever inputs are present.
func (gw safeGroupWriter) writeAllSafe() error {
// Decide which writes we need to do
var writeConf, writeLock, writeVendor bool
if gw.conf != nil {
writeConf = true
}
if gw.resultLock != nil {
if gw.lock == nil {
writeLock, writeVendor = true, true
} else {
rlf, err := cfg.LockfileFromSolverLock(gw.resultLock)
// This err really shouldn't occur, but could if we get an unpaired
// version back from gps somehow
if err != nil {
return err
}
if !locksAreEquivalent(rlf, gw.lock) {
writeLock, writeVendor = true, true
}
}
} else if gw.lock != nil {
writeLock = true
}
if !writeConf && !writeLock && !writeVendor {
// nothing to do
return nil
}
if writeConf && gw.glidefile == "" {
return fmt.Errorf("Must provide a path if writing out a config yaml.")
}
if (writeLock || writeVendor) && gw.vendor == "" {
return fmt.Errorf("Must provide a vendor dir if writing out a lock or vendor dir.")
}
if writeVendor && gw.sm == nil {
return fmt.Errorf("Must provide a SourceManager if writing out a vendor dir.")
}
td, err := ioutil.TempDir(os.TempDir(), "glide")
if err != nil {
return fmt.Errorf("Error while creating temp dir for vendor directory: %s", err)
}
defer os.RemoveAll(td)
if writeConf {
if err := gw.conf.WriteFile(filepath.Join(td, "glide.yaml")); err != nil {
return fmt.Errorf("Failed to write glide YAML file: %s", err)
}
}
if writeLock {
if gw.resultLock == nil {
// the result lock is nil but the flag is on, so we must be writing
// the other one
if err := gw.lock.WriteFile(filepath.Join(td, gpath.LockFile)); err != nil {
return fmt.Errorf("Failed to write glide lock file: %s", err)
}
} else {
rlf, err := cfg.LockfileFromSolverLock(gw.resultLock)
// As with above, this case really shouldn't get hit unless there's
// a bug in gps, or guarantees change
if err != nil {
return err
}
if err := rlf.WriteFile(filepath.Join(td, gpath.LockFile)); err != nil {
return fmt.Errorf("Failed to write glide lock file: %s", err)
}
}
}
if writeVendor {
err = gps.WriteDepTree(filepath.Join(td, "vendor"), gw.resultLock, gw.sm, gw.stripVendor)
if err != nil {
return fmt.Errorf("Error while generating vendor tree: %s", err)
}
}
// Move the existing files and dirs to the temp dir while we put the new
// ones in, to provide insurance against errors for as long as possible
var fail bool
var failerr error
type pathpair struct {
from, to string
}
var restore []pathpair
if writeConf {
if _, err := os.Stat(gw.glidefile); err == nil {
// move out the old one
tmploc := filepath.Join(td, "glide.yaml-old")
failerr = os.Rename(gw.glidefile, tmploc)
if failerr != nil {
fail = true
} else {
restore = append(restore, pathpair{from: tmploc, to: gw.glidefile})
}
}
// move in the new one
failerr = os.Rename(filepath.Join(td, "glide.yaml"), gw.glidefile)
if failerr != nil {
fail = true
}
}
if !fail && writeLock {
tgt := filepath.Join(filepath.Dir(gw.vendor), gpath.LockFile)
if _, err := os.Stat(tgt); err == nil {
// move out the old one
tmploc := filepath.Join(td, "glide.lock-old")
failerr = os.Rename(tgt, tmploc)
if failerr != nil {
fail = true
} else {
restore = append(restore, pathpair{from: tmploc, to: tgt})
}
}
// move in the new one
failerr = os.Rename(filepath.Join(td, gpath.LockFile), tgt)
if failerr != nil {
fail = true
}
}
// have to declare out here so it's present later
var vendorbak string
if !fail && writeVendor {
if _, err := os.Stat(gw.vendor); err == nil {
// move out the old vendor dir. just do it into an adjacent dir, in
// order to mitigate the possibility of a pointless cross-filesystem move
vendorbak = gw.vendor + "-old"
if _, err := os.Stat(vendorbak); err == nil {
// Just in case that happens to exist...
vendorbak = filepath.Join(td, "vendor-old")
}
failerr = os.Rename(gw.vendor, vendorbak)
if failerr != nil {
fail = true
} else {
restore = append(restore, pathpair{from: vendorbak, to: gw.vendor})
}
}
// move in the new one
failerr = os.Rename(filepath.Join(td, "vendor"), gw.vendor)
if failerr != nil {
fail = true
}
}
// If we failed at any point, move all the things back into place, then bail
if fail {
for _, pair := range restore {
// Nothing we can do on err here, we're already in recovery mode
os.Rename(pair.from, pair.to)
}
return failerr
}
// Renames all went smoothly. The deferred os.RemoveAll will get the temp
// dir, but if we wrote vendor, we have to clean that up directly
if writeVendor {
// Again, kinda nothing we can do about an error at this point
os.RemoveAll(vendorbak)
}
return nil
}
// loadLockfile loads the contents of a glide.lock file.
func loadLockfile(base string, conf *cfg.Config) (*cfg.Lockfile, bool, error) {
yml, err := ioutil.ReadFile(filepath.Join(base, gpath.LockFile))
if err != nil {
return nil, false, err
}
lock, legacy, err := cfg.LockfileFromYaml(yml)
if err != nil {
return nil, false, err
}
return lock, legacy, nil
}