blob: 5549b8f778721c26b6e41caeb15f3f2733e93890 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package profiles
import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"v.io/jiri"
"v.io/jiri/runutil"
)
const (
defaultFileMode = os.FileMode(0644)
)
type Version int
const (
// Original, old-style profiles without a version #
Original Version = 0
// First version of new-style profiles.
V2 Version = 2
// V3 added support for recording the options that were used to install profiles.
V3 Version = 3
// V4 adds support for relative path names in profiles and environment variable.
V4 Version = 4
// V5 adds support for multiple profile installers.
V5 Version = 5
)
type profilesSchema struct {
XMLName xml.Name `xml:"profiles"`
// The Version of the schema used for this file.
Version Version `xml:"version,attr,omitempty"`
// The name of the installer that created the profiles in this file.
Installer string `xml:"installer,attr,omitempty"`
Profiles []*profileSchema `xml:"profile"`
}
type profileSchema struct {
XMLName xml.Name `xml:"profile"`
Name string `xml:"name,attr"`
Root string `xml:"root,attr"`
Targets []*targetSchema `xml:"target"`
}
type targetSchema struct {
XMLName xml.Name `xml:"target"`
Arch string `xml:"arch,attr"`
OS string `xml:"os,attr"`
InstallationDir string `xml:"installation-directory,attr"`
Version string `xml:"version,attr"`
UpdateTime time.Time `xml:"date,attr"`
Env Environment `xml:"envvars"`
CommandLineEnv Environment `xml:"command-line"`
}
type DB struct {
mu sync.Mutex
version Version
path string
db map[string]*Profile
}
// NewDB returns a new instance of a profile database.
func NewDB() *DB {
return &DB{db: make(map[string]*Profile), version: V5}
}
// Path returns the directory or filename that this database was read from.
func (pdb *DB) Path() string {
return pdb.path
}
// InstallProfile will create a new profile to the profiles database,
// it has no effect if the profile already exists. It returns the profile
// that was either newly created or already installed.
func (pdb *DB) InstallProfile(installer, name, root string) *Profile {
pdb.mu.Lock()
defer pdb.mu.Unlock()
qname := QualifiedProfileName(installer, name)
if p := pdb.db[qname]; p == nil {
pdb.db[qname] = &Profile{name: qname, root: root}
}
return pdb.db[qname]
}
// AddProfileTarget adds the specified target to the named profile.
// The UpdateTime of the newly installed target will be set to time.Now()
func (pdb *DB) AddProfileTarget(installer, name string, target Target) error {
pdb.mu.Lock()
defer pdb.mu.Unlock()
target.UpdateTime = time.Now()
qname := QualifiedProfileName(installer, name)
if pi, present := pdb.db[qname]; present {
for _, t := range pi.Targets() {
if target.Match(t) {
return fmt.Errorf("%s is already used by profile %s %s", target, qname, pi.Targets())
}
}
pi.targets = InsertTarget(pi.targets, &target)
return nil
}
return fmt.Errorf("profile %v is not installed", qname)
}
// UpdateProfileTarget updates the specified target from the named profile.
// The UpdateTime of the updated target will be set to time.Now()
func (pdb *DB) UpdateProfileTarget(installer, name string, target Target) error {
pdb.mu.Lock()
defer pdb.mu.Unlock()
target.UpdateTime = time.Now()
qname := QualifiedProfileName(installer, name)
pi, present := pdb.db[qname]
if !present {
return fmt.Errorf("profile %v is not installed", qname)
}
for _, t := range pi.targets {
if target.Match(t) {
*t = target
t.UpdateTime = time.Now()
return nil
}
}
return fmt.Errorf("profile %v does not have target: %v", qname, target)
}
// RemoveProfileTarget removes the specified target from the named profile.
// If this is the last target for the profile then the profile will be deleted
// from the database. It returns true if the profile was so deleted or did
// not originally exist.
func (pdb *DB) RemoveProfileTarget(installer, name string, target Target) bool {
pdb.mu.Lock()
defer pdb.mu.Unlock()
qname := QualifiedProfileName(installer, name)
pi, present := pdb.db[qname]
if !present {
return true
}
pi.targets = RemoveTarget(pi.targets, &target)
if len(pi.targets) == 0 {
delete(pdb.db, qname)
return true
}
return false
}
// Names returns the names, in lexicographic order, of all of the currently
// available profiles.
func (pdb *DB) Names() []string {
pdb.mu.Lock()
defer pdb.mu.Unlock()
return pdb.profilesUnlocked()
}
// Profiles returns all currently installed the profiles, in lexicographic order.
func (pdb *DB) Profiles() []*Profile {
pdb.mu.Lock()
defer pdb.mu.Unlock()
names := pdb.profilesUnlocked()
r := make([]*Profile, len(names), len(names))
for i, name := range names {
r[i] = pdb.db[name]
}
return r
}
func (pdb *DB) profilesUnlocked() []string {
names := make([]string, 0, len(pdb.db))
for name := range pdb.db {
names = append(names, name)
}
sort.Strings(names)
return names
}
// LookupProfile returns the profile for the supplied installer and profile
// name or nil if one is not found.
func (pdb *DB) LookupProfile(installer, name string) *Profile {
qname := QualifiedProfileName(installer, name)
pdb.mu.Lock()
defer pdb.mu.Unlock()
return pdb.db[qname]
}
// LookupProfileTarget returns the target information stored for the
// supplied installer, profile name and target.
func (pdb *DB) LookupProfileTarget(installer, name string, target Target) *Target {
qname := QualifiedProfileName(installer, name)
pdb.mu.Lock()
defer pdb.mu.Unlock()
mgr := pdb.db[qname]
if mgr == nil {
return nil
}
return FindTarget(mgr.targets, &target)
}
// EnvFromProfile obtains the environment variable settings from the specified
// profile and target. It returns nil if the target and/or profile could not
// be found.
func (pdb *DB) EnvFromProfile(installer, name string, target Target) []string {
t := pdb.LookupProfileTarget(installer, name, target)
if t == nil {
return nil
}
return t.Env.Vars
}
func getDBFilenames(jirix *jiri.X, path string) (bool, []string, error) {
s := jirix.NewSeq()
isdir, err := s.IsDir(path)
if err != nil {
return false, nil, err
}
if !isdir {
return false, []string{path}, nil
}
fis, err := s.ReadDir(path)
if err != nil {
return true, nil, err
}
paths := []string{}
for _, fi := range fis {
if strings.HasSuffix(fi.Name(), ".prev") {
continue
}
paths = append(paths, filepath.Join(path, fi.Name()))
}
return true, paths, nil
}
// Read reads the specified database directory or file to obtain the current
// set of installed profiles into the receiver database. It is not
// an error if the database does not exist, instead, an empty database
// is returned.
func (pdb *DB) Read(jirix *jiri.X, path string) error {
pdb.mu.Lock()
defer pdb.mu.Unlock()
pdb.db = make(map[string]*Profile)
isDir, filenames, err := getDBFilenames(jirix, path)
if err != nil {
return err
}
pdb.path = path
s := jirix.NewSeq()
for i, filename := range filenames {
data, err := s.ReadFile(filename)
if err != nil {
// It's not an error if the database doesn't exist yet, it'll
// just have no data in it and then be written out. This is the
// case when starting with a new/empty repo. The original profiles
// implementation behaved this way and I've tried to maintain it
// without having to special case all of the call sites.
if runutil.IsNotExist(err) {
continue
}
return err
}
var schema profilesSchema
if err := xml.Unmarshal(data, &schema); err != nil {
return fmt.Errorf("Unmarshal(%v) failed: %v", string(data), err)
}
if isDir {
if schema.Version < V5 {
return fmt.Errorf("Profile database files must be at version %d (not %d) when more than one is found in a directory", V5, schema.Version)
}
if i >= 1 && pdb.version != schema.Version {
return fmt.Errorf("Profile database files must have the same version (%d != %d) when more than one is found in a directory", pdb.version, schema.Version)
}
}
pdb.version = schema.Version
for _, p := range schema.Profiles {
qname := QualifiedProfileName(schema.Installer, p.Name)
pdb.db[qname] = &Profile{
// Use the unqualified name in each profile since the
// reader will read the installer from the xml installer
// tag.
name: p.Name,
installer: schema.Installer,
root: p.Root,
}
for _, target := range p.Targets {
pdb.db[qname].targets = append(pdb.db[qname].targets, &Target{
arch: target.Arch,
opsys: target.OS,
Env: target.Env,
commandLineEnv: target.CommandLineEnv,
version: target.Version,
UpdateTime: target.UpdateTime,
InstallationDir: target.InstallationDir,
isSet: true,
})
}
}
}
return nil
}
// Write writes the current set of installed profiles to the specified
// database location. No data will be written and an error returned if the
// path is a directory and installer is an empty string.
func (pdb *DB) Write(jirix *jiri.X, installer, path string) error {
pdb.mu.Lock()
defer pdb.mu.Unlock()
if len(path) == 0 {
return fmt.Errorf("please specify a profiles database path")
}
s := jirix.NewSeq()
isdir, err := s.IsDir(path)
if err != nil && !runutil.IsNotExist(err) {
return err
}
filename := path
if isdir {
if installer == "" {
return fmt.Errorf("no installer specified for directory path %v", path)
}
filename = filepath.Join(filename, installer)
}
var schema profilesSchema
schema.Version = V5
schema.Installer = installer
for _, name := range pdb.profilesUnlocked() {
profileInstaller, profileName := SplitProfileName(name)
if profileInstaller != installer {
continue
}
profile := pdb.db[name]
current := &profileSchema{Name: profileName, Root: profile.root}
schema.Profiles = append(schema.Profiles, current)
for _, target := range profile.targets {
sort.Strings(target.Env.Vars)
if len(target.version) == 0 {
return fmt.Errorf("missing version for profile %s target: %s", name, target)
}
current.Targets = append(current.Targets,
&targetSchema{
Arch: target.arch,
OS: target.opsys,
Env: target.Env,
CommandLineEnv: target.commandLineEnv,
Version: target.version,
InstallationDir: target.InstallationDir,
UpdateTime: target.UpdateTime,
})
}
}
data, err := xml.MarshalIndent(schema, "", " ")
if err != nil {
return fmt.Errorf("MarshalIndent() failed: %v", err)
}
oldName := filename + ".prev"
newName := filename + fmt.Sprintf(".%d", time.Now().UnixNano())
if err := s.WriteFile(newName, data, defaultFileMode).
AssertFileExists(filename).
Rename(filename, oldName).Done(); err != nil && !runutil.IsNotExist(err) {
return err
}
if err := s.Rename(newName, filename).Done(); err != nil {
return err
}
return nil
}
// SchemaVersion returns the version of the xml schema used to implement
// the database.
func (pdb *DB) SchemaVersion() Version {
pdb.mu.Lock()
defer pdb.mu.Unlock()
return pdb.version
}