blob: 425c346ffac73212de18b656257a9fb3b9f1f9ca [file] [log] [blame] [edit]
#!/usr/bin/python
# Copyright (c) 2014 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Encompasses a revision file of a package.
A revision file associates an SVN revision with a package. The revision
file will encompass the package information for all package targets. All
packages must have already been built by the buildbot before a revision file
can be made for it.
"""
import hashlib
import json
import archive_info
import error
import package_info
import packages_info
FIELD_PACKAGE_NAME = 'package_name'
FIELD_REVISION = 'revision'
FIELD_PACKAGE_TARGETS = 'package_targets'
FIELD_REVISION_HASH = 'revision_hash'
class RevisionInfo(object):
"""Revision information object describing a set revision for a package."""
def __init__(self, packages_desc, revision_file=None):
"""Constructor for a RevisionInfo object.
Args:
packages_desc: Packages description containing all the necessary packages
and package targets to verify a revision file is complete.
revision_file: Optional JSON file representing a RevisionInfo object.
"""
assert isinstance(packages_desc, packages_info.PackagesInfo)
self._packages_desc = packages_desc
self._revision_num = None
self._package_name = None
# A revision describes all the package_info's for each package target.
# Every package target in the package_desc must have its revision set
# for the revision info to be valid.
self._package_targets = {}
if revision_file is not None:
self.LoadRevisionFile(revision_file)
def __eq__(self, other):
return (type(self) == type(other) and
self._revision_num == other._revision_num and
self._package_name == other._package_name and
self._package_targets == other._package_targets)
def _GetRevisionHash(self):
"""Returns a stable hash for a revision file for validation purposes."""
hash_string = str(self._revision_num)
hash_string += str(self._package_name)
for package_target in sorted(self._package_targets):
package_desc = self._package_targets[package_target]
archive_list = package_desc.GetArchiveList()
hash_string += str(package_target)
for archive in archive_list:
for field, member in archive.GetArchiveData()._asdict().iteritems():
hash_string += '[%s:%s]' % (field, member)
return hashlib.sha1(hash_string).hexdigest()
def _ValidateRevisionComplete(self):
"""Validate packages to make sure it matches the packages description."""
if self._package_name is None:
raise error.Error('Invalid revision information - '
'no package name.')
elif self._revision_num is None:
raise error.Error('Invalid revision information - '
'no revision identifier')
package_targets = self._packages_desc.GetPackageTargetsForPackage(
self._package_name
)
if package_targets:
package_targets = set(package_targets)
revision_targets = set(self._package_targets.keys())
if package_targets != revision_targets:
raise error.Error('Invalid revision information - '
'target mismatch:'
+ '\n%s:' % self._package_name
+ '\n Required Target Packages:'
+ '\n\t' + '\n\t'.join(sorted(package_targets))
+ '\n Supplied Target Packages:'
+ '\n\t' + '\n\t'.join(sorted(revision_targets)))
def LoadRevisionFile(self, revision_file, skip_hash_verify=False):
"""Loads a revision JSON file into this object.
Args:
revision_file: File name for a revision JSON file.
skip_hash_verify: If True, will skip the hash validation check. This
should only be used if a field has been added or
removed in order to recalculate the revision hash.
"""
try:
with open(revision_file, 'rt') as f:
revision_json = json.load(f)
self._package_name = revision_json[FIELD_PACKAGE_NAME]
self._revision_num = revision_json[FIELD_REVISION]
self._package_targets = {}
package_targets = revision_json[FIELD_PACKAGE_TARGETS]
for package_target, archive_list in package_targets.iteritems():
self._package_targets[package_target] = package_info.PackageInfo(
archive_list
)
except (TypeError, KeyError) as e:
raise error.Error('Invalid revision file [%s]: %s' %
(revision_file, e))
self._ValidateRevisionComplete()
if not skip_hash_verify:
hash_value = revision_json[FIELD_REVISION_HASH]
if self._GetRevisionHash() != hash_value:
raise error.Error('Invalid revision file [%s] - revision hash check '
'failed' % revision_file)
def SaveRevisionFile(self, revision_file):
"""Saves this object to a revision JSON file to be loaded later.
Args:
revision_file: File name where revision JSON file will be saved.
"""
self._ValidateRevisionComplete()
package_targets = {}
for package_target, package_desc in self._package_targets.iteritems():
package_targets[package_target] = package_desc.DumpPackageJson()
revision_json = {
FIELD_PACKAGE_NAME: self._package_name,
FIELD_REVISION: self._revision_num,
FIELD_PACKAGE_TARGETS: package_targets,
FIELD_REVISION_HASH: self._GetRevisionHash()
}
with open(revision_file, 'wt') as f:
json.dump(revision_json, f, sort_keys=True,
indent=2, separators=(',', ': '))
def SetRevisionNumber(self, revision_num):
"""Sets the current revision number for this object."""
self._revision_num = revision_num
def GetRevisionNumber(self):
"""Gets the currently set revision number for this object."""
return self._revision_num
def ClearRevisions(self):
"""Clears all package information for this object"""
self._package_name = None
self._package_targets = {}
def SetTargetRevision(self, package_name, package_target, package_desc):
"""Sets a package description for a package target.
The package description is a package_info object representing the package
for this particular revision.
Args:
package_name: Name of the package this revision object represents.
package_target: Package target name for the package we are setting.
package_desc: package_info object representing the package target.
"""
if self._package_name is None:
self._package_name = package_name
elif self._package_name != package_name:
raise error.Error('Revision information must be all for the '
'same package\n'
'Original package name: %s\nNew package name: %s'
% (self._package_name, package_name))
self._package_targets[package_target] = package_desc
def GetPackageInfo(self, package_target):
"""Gets the package description for a particular package target.
The package description is a package_info object representing the package
for this particular revision.
Args:
package_target: Package target name for which we want the package info.
Returns:
A package_info object for the package target, or None for invalid targets.
"""
return self._package_targets.get(package_target, None)