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 }