| # 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 hashlib |
| import os.path |
| from StringIO import StringIO |
| 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', True, False, 'all'), |
| ('Depends', False, True, []), |
| ('Recommends', False, True, []), |
| ('Suggests', False, True, []), |
| ('Enhances', False, True, []), |
| ('Pre-Depends', False, True, []), |
| ('Installed-Size', False, False), |
| ('Maintainer', True, False), |
| ('Description', True, True), |
| ('Homepage', False, False), |
| ('Built-Using', False, False, 'Bazel') |
| ] |
| |
| 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).') |
| |
| |
| 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=0644): |
| """Add a AR file entry to fileobj.""" |
| fileobj.write((filename + '/').ljust(16)) # filename (SysV) |
| fileobj.write(str(timestamp).ljust(12)) # timestamp |
| fileobj.write(str(owner_id).ljust(6)) # owner id |
| fileobj.write(str(group_id).ljust(6)) # group id |
| fileobj.write(oct(mode).ljust(8)) # mode |
| fileobj.write(str(len(content)).ljust(10)) # size |
| fileobj.write('\x60\x0a') # end of file entry |
| fileobj.write(content) |
| if len(content) % 2 != 0: |
| fileobj.write('\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 = StringIO() |
| with tarfile.open('control.tar.gz', mode='w:gz', fileobj=tar) as f: |
| tarinfo = tarfile.TarInfo('control') |
| tarinfo.size = len(controlfile) |
| f.addfile(tarinfo, fileobj=StringIO(controlfile)) |
| if extrafiles: |
| for name in extrafiles: |
| tarinfo = tarfile.TarInfo(name) |
| tarinfo.size = len(extrafiles[name]) |
| tarinfo.mode = 0755 |
| f.addfile(tarinfo, fileobj=StringIO(extrafiles[name])) |
| control = tar.getvalue() |
| tar.close() |
| return control |
| |
| |
| def CreateDeb(output, data, |
| preinst=None, postinst=None, prerm=None, postrm=None, **kwargs): |
| """Create a full debian package.""" |
| extrafiles = {} |
| if preinst: |
| extrafiles['preinst'] = preinst |
| if postinst: |
| extrafiles['postinst'] = postinst |
| if prerm: |
| extrafiles['prerm'] = prerm |
| if postrm: |
| extrafiles['postrm'] = postrm |
| control = CreateDebControl(extrafiles=extrafiles, **kwargs) |
| |
| # Write the final AR archive (the deb package) |
| with open(output, 'w') as f: |
| f.write('!<arch>\n') # Magic AR header |
| AddArFileEntry(f, 'debian-binary', '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, 'r') 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) 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, |
| timestamp=0, |
| distro='unstable', |
| urgency='medium'): |
| """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', distro), |
| ('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, distro, 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 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), |
| 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, |
| installedSize=GetFlagValue(FLAGS.installed_size)) |
| CreateChanges( |
| FLAGS.changes, |
| 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) |
| |
| if __name__ == '__main__': |
| MakeGflags() |
| FLAGS = gflags.FLAGS |
| main(FLAGS(sys.argv)) |