Monday 5 March 2007

Eggifying the Python OpenSSL Package M2Crypto

This has been on a todo list for a long time. A priority NDG (NERC DataGrid) Security package is make it as easy to deploy as possible. For this, I'm creating Python Eggs for separate client and server packages (http://proj.badc.rl.ac.uk/ndg/wiki/TI12_Security/EggifyingNDGsecurity).

Third party packages are setup as dependencies some being easier to incorporate than others. For M2Crypto, a Python wrapper to OpenSSL, the one problem is getting setup to recognise alternative installation locations of openssl to link with.

There is already a an --openssl command line option to specify an alternate path but this is not integrated with the rest of distutils. In particular you can't specify it as a config file option and you also can't use include_dirs or library_dirs options either as alternative means of passing in the correct openssl paths. Using config file options will help greatly for setting up M2Crypto as an Egg dependency.

One straightforward way to solve this would seem to be to write your own build_ext class. Each command option set on the command line for setup has an equivalent class implemented for it in distutils. You can add new ones or override existing ones.


from distutils.command import build_ext

class _build_ext(build_ext.build_ext):
    '''Specialization of build_ext to enable swig_opts to 
    inherit any include_dirs settings made at the command 
    line or in a setup.cfg file'''

    user_options = build_ext.build_ext.user_options + \
    [('openssl=', 'o', 'Prefix for openssl installation location')]

    def initialize_options(self):
        '''Overload to enable custom openssl settings to 
        be picked up'''
        build_ext.build_ext.initialize_options(self)
        
        # openssl is the attribute corresponding to openssl 
        # directory prefix command line option
         if os.name == 'nt':
            self.libraries = ['ssleay32', 'libeay32']
            self.openssl = 'c:\\pkg'
        else:
            self.libraries = ['ssl', 'crypto']
            self.openssl = '/usr'
            
        # ...
    
    def finalize_options(self):
        '''Overloaded build_ext implementation to append 
        custom openssl include file and library linking 
        options'''
        build_ext.build_ext.finalize_options(self)

        opensslIncludeDir = os.path.join(self.openssl, 
                                         'include')
        opensslLibraryDir = os.path.join(self.openssl, 
                                         'lib')
        
        self.swig_opts = ['-I%s' % i \
            for i in self.include_dirs + [opensslIncludeDir]]
        
        self.include_dirs += [os.path.join(self.openssl, 
                                           opensslIncludeDir)]        
        self.library_dirs += [os.path.join(self.openssl, 
                                           opensslLibraryDir)]


The class variable user_options is extended to include the openssl option (line 08). When this option is parsed, distutils expects to store the setting in an attribute openssl. This is set up in the customised initialize_options method. The openssl option will be displayed the help message for build_ext and can be set on the command line with --openssl/-o or in a config file under build as openssl=...

initialize_options and finalize_options are the key methods which are customised to tweak the link settings for openssl. Making the settings here means any changes set on the command line or in a config file are correctly picked up and set before compilation.

setup picks up the custom _build_ext class by setting it the cmdclass keyword giving the command name and equivalent class in a dictionary:


setup(name = 'M2Crypto',
      # ...
      cmdclass={'build_ext': _build_ext})


Distutils customisation looks much more straightforward now but this is after lots of digging through documentation and code :)

No comments: