blob: 86627a1a673f720dcf76dbba907666c7c5adcb69 [file] [log] [blame]
package gps
import (
"encoding/json"
"fmt"
"go/build"
"os"
"path"
"github.com/Masterminds/vcs"
)
// A SourceManager is responsible for retrieving, managing, and interrogating
// source repositories. Its primary purpose is to serve the needs of a Solver,
// but it is handy for other purposes, as well.
//
// gps's built-in SourceManager, accessible via NewSourceManager(), is
// intended to be generic and sufficient for any purpose. It provides some
// additional semantics around the methods defined here.
type SourceManager interface {
// RepoExists checks if a repository exists, either upstream or in the
// SourceManager's central repository cache.
RepoExists(ProjectRoot) (bool, error)
// ListVersions retrieves a list of the available versions for a given
// repository name.
ListVersions(ProjectRoot) ([]Version, error)
// RevisionPresentIn indicates whether the provided Version is present in
// the given repository.
RevisionPresentIn(ProjectRoot, Revision) (bool, error)
// ListPackages retrieves a tree of the Go packages at or below the provided
// import path, at the provided version.
ListPackages(ProjectRoot, Version) (PackageTree, error)
// GetProjectInfo returns manifest and lock information for the provided
// import path. gps currently requires that projects be rooted at their
// repository root, which means that this ProjectRoot must also be a
// repository root.
GetProjectInfo(ProjectRoot, Version) (Manifest, Lock, error)
// ExportProject writes out the tree of the provided import path, at the
// provided version, to the provided directory.
ExportProject(ProjectRoot, Version, string) error
// Release lets go of any locks held by the SourceManager.
Release()
}
// A ProjectAnalyzer is responsible for analyzing a path for Manifest and Lock
// information. Tools relying on gps must implement one.
type ProjectAnalyzer interface {
GetInfo(string, ProjectRoot) (Manifest, Lock, error)
}
// SourceMgr is the default SourceManager for gps.
//
// There's no (planned) reason why it would need to be reimplemented by other
// tools; control via dependency injection is intended to be sufficient.
type SourceMgr struct {
cachedir string
pms map[ProjectRoot]*pmState
an ProjectAnalyzer
ctx build.Context
//pme map[ProjectRoot]error
}
var _ SourceManager = &SourceMgr{}
// Holds a projectManager, caches of the managed project's data, and information
// about the freshness of those caches
type pmState struct {
pm *projectManager
cf *os.File // handle for the cache file
vcur bool // indicates that we've called ListVersions()
}
// NewSourceManager produces an instance of gps's built-in SourceManager. It
// takes a cache directory (where local instances of upstream repositories are
// stored), a vendor directory for the project currently being worked on, and a
// force flag indicating whether to overwrite the global cache lock file (if
// present).
//
// The returned SourceManager aggressively caches information wherever possible.
// It is recommended that, if tools need to do preliminary, work involving
// upstream repository analysis prior to invoking a solve run, that they create
// this SourceManager as early as possible and use it to their ends. That way,
// the solver can benefit from any caches that may have already been warmed.
//
// gps's SourceManager is intended to be threadsafe (if it's not, please
// file a bug!). It should certainly be safe to reuse from one solving run to
// the next; however, the fact that it takes a basedir as an argument makes it
// much less useful for simultaneous use by separate solvers operating on
// different root projects. This architecture may change in the future.
func NewSourceManager(an ProjectAnalyzer, cachedir string, force bool) (*SourceMgr, error) {
if an == nil {
return nil, fmt.Errorf("a ProjectAnalyzer must be provided to the SourceManager")
}
err := os.MkdirAll(cachedir, 0777)
if err != nil {
return nil, err
}
glpath := path.Join(cachedir, "sm.lock")
_, err = os.Stat(glpath)
if err == nil && !force {
return nil, fmt.Errorf("cache lock file %s exists - another process crashed or is still running?", glpath)
}
_, err = os.OpenFile(glpath, os.O_CREATE|os.O_RDONLY, 0700) // is 0700 sane for this purpose?
if err != nil {
return nil, fmt.Errorf("failed to create global cache lock file at %s with err %s", glpath, err)
}
ctx := build.Default
// Replace GOPATH with our cache dir
ctx.GOPATH = cachedir
return &SourceMgr{
cachedir: cachedir,
pms: make(map[ProjectRoot]*pmState),
ctx: ctx,
an: an,
}, nil
}
// Release lets go of any locks held by the SourceManager.
func (sm *SourceMgr) Release() {
os.Remove(path.Join(sm.cachedir, "sm.lock"))
}
// GetProjectInfo returns manifest and lock information for the provided import
// path. gps currently requires that projects be rooted at their repository
// root, which means that this ProjectRoot must also be a repository root.
//
// The work of producing the manifest and lock information is delegated to the
// injected ProjectAnalyzer.
func (sm *SourceMgr) GetProjectInfo(n ProjectRoot, v Version) (Manifest, Lock, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
return nil, nil, err
}
return pmc.pm.GetInfoAt(v)
}
// ListPackages retrieves a tree of the Go packages at or below the provided
// import path, at the provided version.
func (sm *SourceMgr) ListPackages(n ProjectRoot, v Version) (PackageTree, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
return PackageTree{}, err
}
return pmc.pm.ListPackages(v)
}
// ListVersions retrieves a list of the available versions for a given
// repository name.
//
// The list is not sorted; while it may be returned in the order that the
// underlying VCS reports version information, no guarantee is made. It is
// expected that the caller either not care about order, or sort the result
// themselves.
//
// This list is always retrieved from upstream; if upstream is not accessible
// (network outage, access issues, or the resource actually went away), an error
// will be returned.
func (sm *SourceMgr) ListVersions(n ProjectRoot) ([]Version, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
// TODO(sdboyer) More-er proper-er errors
return nil, err
}
return pmc.pm.ListVersions()
}
// RevisionPresentIn indicates whether the provided Revision is present in the given
// repository.
func (sm *SourceMgr) RevisionPresentIn(n ProjectRoot, r Revision) (bool, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
// TODO(sdboyer) More-er proper-er errors
return false, err
}
return pmc.pm.RevisionPresentIn(r)
}
// RepoExists checks if a repository exists, either upstream or in the cache,
// for the provided ProjectRoot.
func (sm *SourceMgr) RepoExists(n ProjectRoot) (bool, error) {
pms, err := sm.getProjectManager(n)
if err != nil {
return false, err
}
return pms.pm.CheckExistence(existsInCache) || pms.pm.CheckExistence(existsUpstream), nil
}
// ExportProject writes out the tree of the provided import path, at the
// provided version, to the provided directory.
func (sm *SourceMgr) ExportProject(n ProjectRoot, v Version, to string) error {
pms, err := sm.getProjectManager(n)
if err != nil {
return err
}
return pms.pm.ExportVersionTo(v, to)
}
// getProjectManager gets the project manager for the given ProjectRoot.
//
// If no such manager yet exists, it attempts to create one.
func (sm *SourceMgr) getProjectManager(n ProjectRoot) (*pmState, error) {
// Check pm cache and errcache first
if pm, exists := sm.pms[n]; exists {
return pm, nil
//} else if pme, errexists := sm.pme[name]; errexists {
//return nil, pme
}
repodir := path.Join(sm.cachedir, "src", string(n))
// TODO(sdboyer) be more robust about this
r, err := vcs.NewRepo("https://"+string(n), repodir)
if err != nil {
// TODO(sdboyer) be better
return nil, err
}
if !r.CheckLocal() {
// TODO(sdboyer) cloning the repo here puts it on a blocking, and possibly
// unnecessary path. defer it
err = r.Get()
if err != nil {
// TODO(sdboyer) be better
return nil, err
}
}
// Ensure cache dir exists
metadir := path.Join(sm.cachedir, "metadata", string(n))
err = os.MkdirAll(metadir, 0777)
if err != nil {
// TODO(sdboyer) be better
return nil, err
}
pms := &pmState{}
cpath := path.Join(metadir, "cache.json")
fi, err := os.Stat(cpath)
var dc *projectDataCache
if fi != nil {
pms.cf, err = os.OpenFile(cpath, os.O_RDWR, 0777)
if err != nil {
// TODO(sdboyer) be better
return nil, fmt.Errorf("Err on opening metadata cache file: %s", err)
}
err = json.NewDecoder(pms.cf).Decode(dc)
if err != nil {
// TODO(sdboyer) be better
return nil, fmt.Errorf("Err on JSON decoding metadata cache file: %s", err)
}
} else {
// TODO(sdboyer) commented this out for now, until we manage it correctly
//pms.cf, err = os.Create(cpath)
//if err != nil {
//// TODO(sdboyer) be better
//return nil, fmt.Errorf("Err on creating metadata cache file: %s", err)
//}
dc = &projectDataCache{
Infos: make(map[Revision]projectInfo),
Packages: make(map[Revision]PackageTree),
VMap: make(map[Version]Revision),
RMap: make(map[Revision][]Version),
}
}
pm := &projectManager{
n: n,
ctx: sm.ctx,
an: sm.an,
dc: dc,
crepo: &repo{
rpath: repodir,
r: r,
},
}
pms.pm = pm
sm.pms[n] = pms
return pms, nil
}