v.io/jiri@v0.0.0-20160715023856-abfb8b131290/profiles/manifest.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package profiles
     6  
     7  import (
     8  	"encoding/xml"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"v.io/jiri"
    18  	"v.io/jiri/runutil"
    19  )
    20  
    21  const (
    22  	defaultFileMode = os.FileMode(0644)
    23  )
    24  
    25  type Version int
    26  
    27  const (
    28  	// Original, old-style profiles without a version #
    29  	Original Version = 0
    30  	// First version of new-style profiles.
    31  	V2 Version = 2
    32  	// V3 added support for recording the options that were used to install profiles.
    33  	V3 Version = 3
    34  	// V4 adds support for relative path names in profiles and environment variable.
    35  	V4 Version = 4
    36  	// V5 adds support for multiple profile installers.
    37  	V5 Version = 5
    38  )
    39  
    40  type profilesSchema struct {
    41  	XMLName xml.Name `xml:"profiles"`
    42  	// The Version of the schema used for this file.
    43  	Version Version `xml:"version,attr,omitempty"`
    44  	// The name of the installer that created the profiles in this file.
    45  	Installer string           `xml:"installer,attr,omitempty"`
    46  	Profiles  []*profileSchema `xml:"profile"`
    47  }
    48  
    49  type profileSchema struct {
    50  	XMLName xml.Name        `xml:"profile"`
    51  	Name    string          `xml:"name,attr"`
    52  	Root    string          `xml:"root,attr"`
    53  	Targets []*targetSchema `xml:"target"`
    54  }
    55  
    56  type targetSchema struct {
    57  	XMLName         xml.Name    `xml:"target"`
    58  	Arch            string      `xml:"arch,attr"`
    59  	OS              string      `xml:"os,attr"`
    60  	InstallationDir string      `xml:"installation-directory,attr"`
    61  	Version         string      `xml:"version,attr"`
    62  	UpdateTime      time.Time   `xml:"date,attr"`
    63  	Env             Environment `xml:"envvars"`
    64  	CommandLineEnv  Environment `xml:"command-line"`
    65  }
    66  
    67  type DB struct {
    68  	mu      sync.Mutex
    69  	version Version
    70  	path    string
    71  	db      map[string]*Profile
    72  }
    73  
    74  // NewDB returns a new instance of a profile database.
    75  func NewDB() *DB {
    76  	return &DB{db: make(map[string]*Profile), version: V5}
    77  }
    78  
    79  // Path returns the directory or filename that this database was read from.
    80  func (pdb *DB) Path() string {
    81  	return pdb.path
    82  }
    83  
    84  // InstallProfile will create a new profile to the profiles database,
    85  // it has no effect if the profile already exists. It returns the profile
    86  // that was either newly created or already installed.
    87  func (pdb *DB) InstallProfile(installer, name, root string) *Profile {
    88  	pdb.mu.Lock()
    89  	defer pdb.mu.Unlock()
    90  	qname := QualifiedProfileName(installer, name)
    91  	if p := pdb.db[qname]; p == nil {
    92  		pdb.db[qname] = &Profile{name: qname, root: root}
    93  	}
    94  	return pdb.db[qname]
    95  }
    96  
    97  // AddProfileTarget adds the specified target to the named profile.
    98  // The UpdateTime of the newly installed target will be set to time.Now()
    99  func (pdb *DB) AddProfileTarget(installer, name string, target Target) error {
   100  	pdb.mu.Lock()
   101  	defer pdb.mu.Unlock()
   102  	target.UpdateTime = time.Now()
   103  	qname := QualifiedProfileName(installer, name)
   104  	if pi, present := pdb.db[qname]; present {
   105  		for _, t := range pi.Targets() {
   106  			if target.Match(t) {
   107  				return fmt.Errorf("%s is already used by profile %s %s", target, qname, pi.Targets())
   108  			}
   109  		}
   110  		pi.targets = InsertTarget(pi.targets, &target)
   111  		return nil
   112  	}
   113  	return fmt.Errorf("profile %v is not installed", qname)
   114  }
   115  
   116  // UpdateProfileTarget updates the specified target from the named profile.
   117  // The UpdateTime of the updated target will be set to time.Now()
   118  func (pdb *DB) UpdateProfileTarget(installer, name string, target Target) error {
   119  	pdb.mu.Lock()
   120  	defer pdb.mu.Unlock()
   121  	target.UpdateTime = time.Now()
   122  	qname := QualifiedProfileName(installer, name)
   123  	pi, present := pdb.db[qname]
   124  	if !present {
   125  		return fmt.Errorf("profile %v is not installed", qname)
   126  	}
   127  	for _, t := range pi.targets {
   128  		if target.Match(t) {
   129  			*t = target
   130  			t.UpdateTime = time.Now()
   131  			return nil
   132  		}
   133  	}
   134  	return fmt.Errorf("profile %v does not have target: %v", qname, target)
   135  }
   136  
   137  // RemoveProfileTarget removes the specified target from the named profile.
   138  // If this is the last target for the profile then the profile will be deleted
   139  // from the database. It returns true if the profile was so deleted or did
   140  // not originally exist.
   141  func (pdb *DB) RemoveProfileTarget(installer, name string, target Target) bool {
   142  	pdb.mu.Lock()
   143  	defer pdb.mu.Unlock()
   144  	qname := QualifiedProfileName(installer, name)
   145  	pi, present := pdb.db[qname]
   146  	if !present {
   147  		return true
   148  	}
   149  	pi.targets = RemoveTarget(pi.targets, &target)
   150  	if len(pi.targets) == 0 {
   151  		delete(pdb.db, qname)
   152  		return true
   153  	}
   154  	return false
   155  }
   156  
   157  // Names returns the names, in lexicographic order, of all of the currently
   158  // available profiles.
   159  func (pdb *DB) Names() []string {
   160  	pdb.mu.Lock()
   161  	defer pdb.mu.Unlock()
   162  	return pdb.profilesUnlocked()
   163  }
   164  
   165  // Profiles returns all currently installed the profiles, in lexicographic order.
   166  func (pdb *DB) Profiles() []*Profile {
   167  	pdb.mu.Lock()
   168  	defer pdb.mu.Unlock()
   169  	names := pdb.profilesUnlocked()
   170  	r := make([]*Profile, len(names), len(names))
   171  	for i, name := range names {
   172  		r[i] = pdb.db[name]
   173  	}
   174  	return r
   175  }
   176  
   177  func (pdb *DB) profilesUnlocked() []string {
   178  	names := make([]string, 0, len(pdb.db))
   179  	for name := range pdb.db {
   180  		names = append(names, name)
   181  	}
   182  	sort.Strings(names)
   183  	return names
   184  }
   185  
   186  // LookupProfile returns the profile for the supplied installer and profile
   187  // name or nil if one is not found.
   188  func (pdb *DB) LookupProfile(installer, name string) *Profile {
   189  	qname := QualifiedProfileName(installer, name)
   190  	pdb.mu.Lock()
   191  	defer pdb.mu.Unlock()
   192  	return pdb.db[qname]
   193  }
   194  
   195  // LookupProfileTarget returns the target information stored for the
   196  // supplied installer, profile name and target.
   197  func (pdb *DB) LookupProfileTarget(installer, name string, target Target) *Target {
   198  	qname := QualifiedProfileName(installer, name)
   199  	pdb.mu.Lock()
   200  	defer pdb.mu.Unlock()
   201  	mgr := pdb.db[qname]
   202  	if mgr == nil {
   203  		return nil
   204  	}
   205  	return FindTarget(mgr.targets, &target)
   206  }
   207  
   208  // EnvFromProfile obtains the environment variable settings from the specified
   209  // profile and target. It returns nil if the target and/or profile could not
   210  // be found.
   211  func (pdb *DB) EnvFromProfile(installer, name string, target Target) []string {
   212  	t := pdb.LookupProfileTarget(installer, name, target)
   213  	if t == nil {
   214  		return nil
   215  	}
   216  	return t.Env.Vars
   217  }
   218  
   219  func getDBFilenames(jirix *jiri.X, path string) (bool, []string, error) {
   220  	s := jirix.NewSeq()
   221  	isdir, err := s.IsDir(path)
   222  	if err != nil {
   223  		return false, nil, err
   224  	}
   225  	if !isdir {
   226  		return false, []string{path}, nil
   227  	}
   228  	fis, err := s.ReadDir(path)
   229  	if err != nil {
   230  		return true, nil, err
   231  	}
   232  	paths := []string{}
   233  	for _, fi := range fis {
   234  		if strings.HasSuffix(fi.Name(), ".prev") {
   235  			continue
   236  		}
   237  		paths = append(paths, filepath.Join(path, fi.Name()))
   238  	}
   239  	return true, paths, nil
   240  }
   241  
   242  // Read reads the specified database directory or file to obtain the current
   243  // set of installed profiles into the receiver database. It is not
   244  // an error if the database does not exist, instead, an empty database
   245  // is returned.
   246  func (pdb *DB) Read(jirix *jiri.X, path string) error {
   247  	pdb.mu.Lock()
   248  	defer pdb.mu.Unlock()
   249  	pdb.db = make(map[string]*Profile)
   250  	isDir, filenames, err := getDBFilenames(jirix, path)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	pdb.path = path
   255  	s := jirix.NewSeq()
   256  	for i, filename := range filenames {
   257  		data, err := s.ReadFile(filename)
   258  		if err != nil {
   259  			// It's not an error if the database doesn't exist yet, it'll
   260  			// just have no data in it and then be written out. This is the
   261  			// case when starting with a new/empty repo. The original profiles
   262  			// implementation behaved this way and I've tried to maintain it
   263  			// without having to special case all of the call sites.
   264  			if runutil.IsNotExist(err) {
   265  				continue
   266  			}
   267  			return err
   268  		}
   269  		var schema profilesSchema
   270  		if err := xml.Unmarshal(data, &schema); err != nil {
   271  			return fmt.Errorf("Unmarshal(%v) failed: %v", string(data), err)
   272  		}
   273  		if isDir {
   274  			if schema.Version < V5 {
   275  				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)
   276  			}
   277  			if i >= 1 && pdb.version != schema.Version {
   278  				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)
   279  			}
   280  		}
   281  		pdb.version = schema.Version
   282  		for _, p := range schema.Profiles {
   283  			qname := QualifiedProfileName(schema.Installer, p.Name)
   284  			pdb.db[qname] = &Profile{
   285  				// Use the unqualified name in each profile since the
   286  				// reader will read the installer from the xml installer
   287  				// tag.
   288  				name:      p.Name,
   289  				installer: schema.Installer,
   290  				root:      p.Root,
   291  			}
   292  			for _, target := range p.Targets {
   293  				pdb.db[qname].targets = append(pdb.db[qname].targets, &Target{
   294  					arch:            target.Arch,
   295  					opsys:           target.OS,
   296  					Env:             target.Env,
   297  					commandLineEnv:  target.CommandLineEnv,
   298  					version:         target.Version,
   299  					UpdateTime:      target.UpdateTime,
   300  					InstallationDir: target.InstallationDir,
   301  					isSet:           true,
   302  				})
   303  			}
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  // Write writes the current set of installed profiles to the specified
   310  // database location. No data will be written and an error returned if the
   311  // path is a directory and installer is an empty string.
   312  func (pdb *DB) Write(jirix *jiri.X, installer, path string) error {
   313  	pdb.mu.Lock()
   314  	defer pdb.mu.Unlock()
   315  
   316  	if len(path) == 0 {
   317  		return fmt.Errorf("please specify a profiles database path")
   318  	}
   319  
   320  	s := jirix.NewSeq()
   321  	isdir, err := s.IsDir(path)
   322  	if err != nil && !runutil.IsNotExist(err) {
   323  		return err
   324  	}
   325  	filename := path
   326  	if isdir {
   327  		if installer == "" {
   328  			return fmt.Errorf("no installer specified for directory path %v", path)
   329  		}
   330  		filename = filepath.Join(filename, installer)
   331  	}
   332  
   333  	var schema profilesSchema
   334  	schema.Version = V5
   335  	schema.Installer = installer
   336  	for _, name := range pdb.profilesUnlocked() {
   337  		profileInstaller, profileName := SplitProfileName(name)
   338  		if profileInstaller != installer {
   339  			continue
   340  		}
   341  		profile := pdb.db[name]
   342  		current := &profileSchema{Name: profileName, Root: profile.root}
   343  		schema.Profiles = append(schema.Profiles, current)
   344  
   345  		for _, target := range profile.targets {
   346  			sort.Strings(target.Env.Vars)
   347  			if len(target.version) == 0 {
   348  				return fmt.Errorf("missing version for profile %s target: %s", name, target)
   349  			}
   350  			current.Targets = append(current.Targets,
   351  				&targetSchema{
   352  					Arch:            target.arch,
   353  					OS:              target.opsys,
   354  					Env:             target.Env,
   355  					CommandLineEnv:  target.commandLineEnv,
   356  					Version:         target.version,
   357  					InstallationDir: target.InstallationDir,
   358  					UpdateTime:      target.UpdateTime,
   359  				})
   360  		}
   361  	}
   362  
   363  	data, err := xml.MarshalIndent(schema, "", "  ")
   364  	if err != nil {
   365  		return fmt.Errorf("MarshalIndent() failed: %v", err)
   366  	}
   367  
   368  	oldName := filename + ".prev"
   369  	newName := filename + fmt.Sprintf(".%d", time.Now().UnixNano())
   370  
   371  	if err := s.WriteFile(newName, data, defaultFileMode).
   372  		AssertFileExists(filename).
   373  		Rename(filename, oldName).Done(); err != nil && !runutil.IsNotExist(err) {
   374  		return err
   375  	}
   376  	if err := s.Rename(newName, filename).Done(); err != nil {
   377  		return err
   378  	}
   379  	return nil
   380  }
   381  
   382  // SchemaVersion returns the version of the xml schema used to implement
   383  // the database.
   384  func (pdb *DB) SchemaVersion() Version {
   385  	pdb.mu.Lock()
   386  	defer pdb.mu.Unlock()
   387  	return pdb.version
   388  }