|  | # Copyright 2015 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | """A simple cross-platform helper to create a debian package.""" | 
|  |  | 
|  | import gzip | 
|  | import hashlib | 
|  | from io import BytesIO | 
|  | import os.path | 
|  | import sys | 
|  | import tarfile | 
|  | import textwrap | 
|  | import time | 
|  |  | 
|  | from third_party.py import gflags | 
|  |  | 
|  | # list of debian fields : (name, mandatory, wrap[, default]) | 
|  | # see http://www.debian.org/doc/debian-policy/ch-controlfields.html | 
|  | DEBIAN_FIELDS = [ | 
|  | ('Package', True, False), | 
|  | ('Version', True, False), | 
|  | ('Section', False, False, 'contrib/devel'), | 
|  | ('Priority', False, False, 'optional'), | 
|  | ('Architecture', False, False, 'all'), | 
|  | ('Depends', False, True, []), | 
|  | ('Recommends', False, True, []), | 
|  | ('Suggests', False, True, []), | 
|  | ('Enhances', False, True, []), | 
|  | ('Conflicts', False, True, []), | 
|  | ('Pre-Depends', False, True, []), | 
|  | ('Installed-Size', False, False), | 
|  | ('Maintainer', True, False), | 
|  | ('Description', True, True), | 
|  | ('Homepage', False, False), | 
|  | ('Built-Using', False, False, 'Bazel'), | 
|  | ('Distribution', False, False, 'unstable'), | 
|  | ('Urgency', False, False, 'medium'), | 
|  | ] | 
|  |  | 
|  | gflags.DEFINE_string('output', None, 'The output file, mandatory') | 
|  | gflags.MarkFlagAsRequired('output') | 
|  |  | 
|  | gflags.DEFINE_string('changes', None, 'The changes output file, mandatory.') | 
|  | gflags.MarkFlagAsRequired('changes') | 
|  |  | 
|  | gflags.DEFINE_string('data', None, | 
|  | 'Path to the data tarball, mandatory') | 
|  | gflags.MarkFlagAsRequired('data') | 
|  |  | 
|  | gflags.DEFINE_string('preinst', None, | 
|  | 'The preinst script (prefix with @ to provide a path).') | 
|  | gflags.DEFINE_string('postinst', None, | 
|  | 'The postinst script (prefix with @ to provide a path).') | 
|  | gflags.DEFINE_string('prerm', None, | 
|  | 'The prerm script (prefix with @ to provide a path).') | 
|  | gflags.DEFINE_string('postrm', None, | 
|  | 'The postrm script (prefix with @ to provide a path).') | 
|  |  | 
|  |  | 
|  | # see | 
|  | # https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-conffile | 
|  | gflags.DEFINE_multistring( | 
|  | 'conffile', None, | 
|  | 'List of conffiles (prefix item with @ to provide a path)') | 
|  |  | 
|  |  | 
|  | def MakeGflags(): | 
|  | for field in DEBIAN_FIELDS: | 
|  | fieldname = field[0].replace('-', '_').lower() | 
|  | msg = 'The value for the %s content header entry.' % field[0] | 
|  | if len(field) > 3: | 
|  | if type(field[3]) is list: | 
|  | gflags.DEFINE_multistring(fieldname, field[3], msg) | 
|  | else: | 
|  | gflags.DEFINE_string(fieldname, field[3], msg) | 
|  | else: | 
|  | gflags.DEFINE_string(fieldname, None, msg) | 
|  | if field[1]: | 
|  | gflags.MarkFlagAsRequired(fieldname) | 
|  |  | 
|  |  | 
|  | def AddArFileEntry(fileobj, filename, | 
|  | content='', timestamp=0, | 
|  | owner_id=0, group_id=0, mode=0o644): | 
|  | """Add a AR file entry to fileobj.""" | 
|  | inputs = [ | 
|  | (filename + '/').ljust(16),  # filename (SysV) | 
|  | str(timestamp).ljust(12),  # timestamp | 
|  | str(owner_id).ljust(6),  # owner id | 
|  | str(group_id).ljust(6),  # group id | 
|  | oct(mode).ljust(8),  # mode | 
|  | str(len(content)).ljust(10),  # size | 
|  | '\x60\x0a',  # end of file entry | 
|  | ] | 
|  | for i in inputs: | 
|  | fileobj.write(i.encode('ascii')) | 
|  | fileobj.write(content) | 
|  | if len(content) % 2 != 0: | 
|  | fileobj.write(b'\n')  # 2-byte alignment padding | 
|  |  | 
|  |  | 
|  | def MakeDebianControlField(name, value, wrap=False): | 
|  | """Add a field to a debian control file.""" | 
|  | result = name + ': ' | 
|  | if type(value) is list: | 
|  | value = ', '.join(value) | 
|  | if wrap: | 
|  | result += ' '.join(value.split('\n')) | 
|  | result = textwrap.fill(result, | 
|  | break_on_hyphens=False, | 
|  | break_long_words=False) | 
|  | else: | 
|  | result += value | 
|  | return result.replace('\n', '\n ') + '\n' | 
|  |  | 
|  |  | 
|  | def CreateDebControl(extrafiles=None, **kwargs): | 
|  | """Create the control.tar.gz file.""" | 
|  | # create the control file | 
|  | controlfile = '' | 
|  | for values in DEBIAN_FIELDS: | 
|  | fieldname = values[0] | 
|  | key = fieldname[0].lower() + fieldname[1:].replace('-', '') | 
|  | if values[1] or (key in kwargs and kwargs[key]): | 
|  | controlfile += MakeDebianControlField(fieldname, kwargs[key], values[2]) | 
|  | # Create the control.tar file | 
|  | tar = BytesIO() | 
|  | with gzip.GzipFile('control.tar.gz', mode='w', fileobj=tar, mtime=0) as gz: | 
|  | with tarfile.open('control.tar.gz', mode='w', fileobj=gz) as f: | 
|  | tarinfo = tarfile.TarInfo('control') | 
|  | tarinfo.size = len(controlfile) | 
|  | f.addfile(tarinfo, fileobj=BytesIO(controlfile.encode('utf-8'))) | 
|  | if extrafiles: | 
|  | for name, (data, mode) in extrafiles.items(): | 
|  | tarinfo = tarfile.TarInfo(name) | 
|  | tarinfo.size = len(data) | 
|  | tarinfo.mode = mode | 
|  | f.addfile(tarinfo, fileobj=BytesIO(data.encode('utf-8'))) | 
|  | control = tar.getvalue() | 
|  | tar.close() | 
|  | return control | 
|  |  | 
|  |  | 
|  | def CreateDeb(output, | 
|  | data, | 
|  | preinst=None, | 
|  | postinst=None, | 
|  | prerm=None, | 
|  | postrm=None, | 
|  | conffiles=None, | 
|  | **kwargs): | 
|  | """Create a full debian package.""" | 
|  | extrafiles = {} | 
|  | if preinst: | 
|  | extrafiles['preinst'] = (preinst, 0o755) | 
|  | if postinst: | 
|  | extrafiles['postinst'] = (postinst, 0o755) | 
|  | if prerm: | 
|  | extrafiles['prerm'] = (prerm, 0o755) | 
|  | if postrm: | 
|  | extrafiles['postrm'] = (postrm, 0o755) | 
|  | if conffiles: | 
|  | extrafiles['conffiles'] = ('\n'.join(conffiles) + '\n', 0o644) | 
|  | control = CreateDebControl(extrafiles=extrafiles, **kwargs) | 
|  |  | 
|  | # Write the final AR archive (the deb package) | 
|  | with open(output, 'wb') as f: | 
|  | f.write(b'!<arch>\n')  # Magic AR header | 
|  | AddArFileEntry(f, 'debian-binary', b'2.0\n') | 
|  | AddArFileEntry(f, 'control.tar.gz', control) | 
|  | # Tries to preserve the extension name | 
|  | ext = os.path.basename(data).split('.')[-2:] | 
|  | if len(ext) < 2: | 
|  | ext = 'tar' | 
|  | elif ext[1] == 'tgz': | 
|  | ext = 'tar.gz' | 
|  | elif ext[1] == 'tar.bzip2': | 
|  | ext = 'tar.bz2' | 
|  | else: | 
|  | ext = '.'.join(ext) | 
|  | if ext not in ['tar.bz2', 'tar.gz', 'tar.xz', 'tar.lzma']: | 
|  | ext = 'tar' | 
|  | with open(data, 'rb') as datafile: | 
|  | data = datafile.read() | 
|  | AddArFileEntry(f, 'data.' + ext, data) | 
|  |  | 
|  |  | 
|  | def GetChecksumsFromFile(filename, hash_fns=None): | 
|  | """Computes MD5 and/or other checksums of a file. | 
|  |  | 
|  | Args: | 
|  | filename: Name of the file. | 
|  | hash_fns: Mapping of hash functions. | 
|  | Default is {'md5': hashlib.md5} | 
|  |  | 
|  | Returns: | 
|  | Mapping of hash names to hexdigest strings. | 
|  | { <hashname>: <hexdigest>, ... } | 
|  | """ | 
|  | hash_fns = hash_fns or {'md5': hashlib.md5} | 
|  | checksums = {k: fn() for (k, fn) in hash_fns.items()} | 
|  |  | 
|  | with open(filename, 'rb') as file_handle: | 
|  | while True: | 
|  | buf = file_handle.read(1048576)  # 1 MiB | 
|  | if not buf: | 
|  | break | 
|  | for hashfn in checksums.values(): | 
|  | hashfn.update(buf) | 
|  |  | 
|  | return {k: fn.hexdigest() for (k, fn) in checksums.items()} | 
|  |  | 
|  |  | 
|  | def CreateChanges(output, | 
|  | deb_file, | 
|  | architecture, | 
|  | short_description, | 
|  | maintainer, | 
|  | package, | 
|  | version, | 
|  | section, | 
|  | priority, | 
|  | distribution, | 
|  | urgency, | 
|  | timestamp=0): | 
|  | """Create the changes file.""" | 
|  | checksums = GetChecksumsFromFile(deb_file, {'md5': hashlib.md5, | 
|  | 'sha1': hashlib.sha1, | 
|  | 'sha256': hashlib.sha256}) | 
|  | debsize = str(os.path.getsize(deb_file)) | 
|  | deb_basename = os.path.basename(deb_file) | 
|  |  | 
|  | changesdata = ''.join( | 
|  | MakeDebianControlField(*x) | 
|  | for x in [('Format', '1.8'), ('Date', time.ctime(timestamp)), ( | 
|  | 'Source', package | 
|  | ), ('Binary', package | 
|  | ), ('Architecture', architecture), ('Version', version), ( | 
|  | 'Distribution', distribution | 
|  | ), ('Urgency', urgency), ('Maintainer', maintainer), ( | 
|  | 'Changed-By', maintainer | 
|  | ), ('Description', '\n%s - %s' % (package, short_description) | 
|  | ), ('Changes', ('\n%s (%s) %s; urgency=%s' | 
|  | '\nChanges are tracked in revision control.' | 
|  | ) % (package, version, distribution, urgency) | 
|  | ), ('Files', '\n' + ' '.join( | 
|  | [checksums['md5'], debsize, section, priority, deb_basename]) | 
|  | ), ('Checksums-Sha1', '\n' + ' '.join( | 
|  | [checksums['sha1'], debsize, deb_basename]) | 
|  | ), ('Checksums-Sha256', '\n' + ' '.join( | 
|  | [checksums['sha256'], debsize, deb_basename]))]) | 
|  | with open(output, 'w') as changes_fh: | 
|  | changes_fh.write(changesdata) | 
|  |  | 
|  |  | 
|  | def GetFlagValue(flagvalue, strip=True): | 
|  | if flagvalue: | 
|  | if flagvalue[0] == '@': | 
|  | with open(flagvalue[1:], 'r') as f: | 
|  | flagvalue = f.read() | 
|  | if strip: | 
|  | return flagvalue.strip() | 
|  | return flagvalue | 
|  |  | 
|  |  | 
|  | def GetFlagValues(flagvalues): | 
|  | if flagvalues: | 
|  | return [GetFlagValue(f, False) for f in flagvalues] | 
|  | else: | 
|  | return None | 
|  |  | 
|  |  | 
|  | def main(unused_argv): | 
|  | CreateDeb( | 
|  | FLAGS.output, | 
|  | FLAGS.data, | 
|  | preinst=GetFlagValue(FLAGS.preinst, False), | 
|  | postinst=GetFlagValue(FLAGS.postinst, False), | 
|  | prerm=GetFlagValue(FLAGS.prerm, False), | 
|  | postrm=GetFlagValue(FLAGS.postrm, False), | 
|  | conffiles=GetFlagValues(FLAGS.conffile), | 
|  | package=FLAGS.package, | 
|  | version=GetFlagValue(FLAGS.version), | 
|  | description=GetFlagValue(FLAGS.description), | 
|  | maintainer=FLAGS.maintainer, | 
|  | section=FLAGS.section, | 
|  | architecture=FLAGS.architecture, | 
|  | depends=FLAGS.depends, | 
|  | suggests=FLAGS.suggests, | 
|  | enhances=FLAGS.enhances, | 
|  | preDepends=FLAGS.pre_depends, | 
|  | recommends=FLAGS.recommends, | 
|  | homepage=FLAGS.homepage, | 
|  | builtUsing=GetFlagValue(FLAGS.built_using), | 
|  | priority=FLAGS.priority, | 
|  | conflicts=FLAGS.conflicts, | 
|  | installedSize=GetFlagValue(FLAGS.installed_size)) | 
|  | CreateChanges( | 
|  | output=FLAGS.changes, | 
|  | deb_file=FLAGS.output, | 
|  | architecture=FLAGS.architecture, | 
|  | short_description=GetFlagValue(FLAGS.description).split('\n')[0], | 
|  | maintainer=FLAGS.maintainer, package=FLAGS.package, | 
|  | version=GetFlagValue(FLAGS.version), section=FLAGS.section, | 
|  | priority=FLAGS.priority, distribution=FLAGS.distribution, | 
|  | urgency=FLAGS.urgency) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | MakeGflags() | 
|  | FLAGS = gflags.FLAGS | 
|  | main(FLAGS(sys.argv)) |