All pastes #722927 Raw Edit

Untitled

public python v1 · immutable
#722927 ·published 2007-10-02 12:57 UTC
rendered paste body
#!/usr/bin/python# -*- coding: utf8 -*-"""Vector Linux Package Builder UtilityThis script serves as a backend to the vpackager utility. You should neverhave to invoke this script manually. [But you can. Ed.]Builds a Vector Linux package from a provided source tarball. Works prettymuch like a slackbuild, only it's much more flexibleCredits:Vector Linux Team:M0E-lnxUelsk8sEasusterBluskhanumizzleTukaani Linux Team:LarhzuOriginal written by M0E-lnx; this version by hanumizzle.I'm not sure who else will want this script. Public domain is probably OKbecause I don't need a long legalese text."""import sysimport osimport shutilimport tempfileimport reimport errnoimport gzipimport timeimport textwrapimport socketfrom optparse import OptionParser__version__ = '0.8'__newline__ = '\n'# TODO:# # Add slack-build generator (not sure how)# Fix install.sh that produces faulty symlinks (perhaps)# Where to deposit package# Add info pages into install.sh (install-info)class PackageBuilder(object):  _build_profiles = {    'configure': {      'standard':        '--prefix=/usr \         --sysconfdir=/etc \         --bindir=/usr/bin \         --libdir=/usr/lib \         --localstatedir=/var \         --mandir=/usr/man \         --with-included-gettext' },    'distutils': {      'standard': 'build' } }           _build_identifiers = {    'configure': 'configure',    'distutils': 'setup.py' }    _default_config = {    'text': {         'build_profile': 'standard',      'custom_build_options': str(),      'pkg_arch': 'i586',       'pkg_release': '1',      'pkg_type': 'tlz',      'pkgr_id': 'vpackager',      'formatted_desc': False,      'execution_method': 'cli'},    'methods': ['build_cflags'] }  _socket_name = os.path.join(os.sep, 'tmp', 'vlpbuild-remote')    _package_dir = '_package'  _command_list = ['extract', 'detect', 'build', 'tweak', 'package']    _tweak_list = (    'slack_desc',    'binaries',    # usr_share tweak must come before man_pages    'usr_share',    'man_pages',    'info_pages',    'documentation',    'desktop_file',    'cruft_files')      _usr_share_to_usr = (    'doc',    'man',    'info')    _pkg_cruft_files = (    '^perllocal\.pod$',    '^ls-R$',    '^dir$')      # Lame solution  _top_level_doc_files = (    'AUTHORS',    'BUGS',    'COPYING',    'INSTALL',    'NEWS',    'README',    'TODO',    'FAQ',    'ChangeLog')  _doc_cruft_files = ['^Makefile']    def __init__(self, **parameters):    """Initializes a PackageBuilder object from 'parameters'.        This method establishes default configuration as necessary and creates a    secure temporary directory for packaging.    """        # self._config represents user parameters from the command line, except    # with a shorter name    self._config = parameters    # Set some default options    self._set_default_config()    # Create a private temporary directory for source compilation and    # package building    self._temp_dir = tempfile.mkdtemp()  def __del__(self):    """Deletes the temporary directory used to package the software."""        # Delete temporary directory    shutil.rmtree(self._temp_dir)    def _ensure_presence_of_directory(self, directory):    """Ensures presence of file system directory.        Creates directory path, identified by 'directory', with os.makedirs in a    try/except block. That mechanism catches OSError, and only propagates the    error upwards if its errno is not EEXIST. In other words, the method    attempts to create the directories and silences the error that arises    when those directories already exist.    """        try:      os.makedirs(directory)    except OSError, e:      if e.errno != errno.EEXIST:        raise e      def _cautious_system(self, command_line):    """Cautiously executes a command.        Executes 'command_line' with os.system and, in the event of a non-zero    return code, raises OSError with the command that failed.     """        if os.WEXITSTATUS(os.system(command_line)) != 0:      raise OSError, "command '%s' failed" % command_line        def _find_files(self, root, criteria):    """Look for files matching 'criteria' under 'root'.        'criteria' must be an iterable enumerating regular expressions or    callables that match basenames of desired files. If criterion is a    callable, _find_files uses it as a predicate, and passes the absolute    path to the file (to avoid directory changes for tests). If criterion    is a regular expression, _find_files matches the basename of the file    against the regular expression. That convenience behavior emulates    find path -name pattern, in effect.    """        found_files = []        for dir_path, dir_names, file_names in os.walk(root):      # Look for files in each directory under the package dir. Use a gay      # little trick to avoid clobbering the built-in type.       for teh_file in file_names:        absolute_path = os.path.join(dir_path, teh_file)                for criterion in criteria:          if callable(criterion):            if criterion(absolute_path):              found_files.append(absolute_path)          else:            if re.search(criterion, teh_file):              found_files.append(absolute_path)        return found_files    # Credit for the name '_compressify' goes to my hero, Jesus Bush.              def _compressify(self, original_file):    """Compresses 'original_file' using gzip compression.        The original file is removed after compression in accordance with the    gzip command.    """        uncompressed = open(original_file, 'rb')    compressed = gzip.GzipFile(original_file + '.gz', 'wb', 9)        # Thanks, crappy shutil    shutil.copyfileobj(uncompressed, compressed)        # Close both buffers and remove original file    uncompressed.close()    compressed.close()        os.remove(original_file)    def _set_default_config(self):    """Sets default parameters.         Uses defaults in _default_config where user has not supplied parameters.    These are divided into the categories 'text' and 'methods'. 'text'    defaults are simply copied into the _config hash if their key has no    value therein. 'methods' defaults are somewhat more complex; the method    locates a method '_get_default_%s' in the PackageBuilder class, where    '%s' is a unique identifier for the default. Such a method is    '_get_default_build_cflags'.    """        for k,v in self._default_config['text'].iteritems():      self._config.setdefault(k, v)        for i in self._default_config['methods']:           self._config.setdefault(i, getattr(self, '_get_default_' + i)())        def _get_default_build_cflags(self):    """Uses value of 'pkg_arch' in _config to determine default cflags.        Specifically, the default cflags are '-O2 -march=%s -mtune=i686', where    '%s' is the package architecture.    """        # Assume compilation for default architecture    pkg_arch = self._config['pkg_arch']    return '-O2 -march=%s -mtune=i686' % pkg_arch  def execute(self):    """Executes creation of package.        To begin, the current umask and working directory are stored, so as not    to interfere with the execution of the program following package    compilation. The process occurs within a try/finally block, so that    umask and working directory are restored even in the event of an    unhandled exception.        Packaging occurs in five stages:            - extraction of source      - detection of build system      - building and installation of source      - tweaking of installation      - compilation of package    """    try:      old_umask = os.umask(0022)      # Used in _package      self._old_wd = os.getcwd()      # Invoke specified execution method      getattr(self, '_execute_in_' + self._config['execution_method'])()    finally:      # Restore old umask and working directory      os.umask(old_umask)      os.chdir(self._old_wd)  def _execute_in_cli(self):    """Executes package creation in command line mode.        Very straightforward; building occurs without interruption or conditions.    Contrast with vpackager-style package creation.    """    for stage in self._command_list:      # Invoke stage method appropriately      getattr(self, '_' + stage)()  def _execute_in_vpackager(self):    """Executes package creation for vpackager.        vlpbuild opens a socket for vpackager, which then sends commands to it.    Such commands are validated against a list (to be safe, though I doubt    anyone with privileges to use the socket would send '_del__'), then    executed to allow vpackager to update its progress bar.    """    try:      receiver = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)      receiver.bind(self._socket_name)      receiver.listen(1)      connection, address = receiver.accept()      while True:        # Maximum of 1024 bytes for command name seems OK...I guess        method = connection.recv(1024)        if not method: break        # Make sure the command issued is on the list        if not method in self._command_list:          raise ValueError, 'method %s does not exist' % method        # Try to invoke the method; send back a message according to the        # success of the operation        try:          getattr(self, '_' + method)()          # It passed          connection.send('PASS')        except Exception, e:          # I suck, or some programmer sucks alternatively          connection.send('FAIL')          connection.close()          # Re-raise e          raise e            connection.close()    finally:      # Always clean up the socket, and ignore any errors in the event that      # vlpbuild could not create the same      try:        os.unlink(self._socket_name)      except OSError:        pass  def _extract(self):    """Extract source archive into temporary directory.        Also determines package name and version, and creates temporary    packaging root directory within the source directory.    """        # Change current directory to teh temp dir created for packager    os.chdir(self._temp_dir)    # Extract the source archive into the package build directory    self._cautious_system('tar xf %(source_archive)s' % self._config)    # Assuming the source archive extracts into a single directory, the    # element in os.listdir() is it:    source_dir = os.listdir(os.getcwd()).pop()    self._source_dir = os.path.join(self._temp_dir, source_dir)    # Finagle name and version of the applications from source_dir    self._finagle_name_and_version(source_dir)    # Set up _package directory within the source dir    self._package_dir = os.path.join(self._source_dir, '_package')    os.mkdir(self._package_dir)      def _finagle_name_and_version(self, source_dir):    """Determines application name and version from source directory."""        regex = re.compile('^([\w-]+)-([\d.-]+)')    self._app_name, self._app_version = regex.match(source_dir).groups()      def _detect(self):    """Detects build system according to unique files in source archive.        The unique files are enumerated in the class variable    _build_identifiers."""        for k,v in self._build_identifiers.iteritems():      if os.path.isfile(os.path.join(self._source_dir, v)):        self._config['build_system'] = k        break    def _build(self):    """Builds package according to its build system."""        # Change directory to source directory    os.chdir(self._source_dir)    # Retrieve user or default cflags    cflags = self._config['build_cflags']    # The build system: configure, SCons, Makefile.PL, et al.    system = self._config['build_system']    # Command line parameters for the build system from build profile    profile = self._config['build_profile']    profile = self._build_profiles[system][profile]    # Custom build parameters (dependent on system)    custom_options = self._config['custom_build_options']    # Internal build method for the system    method = getattr(self, '_build_for_' + system)    # Run build backend    method(cflags, profile, custom_options)    def _build_for_configure(self, cflags, profile, custom_options):    """Builds source with GNU autoconf."""        # Apply same settings for CFLAGS and CXXFLAGS (C++ compile flags) alike.    # (Too bad hardly anyone uses Objective C...)    os.putenv('CFLAGS', cflags)    os.putenv('CXXFLAGS', cflags)    command = './configure %s %s' % (profile, custom_options)    # Strip the ugly extraneous spaces out of the command line in the event    # of an error    command = ' '.join(i for i in command.split(' ') if i != str())        # Store build command for later use    self._build_command = command        # Run configure    self._cautious_system(self._build_command)    # Now run make    self._cautious_system('make')    # Install into the _package subdirectory    package_dir = self._package_dir    self._cautious_system('make install DESTDIR=%s' % package_dir)      def _build_for_distutils(self, cflags, profile, custom_options):    """Builds source with distutils."""        # CFLAGS perhaps        # Generate command    build_command = 'python setup.py %s %s' % (profile, custom_options)    # Store command for later use    self._build_command = build_command    # Run setup.py...    self._cautious_system(self._build_command)    # Install into the _package subdirectory    install_command = 'python setup.py install --root=%s' % self._package_dir    self._cautious_system(install_command)    # Add a penguinporker easter egg somewhere  def _tweak(self):    """Executes a battery of 'tweaks' on the installation before packaging.        These tweaks modify the binary installation in various ways to improve    performance or conform more strongly to Slackware and Vector standards    for software installation."""        for tweak in self._tweak_list:      getattr(self, '_tweak_' + tweak)()        def _tweak_slack_desc(self):    """Generate a full slack-desc, with header, description, and data.    The slack-desc may come from vpackager, in which case most of it will    already exist in good form. Only package statistics remain after that;    those are appended.    The slack-desc may also be a simple paragraph. In this case, a full    slack-desc shall be generated, neatly formatting the paragraph into the    whole.    """    if not self._config['formatted_desc']:      # When desc_file is currently a simple paragraph      self._generate_new_slack_desc()    else:      # desc_file came from vpackager      self._modify_old_slack_desc()  def _open_slack_desc(self):    """Opens install/slack-desc in package dir as file object."""    # Ensure existence of install/ directory in package and open    # slack-desc    install_dir = os.path.join(self._package_dir, 'install')    self._ensure_presence_of_directory(install_dir)    slack_desc = open(os.path.join(install_dir, 'slack-desc'), 'w')    return slack_desc  def _get_slack_desc_margin(self):    return self._app_name + ': '  def _generate_new_slack_desc(self):    """Format raw paragraph into complete slack_desc."""    # 'contents' holds contents of slack-desc prior to formatting and writing    # to disk. Specifically, it is a dictionary consisting of keys 'header',    # 'description', and 'data'. 'description' is optional, and is wrapped.    contents = {}    # Generate a header like 'foobar 5.6'    contents['header'] = '%s %s' % (self._app_name, self._app_version)          # Try to include user supplied description    try:      desc_file = open(self._config['desc_file'])      contents['description'] = [i.rstrip() for i in desc_file]      desc_file.close()    except KeyError:      pass      # Add some vital data to the slack-desc    contents['data'] = self._get_slack_desc_data()    # Write out the slack-desc    self._write_new_slack_desc(contents)      def _get_slack_desc_data(self):    """Appends automatically determined data to slack-desc."""        data = []        # Build date    format = '%a %b %e %H:%M:%S %Z %Y'    localtime = time.localtime()    data.append('BUILD_DATE: %s' % time.strftime(format, localtime))    # Packager ID    data.append('PACKAGER: %s' % self._config['pkgr_id'])    # Host (uname fields sysname, release, and machine)    data.append('HOST: %s' % ' '.join(os.uname()[::2]))    # Distro    vector_version = open('/etc/vector-version')    version_string = vector_version.readline()    vector_version.close()    data.append('DISTRO: %s' % version_string)    # Compilation flags    data.append('CFLAGS: %s' % self._config['build_cflags'])    # Build command    data.append('BUILD_COMMAND: %s' % self._build_command)        return data      def _write_new_slack_desc(self, contents):    """Writes out completely formatted slack-desc."""        # Ensure existence of install/ directory in package and open    # slack-desc    slack_desc = self._open_slack_desc()        # The margin that precedes every line in a slack-desc, the app name    # followed by a colon and space.    margin = self._get_slack_desc_margin()        # Write teh header    slack_desc.write(margin + contents['header'] + __newline__)    slack_desc.write(margin + __newline__)        # Write out formatted description, if available    try:      text = __newline__.join(contents['description'])      formatted_text = textwrap.wrap(text, width=78 - len(margin))            for line in formatted_text:        slack_desc.write(margin + line + __newline__)      slack_desc.write(margin + __newline__)    except KeyError:      pass       # Lastly, write out the generated data    for datum in contents['data']:      slack_desc.write(margin + datum + __newline__)    slack_desc.close()  def _modify_old_slack_desc(self):    """Frobs existing slack-desc a little (adds statistics)."""    # Make sure install/ exists and open slack-desc    slack_desc = self._open_slack_desc()        # Copy original slack-desc to the package slack-desc    desc_file = open(self._config['desc_file'])    shutil.copyfileobj(desc_file, slack_desc)    desc_file.close()    # Add statistics to the end of the install/slack-desc    data = self._get_slack_desc_data()    margin = self._get_slack_desc_margin()    slack_desc.write(margin + __newline__)    for datum in data:      slack_desc.write(margin + datum + __newline__)    slack_desc.close()  def _tweak_binaries(self):    """Strips unnecessary symbols from binaries in the package.        Because some people leave -g on for compilation of finished    products. grrr...    """        root = self._package_dir    criteria = [self._is_executable]        for binary_file in self._find_files(root, criteria):      # Hack introduced for absolute symlink in wxWidgets packaging, which      # *appears* broken.      if os.path.isfile(binary_file):        self._cautious_system('strip --strip-unneeded %s' % binary_file)    def _is_executable(self, teh_file):    """Determines whether a file is executable."""        # Replace with a file /object/    teh_file = open(teh_file, 'rb')    # Read first four bytes    magic_string = teh_file.read(4)    # Check the magic string against ELF constant    if magic_string == '\x7fELF':      return True    else:      return False    # Move /usr/share/doc and /usr/share/man contents into /usr/doc and  # /usr/man  def _tweak_usr_share(self):    """Moves certain files installed in /usr/share into /usr.        The measure exists to conform with Slackware file system     standards.    """        for i in self._usr_share_to_usr:      original_path = os.path.join(self._package_dir, 'usr', 'share', i)      new_path = os.path.join(self._package_dir, 'usr', i)            if os.path.isdir(original_path):        self._ensure_presence_of_directory(new_path)                  for i in os.listdir(original_path):          original_file = os.path.join(original_path, i)          new_file = os.path.join(new_path, i)          os.rename(original_file, new_file)                # Remove the original path        shutil.rmtree(original_path)  def _tweak_man_pages(self):    """Compresses uncompressed manual pages.        Too many packages don't fucking compress their man pages.    """        man_dir = os.path.join(self._package_dir, 'usr', 'man')        for man_page in self._find_files(man_dir, ['\.\d$']):      # Fix the symbolic links that would otherwise point to uncompressed      # man pages that will soon cease to be            if os.path.islink(man_page):        # A potential bottleneck emerges here, but tests will determine        # whether it is a real issue.        os.chdir(os.path.dirname(man_page))        basename = os.path.basename(man_page)                os.symlink(os.readlink(basename) + '.gz', basename + '.gz')      else:        # It's a a true man page, not a link; compress        self._compressify(man_page)          def _tweak_info_pages(self):    """As with _tweak_man_pages, compress info pages."""        info_dir = os.path.join(self._package_dir, 'usr', 'info')        for info_page in self._find_files(info_dir, ['\.info']):      self._compressify(info_page)    def _tweak_documentation(self):    """Weakly tries to ensure some basic documentation for package.        Copies top-level files in source directory, such as README and AUTHORS,    into usr/doc/app_name-app_version in packaging directory. These files    are enumerated in class variable _top_level_doc_files.    """        pkg_name = self._app_name + '-' + self._app_version    pkg_doc_dir = os.path.join(self._package_dir, 'usr', 'doc', pkg_name)    # Ensure presence of documentation directory    self._ensure_presence_of_directory(pkg_doc_dir)        # Copy certain top-level files into documentation directory    for doc_file in self._top_level_doc_files:      absolute_path = os.path.join(self._source_dir, doc_file)            if os.path.isfile(absolute_path):        shutil.copy(absolute_path, pkg_doc_dir)            # Try copying doc/ or docs/ from top-level of source directory into    # documentation directory of package        for i in ('doc', 'docs'):      source_doc_dir = os.path.join(self._source_dir, i)      if os.path.isdir(source_doc_dir):        shutil.copytree(source_doc_dir, os.path.join(pkg_doc_dir, 'docs'))        break          # Remove some cruft from the copy.     for cruft_file in self._find_files(pkg_doc_dir, self._doc_cruft_files):      os.remove(cruft_file)  def _tweak_desktop_file(self):    """Package stray .desktop files in the source directory.         Copy .desktop file(s) under the source directory into    usr/share/applications under the package directory; create the same    directory if necessary.    """        func = os.path.join    desktop_dir = func(self._package_dir, 'usr', 'share', 'applications')    desktop_files = self._find_files(self._source_dir, ['\.desktop$'])        for desktop_file in desktop_files:      # Ignore .desktop files present in package directory        function = os.path.commonprefix      common_prefix = function((self._package_dir, desktop_file))      if common_prefix == self._package_dir:        continue            # Otherwise, make sure /usr/share/applications exists and move the      # .desktop file into it.      self._ensure_presence_of_directory(desktop_dir)      shutil.move(desktop_file, desktop_dir)    # To wit, remove them  def _tweak_cruft_files(self):    """Remove 'cruft' files from the package.        The cruft file patterns are listed in _pkg_cruft_file and are    automatically generated directories whose presence is undesired in a    completed package.    """    root = self._package_dir    criteria = self._pkg_cruft_files        for cruft_file in self._find_files(root, criteria):      os.remove(cruft_file)        def _package(self):    """Compile a usable binary package.        Automatically generates a package name from the original source file, as    well as user parameters. It uses sane defaults where necessary. In    particular, the default compression method is LZMA.    """        # Change directory to package dir    os.chdir(self._package_dir)    # Run makeslapt to produce a tlz (default) or tgz package    pkg_type = self._config['pkg_type']    pkg_name = self._get_pkg_name()    self._cautious_system('/sbin/makeslapt --%s %s' % (pkg_type, pkg_name))    # Move package to original current directory    shutil.move(pkg_name, self._old_wd)    def _get_pkg_name(self):    """Return a package name.        Concatenates application name and version, inferred from source archive    name, with package architecture and release number to create the    basename, then adds extension according to compression type. The    resulting name may resemble:            foobar-2.6-i586-1.tlz    """        parts = []    parts.append(self._app_name)    parts.append(self._app_version)    parts.append(self._config['pkg_arch'])    parts.append(self._config['pkg_release'])        pkg_basename = '-'.join(parts)    pkg_type = self._config['pkg_type']    return pkg_basename + '.' + pkg_typedef main(args):  """Executes PackageBuilder instance via command-line."""    option_template = {    'desc_file': {'short': 'd'},    'build_system': {'short': 's'},    'build_profile': {'short': 'p'},    'custom_build_options': {'short': 'o'},    'pkg_arch': {'short': 'a'},    'pkg_release': {'short': 'r'},    'pkg_type': {'short': 't'},    'pkgr_id': {'short': 'i'},    'build_cflags': {'short': 'c'},    'execution_method': {'short': 'e' },    'formatted_desc': {'short': 'f', 'action': 'store_true'} }    usage = 'usage: %prog [options] source_archive'  version = '%%prog %s' % __version__  option_parser = OptionParser(usage=usage, version=version)    for k,v in option_template.iteritems():    short_option = '-' + v['short']    # Change hyphens to underscores; I worry not over the expense here    long_option = '--' + k.replace('_', '-')    # The action is 'store', by default    try:      action = v['action']    except KeyError:      action = 'store'        # Add the new option to the parser instance    option_parser.add_option(short_option, long_option, action=action, dest=k)    # Collect parameters from the command line  options, args = option_parser.parse_args(args)    # Parameters is a hash that represents both options and positional  # parameters  parameters = {}    # Add all options to the parameters hash that are not None; note the  # subtle difference between that test and a simple truth test.  for k in option_template.iterkeys():    option = getattr(options, k)        if option is not None:      parameters[k] = option    # The single positional parameter for now is the source archive.  try:    parameters['source_archive'] = args.pop()  except IndexError:    raise LookupError, 'source archive not given to script'      # Make sure paths to file parameters are absolute  try:    for k in ['source_archive', 'desc_file']:      parameters[k] = os.path.abspath(parameters[k])  except KeyError:    pass  # Create the package builder and execute package creation  package_builder = PackageBuilder(**parameters)  package_builder.execute()  if __name__ == '__main__':  main(sys.argv[1:])