blob: 6ab9c0123810224bcaa289002940c57f970207b9 [file] [log] [blame]
package vsolver
import (
"encoding/json"
"fmt"
"go/build"
"os"
"path"
"github.com/Masterminds/vcs"
)
type SourceManager interface {
GetProjectInfo(ProjectName, Version) (ProjectInfo, error)
ListVersions(ProjectName) ([]Version, error)
RepoExists(ProjectName) (bool, error)
VendorCodeExists(ProjectName) (bool, error)
ListPackages(ProjectName, Version) (PackageTree, error)
ExportProject(ProjectName, Version, string) error
Release()
// Flush()
}
// ExistenceError is a specialized error type that, in addition to the standard
// error interface, also indicates the amount of searching for a project's
// existence that has been performed, and what level of existence has been
// ascertained.
//
// ExistenceErrors should *only* be returned if the (lack of) existence of a
// project was the underling cause of the error.
//type ExistenceError interface {
//error
//Existence() (search ProjectExistence, found ProjectExistence)
//}
// sourceManager is the default SourceManager for vsolver.
//
// 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 sourceManager struct {
cachedir, basedir string
pms map[ProjectName]*pmState
an ProjectAnalyzer
ctx build.Context
//pme map[ProjectName]error
}
// 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()
}
func NewSourceManager(cachedir, basedir string, force bool, an ProjectAnalyzer) (SourceManager, 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("Another process has locked the cachedir, or crashed without cleaning itself properly. Pass force=true to override.")
}
_, 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 &sourceManager{
cachedir: cachedir,
pms: make(map[ProjectName]*pmState),
ctx: ctx,
an: an,
}, nil
// recovery in a defer to be really proper, though
}
func (sm *sourceManager) Release() {
os.Remove(path.Join(sm.cachedir, "sm.lock"))
}
func (sm *sourceManager) GetProjectInfo(n ProjectName, v Version) (ProjectInfo, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
return ProjectInfo{}, err
}
return pmc.pm.GetInfoAt(v)
}
func (sm *sourceManager) ListPackages(n ProjectName, v Version) (PackageTree, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
return PackageTree{}, err
}
return pmc.pm.ListPackages(v)
}
func (sm *sourceManager) ListVersions(n ProjectName) ([]Version, error) {
pmc, err := sm.getProjectManager(n)
if err != nil {
// TODO More-er proper-er errors
return nil, err
}
return pmc.pm.ListVersions()
}
func (sm *sourceManager) VendorCodeExists(n ProjectName) (bool, error) {
pms, err := sm.getProjectManager(n)
if err != nil {
return false, err
}
return pms.pm.CheckExistence(ExistsInVendorRoot), nil
}
func (sm *sourceManager) RepoExists(n ProjectName) (bool, error) {
pms, err := sm.getProjectManager(n)
if err != nil {
return false, err
}
return pms.pm.CheckExistence(ExistsInCache) || pms.pm.CheckExistence(ExistsUpstream), nil
}
func (sm *sourceManager) ExportProject(n ProjectName, 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 ProjectName.
//
// If no such manager yet exists, it attempts to create one.
func (sm *sourceManager) getProjectManager(n ProjectName) (*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 be more robust about this
r, err := vcs.NewRepo("https://"+string(n), repodir)
if err != nil {
// TODO be better
return nil, err
}
if !r.CheckLocal() {
// TODO cloning the repo here puts it on a blocking, and possibly
// unnecessary path. defer it
err = r.Get()
if err != nil {
// TODO 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 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 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 be better
return nil, fmt.Errorf("Err on JSON decoding metadata cache file: %s", err)
}
} else {
// TODO commented this out for now, until we manage it correctly
//pms.cf, err = os.Create(cpath)
//if err != nil {
//// TODO be better
//return nil, fmt.Errorf("Err on creating metadata cache file: %s", err)
//}
dc = &projectDataCache{
Infos: make(map[Revision]ProjectInfo),
VMap: make(map[Version]Revision),
RMap: make(map[Revision][]Version),
}
}
pm := &projectManager{
n: n,
ctx: sm.ctx,
vendordir: sm.basedir + "/vendor",
an: sm.an,
dc: dc,
crepo: &repo{
rpath: repodir,
r: r,
},
}
pms.pm = pm
sm.pms[n] = pms
return pms, nil
}