2 # Copyright (c) 2014 The Native Client Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Encompasses a revision file of a package.
8 A revision file associates an SVN revision with a package. The revision
9 file will encompass the package information for all package targets. All
10 packages must have already been built by the buildbot before a revision file
23 FIELD_PACKAGE_NAME = 'package_name'
24 FIELD_REVISION = 'revision'
25 FIELD_PACKAGE_TARGETS = 'package_targets'
27 FIELD_REVISION_HASH = 'revision_hash'
30 class RevisionInfo(object):
31 """Revision information object describing a set revision for a package."""
32 def __init__(self, packages_desc, revision_file=None):
33 """Constructor for a RevisionInfo object.
36 packages_desc: Packages description containing all the necessary packages
37 and package targets to verify a revision file is complete.
38 revision_file: Optional JSON file representing a RevisionInfo object.
40 assert isinstance(packages_desc, packages_info.PackagesInfo)
41 self._packages_desc = packages_desc
42 self._revision_num = -1
43 self._package_name = None
45 # A revision describes all the package_info's for each package target.
46 # Every package target in the package_desc must have its revision set
47 # for the revision info to be valid.
48 self._package_targets = {}
50 if revision_file is not None:
51 self.LoadRevisionFile(revision_file)
53 def __eq__(self, other):
54 return (type(self) == type(other) and
55 self._revision_num == other._revision_num and
56 self._package_name == other._package_name and
57 self._package_targets == other._package_targets)
59 def _GetRevisionHash(self):
60 """Returns a stable hash for a revision file for validation purposes."""
61 hash_string = str(self._revision_num)
62 hash_string += str(self._package_name)
63 for package_target in sorted(self._package_targets):
64 package_desc = self._package_targets[package_target]
65 archive_list = package_desc.GetArchiveList()
67 hash_string += str(package_target)
68 for archive in archive_list:
69 for field, member in archive.GetArchiveData()._asdict().iteritems():
70 hash_string += '[%s:%s]' % (field, member)
72 return hashlib.sha1(hash_string).hexdigest()
74 def _ValidateRevisionComplete(self):
75 """Validate packages to make sure it matches the packages description."""
76 if self._package_name is None:
77 raise error.Error('Invalid revision information - '
79 elif self._revision_num == -1:
80 raise error.Error('Invalid revision information - '
83 package_targets = self._packages_desc.GetPackageTargetsForPackage(
88 package_targets = set(package_targets)
89 revision_targets = set(self._package_targets.keys())
91 if package_targets != revision_targets:
92 raise error.Error('Invalid revision information - '
94 + '\n%s:' % self._package_name
95 + '\n Required Target Packages:'
96 + '\n\t' + '\n\t'.join(sorted(package_targets))
97 + '\n Supplied Target Packages:'
98 + '\n\t' + '\n\t'.join(sorted(revision_targets)))
100 def LoadRevisionFile(self, revision_file, skip_hash_verify=False):
101 """Loads a revision JSON file into this object.
104 revision_file: File name for a revision JSON file.
105 skip_hash_verify: If True, will skip the hash validation check. This
106 should only be used if a field has been added or
107 removed in order to recalculate the revision hash.
110 with open(revision_file, 'rt') as f:
111 revision_json = json.load(f)
113 self._package_name = revision_json[FIELD_PACKAGE_NAME]
114 self._revision_num = revision_json[FIELD_REVISION]
115 self._package_targets = {}
117 package_targets = revision_json[FIELD_PACKAGE_TARGETS]
118 for package_target, archive_list in package_targets.iteritems():
119 self._package_targets[package_target] = package_info.PackageInfo(
122 except (TypeError, KeyError) as e:
123 raise error.Error('Invalid revision file [%s]: %s' %
126 self._ValidateRevisionComplete()
128 if not skip_hash_verify:
129 hash_value = revision_json[FIELD_REVISION_HASH]
130 if self._GetRevisionHash() != hash_value:
131 raise error.Error('Invalid revision file [%s] - revision hash check '
132 'failed' % revision_file)
134 def SaveRevisionFile(self, revision_file):
135 """Saves this object to a revision JSON file to be loaded later.
138 revision_file: File name where revision JSON file will be saved.
140 self._ValidateRevisionComplete()
143 for package_target, package_desc in self._package_targets.iteritems():
144 package_targets[package_target] = package_desc.DumpPackageJson()
147 FIELD_PACKAGE_NAME: self._package_name,
148 FIELD_REVISION: self._revision_num,
149 FIELD_PACKAGE_TARGETS: package_targets,
150 FIELD_REVISION_HASH: self._GetRevisionHash()
153 with open(revision_file, 'wt') as f:
154 json.dump(revision_json, f, sort_keys=True,
155 indent=2, separators=(',', ': '))
157 def SetRevisionNumber(self, revision_num):
158 """Sets the current revision number for this object."""
159 self._revision_num = revision_num
161 def GetRevisionNumber(self):
162 """Gets the currently set revision number for this object."""
163 return self._revision_num
165 def ClearRevisions(self):
166 """Clears all package information for this object"""
167 self._package_name = None
168 self._package_targets = {}
170 def SetTargetRevision(self, package_name, package_target, package_desc):
171 """Sets a package description for a package target.
173 The package description is a package_info object representing the package
174 for this particular revision.
177 package_name: Name of the package this revision object represents.
178 package_target: Package target name for the package we are setting.
179 package_desc: package_info object representing the package target.
181 if self._package_name is None:
182 self._package_name = package_name
183 elif self._package_name != package_name:
184 raise error.Error('Revision information must be all for the '
186 'Original package name: %s\nNew package name: %s'
187 % (self._package_name, package_name))
188 self._package_targets[package_target] = package_desc
190 def GetPackageInfo(self, package_target):
191 """Gets the package description for a particular package target.
193 The package description is a package_info object representing the package
194 for this particular revision.
197 package_target: Package target name for which we want the package info.
200 A package_info object for the package target, or None for invalid targets.
202 return self._package_targets.get(package_target, None)