root/MGET/Trunk/PythonPackage/setup.py @ 61

Revision 61, 44.7 KB (checked in by jjr8, 6 years ago)

Fixed generation of HTML files for the Trac online documentation.

Line 
1#!/usr/bin/env python
2#
3# setup.py - GeoEco Python package build script
4#
5# Copyright (C) 2007 Jason J. Roberts
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License
9# as published by the Free Software Foundation; either version 2
10# of the License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License (available in the file LICENSE.txt)
16# for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21
22import distutils.core
23import glob
24import inspect
25import os
26import os.path
27import re
28import shutil
29import stat
30import subprocess
31import sys
32import time
33import types
34import xml.dom.minidom
35
36print(u'Python ' + sys.version)
37
38
39# Helper functions
40
41def AppendMetadataXMLForModule(moduleName, modulePath, node, document):
42    u"""Append the metadata for the moduleName module, located at modulePath, as child XML elements of node. If this is a package, recurse down subpackages and submodules."""
43
44    import GeoEco
45    import GeoEco.Metadata
46
47    # Import the module.
48
49    print(u'  ' + moduleName)   
50    __import__(moduleName, globals(), locals())
51
52    # Append the module's metadata as XML elements.
53
54    if isinstance(sys.modules[moduleName].__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(sys.modules[moduleName].__doc__.Obj, GeoEco.Metadata.ModuleMetadata):
55        sys.modules[moduleName].__doc__.Obj.AppendXMLNodes(node, document)
56    else:
57        GeoEco.Metadata.ModuleMetadata(moduleName).AppendXMLNodes(node, document)
58    submodulesNode = node.appendChild(document.createElement(u'Modules'))
59
60    # If this is a directory, then it is a Python package. Process the
61    # submodules and subpackages.
62
63    if os.path.isdir(modulePath):
64        paths = glob.glob(os.path.join(modulePath, u'*'))
65        for p in paths:
66            if os.path.isfile(p) and p.endswith(u'.py') and os.path.basename(p) != u'__init__.py':
67                submoduleName = moduleName + u'.' + os.path.basename(p)[:-3]
68            elif os.path.isdir(p) and p.lower() != u'.svn' and os.path.isfile(os.path.join(p, u'__init__.py')):
69                submoduleName = moduleName + u'.' + os.path.basename(p)
70            else:
71                continue
72            AppendMetadataXMLForModule(submoduleName, p, submodulesNode.appendChild(document.createElement(u'ModuleMetadata')), document)
73
74
75def WriteArcGISWrapperScriptForMethod(moduleName, method, scriptsDir):
76    u"""Write a Python wrapper script to scriptsDir for invoking method from an ArcGIS toolbox."""
77
78    import GeoEco.ArcGIS
79   
80    GeoEco.ArcGIS.ValidateMethodMetadataForExposureAsArcGISTool(moduleName, method.Class.Name, method.Name)
81    fileName = os.path.join(scriptsDir, u'%s.%s.py' % (method.Class.Name, method.Name))
82    print(u'  Writing \"%s\"' % fileName)
83    f = file(fileName, u'w')
84    f.write('import GeoEco.ArcGIS\n')
85    f.write('GeoEco.ArcGIS.ExecuteMethodFromCommandLineAsArcGISTool(\'%s\', \'%s\', \'%s\')\n' % (str(moduleName), str(method.Class.Name), str(method.Name)))
86    f.close()
87
88
89def WriteArcGISWrapperScriptsForMethodsInModule(moduleName, modulePath, scriptsDir):
90    u"""For all methods of all classes in the specified module, if the method is flagged for exposure as ArcGIS tool, write a Python wrapper script to scriptsDir for invoking the method from an ArcGIS toolbox. If the specified module is a package, recurse down subpackages and submodules."""
91
92    import GeoEco.Metadata
93
94    # Write the wrapper scripts for all of the methods in this module that are
95    # flagged for exposure as ArcGIS tools.
96
97    arcGISMethods = []   
98   
99    if isinstance(sys.modules[moduleName].__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(sys.modules[moduleName].__doc__.Obj, GeoEco.Metadata.ModuleMetadata):
100        for (className, cls) in sys.modules[moduleName].__dict__.items():
101            if inspect.isclass(cls) and isinstance(cls.__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(cls.__doc__.Obj, GeoEco.Metadata.ClassMetadata) and cls.__doc__.Obj.Module == sys.modules[moduleName].__doc__.Obj:
102                for (methodName, method) in inspect.getmembers(cls, inspect.ismethod):
103                    if isinstance(method.__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(method.__doc__.Obj, GeoEco.Metadata.MethodMetadata) and method.__doc__.Obj.IsExposedAsArcGISTool:
104                        WriteArcGISWrapperScriptForMethod(moduleName, method.__doc__.Obj, scriptsDir)
105                        arcGISMethods.append(method.__doc__.Obj)
106
107    # If this is a directory, then it is a Python package. Process the
108    # submodules and subpackages.
109
110    if os.path.isdir(modulePath):
111        paths = glob.glob(os.path.join(modulePath, u'*'))
112        for p in paths:
113            if os.path.isfile(p) and p.endswith(u'.py') and os.path.basename(p) != u'__init__.py':
114                submoduleName = moduleName + u'.' + os.path.basename(p)[:-3]
115            elif os.path.isdir(p) and p.lower() != u'.svn' and os.path.isfile(os.path.join(p, u'__init__.py')):
116                submoduleName = moduleName + u'.' + os.path.basename(p)
117            else:
118                continue
119            arcGISMethods.extend(WriteArcGISWrapperScriptsForMethodsInModule(submoduleName, p, scriptsDir))
120
121    # Return the list of methods that are flagged for exposure as ArcGIS tools.
122
123    return arcGISMethods   
124
125
126def GetCOMClassesInModule(moduleName, modulePath):
127    u"""Return all classes in the specified module that are flagged for exposure using Microsoft COM. If the specified module is a package, recurse down subpackages and submodules."""
128
129    import GeoEco.Metadata
130
131    # Add all classes from the specified module.
132
133    comClasses = []   
134   
135    if isinstance(sys.modules[moduleName].__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(sys.modules[moduleName].__doc__.Obj, GeoEco.Metadata.ModuleMetadata):
136        for (className, cls) in sys.modules[moduleName].__dict__.items():
137            if inspect.isclass(cls) and isinstance(cls.__doc__, GeoEco.Metadata.DynamicDocString) and isinstance(cls.__doc__.Obj, GeoEco.Metadata.ClassMetadata) and cls.__doc__.Obj.Module == sys.modules[moduleName].__doc__.Obj and cls.__doc__.Obj.IsExposedAsCOMServer:
138                comClasses.append(cls)
139
140    # If the specified module is a directory, then it is a Python package.
141    # Process the submodules and subpackages.
142
143    if os.path.isdir(modulePath):
144        paths = glob.glob(os.path.join(modulePath, u'*'))
145        for p in paths:
146            if os.path.isfile(p) and p.endswith(u'.py') and os.path.basename(p) != u'__init__.py':
147                submoduleName = moduleName + u'.' + os.path.basename(p)[:-3]
148            elif os.path.isdir(p) and p.lower() != u'.svn' and os.path.isfile(os.path.join(p, u'__init__.py')):
149                submoduleName = moduleName + u'.' + os.path.basename(p)
150            else:
151                continue
152            moreCOMClasses = GetCOMClassesInModule(submoduleName, p)
153            comClasses.extend(moreCOMClasses)
154
155    # Return a list of classes that are flagged for exposure using COM, so
156    # the caller does not have to enumerate them again.
157
158    return comClasses
159
160
161def RunXslTransform(setupDir, xmlInputFile, xslFile, outputFile):
162    u"""Transform xmlInputFile using xslFile into outputFile, using the GNOME xsltproc program."""
163
164    args = [os.path.join(setupDir, u'..', u'Bin', sys.platform.lower(), u'xsltproc'),
165            #u'-v',     # Uncomment this for verbose output from xsltproc
166            u'-o',
167            outputFile,
168            xslFile,
169            xmlInputFile]
170
171    if sys.platform.lower() == u'win32':
172        args[0] = args[0] + u'.exe'
173
174    try:
175        p = subprocess.Popen(args, stderr=subprocess.PIPE)
176    except:
177        print(u'Failed to invoke %s' % u' '.join(args))
178        raise
179    retcode = p.wait()
180
181    # Occasionally xsltproc seems to fail with transient but obscure messages
182    # such as:
183    #
184    #     error : No such file or directory
185    #     I/O error : No such file or directory
186    #     error : Unknown IO error
187    #
188    # If we receive an error on the STDERR pipe, try again just to make sure
189    # it is not a transient error.
190   
191    msg = p.stderr.read()
192    p.stderr.close()
193    if len(msg) > 0:
194        try:
195            p = subprocess.Popen(args)
196        except:
197            print(u'Failed to invoke %s' % u' '.join(args))
198            raise
199        retcode = p.wait()
200   
201    if retcode != 0:
202        print(u'A non-zero exit code was returned by %s' % u' '.join(args))
203        sys.exit(u'xsltproc failed and returned exit code %i.' % retcode)
204
205
206def CopyTree(src, dest):
207    u"""Copy the directory tree src to dest but omit any subversion directories (.SVN)."""
208
209    if os.path.basename(src).lower() == u'.svn':
210        return
211    if not os.path.isdir(src):
212        raise IOError(u'The src directory "%s" does not exist' % src)
213    if os.path.exists(dest):
214        raise IOError(u'The dest "%s" already exists' % dest)
215    os.mkdir(dest)
216    for name in os.listdir(src):
217        if os.path.isfile(os.path.join(src, name)):
218            shutil.copy2(os.path.join(src, name), os.path.join(dest, name))
219        elif os.path.isdir(os.path.join(src, name)) and name != u'.svn':
220            CopyTree(os.path.join(src, name), os.path.join(dest, name))
221
222
223# Entry point
224
225def main():
226    u"""Entry point."""
227
228    # Determine the path to the directory containing this file (setup.py) and
229    # log a startup message.
230
231    if os.path.isabs(__file__):
232        setupDir = os.path.dirname(__file__)
233    elif len(os.path.dirname(__file__)) <= 0:
234        setupDir = os.getcwd()
235    else:
236        setupDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
237
238    print(u'')
239    print(u'********************************************************************************')
240    print(u'* Initializing')
241    print(u'********************************************************************************')
242    print(u'')
243
244    print(u'GeoEco setup script (last modified %s)' % time.asctime(time.localtime(os.stat(os.path.join(setupDir, u'setup.py')).st_mtime)))
245
246    # Validate that we're building for a supported operating system, and import
247    # os-specific modules.
248
249    if sys.platform.lower() != u'win32':
250        sys.exit(u'Currently, GeoEco can only be built for 32-bit Microsoft Windows, although the GeoEco base modules were all written to be platform independent. The Windows-only constraint will be removed once the development team completes testing of GeoEco on non-Windows platforms.')
251
252    if sys.platform.lower() == u'win32':
253        major, minor, build, platform, text = sys.getwindowsversion()
254        if major < 5:
255            sys.exit(u'GeoEco cannot be built on Microsoft Windows versions prior to Windows 2000.')
256        if text is not None and len(text) > 0:
257            print(u'Building GeoEco on Microsoft Windows %i.%i build %i with %s' % (major, minor, build, text))
258        else:
259            print(u'Building GeoEco on Microsoft Windows %i.%i build %i' % (major, minor, build))
260
261        import pythoncom
262
263    # Make a temporary copy of the source code, so that changes can be made to
264    # it without affecting the original code. Changes include distutils
265    # compiling the .py files and some additional files being generated. We do
266    # not want these files ending up in the subversion database.
267
268    if not os.path.isdir(os.path.join(setupDir, u'build')):
269        os.makedirs(os.path.join(setupDir, u'build'))
270        print(u'Created directory %s.' % os.path.join(setupDir, u'build'))
271    srcDir = os.path.join(setupDir, u'build', u'src')
272    if os.path.isdir(srcDir):
273        print(u'Removing existing directory %s...' % srcDir)
274        for root, dirs, files in os.walk(srcDir):
275            for d in dirs:
276                os.chmod(os.path.join(root, d), stat.S_IWRITE)
277            for f in files:
278                os.chmod(os.path.join(root, f), stat.S_IWRITE)
279        shutil.rmtree(srcDir, False)
280    print(u'Copying directory %s to %s...' % (os.path.join(setupDir, u'src'), srcDir))
281    CopyTree(os.path.join(setupDir, u'src'), srcDir)
282
283    # Add the temporary source code directory to the front of the Python import
284    # path, so we can import GeoEco modules from this directory.
285
286    sys.path.insert(0, srcDir)
287
288    # Define parameters that we will ultimately pass to distutils.core.setup.
289    # Various parts of this script modify these parameters.
290
291    packages = ['GeoEco', 'GeoEco.DataManagement', 'GeoEco.Test', 'GeoEco.Tools']       # TODO: Generate this list automatically
292    packageData = {'GeoEco': [os.path.join(u'Configuration', u'*')]}
293    scripts = []
294
295    # Append the LICENSE.txt file to the list of package data.
296
297    shutil.copy2(os.path.join(setupDir, u'..', u'LICENSE.txt'), os.path.join(srcDir, u'GeoEco', u'LICENSE.txt'))
298    packageData[u'GeoEco'].append(u'LICENSE.txt')
299
300    # Append platform-specific binaries to the list of package data.
301
302    if sys.platform.lower() == u'win32':
303        packageData[u'GeoEco'].append(os.path.join(u'Bin', u'win32', u'*.*'))
304
305    # C / C++ extension modules.
306    #
307    # TODO: make this dynamic, so we don't have to edit the list every time we
308    # add an extension module.
309
310    extensionModules=[distutils.core.Extension('GeoEco.DataManagement.BinaryRasterUtils', sources=['src/GeoEco/DataManagement/BinaryRasterUtils.cpp']),
311                      distutils.core.Extension('GeoEco.MetadataUtils', sources=['src/GeoEco/MetadataUtils.cpp'])]
312
313    # Write the metadata for all GeoEco modules to Metadata.xml. This will serve as
314    # input to subsequent build operations.
315
316    print(u'')
317    print(u'********************************************************************************')
318    print(u'* Generating Metadata.xml')
319    print(u'********************************************************************************')
320    print(u'')
321
322    import GeoEco
323    import GeoEco.Metadata
324
325    print(u'Generating XML metadata for Python modules:')
326    dom = xml.dom.minidom.getDOMImplementation()
327    document = dom.createDocument(None, u'ModuleMetadata', None)
328    document.documentElement.setAttribute(u'xmlns:xsi', u'http://www.w3.org/2001/XMLSchema-instance')
329    AppendMetadataXMLForModule(u'GeoEco', sys.modules[u'GeoEco'].__path__[0], document.documentElement, document)
330
331    print(u'Writing %s...' % os.path.join(srcDir, u'Metadata.xml'))
332    f = file(os.path.join(srcDir, u'Metadata.xml'), 'wb')
333    document.writexml(f)
334    f.close()
335
336    # If running on Windows, generate the ArcGIS toolbox.
337
338    if sys.platform.lower() == u'win32':
339        print(u'')
340        print(u'********************************************************************************')
341        print(u'* Generating the ArcGIS toolbox')
342        print(u'********************************************************************************')
343        print(u'')   
344
345        # Create the directories that will hold the ArcGIS-related files.
346       
347        arcGISDir = os.path.join(srcDir, u'GeoEco', u'ArcGISToolbox')
348        os.makedirs(os.path.join(arcGISDir, u'Scripts'))
349        print(u'Created %s.' % os.path.join(arcGISDir, u'Scripts'))
350
351        # Create the ArcGIS wrapper scripts.
352
353        import GeoEco.ArcGIS   
354
355        print(u'Writing ArcGIS wrapper scripts:')
356        arcGISMethods = WriteArcGISWrapperScriptsForMethodsInModule(u'GeoEco', sys.modules[u'GeoEco'].__path__[0], os.path.join(arcGISDir, u'Scripts'))
357
358        # TODO: validate that the same tool name is not exposed twice (Trac ticket #29)
359
360        # Create the ArcGIS toolbox.
361
362        args = [os.path.join(setupDir, u'..', u'VisualStudioSolutions', u'CreateArcGISToolbox', u'bin', u'Release', u'CreateArcGISToolbox.exe'),
363                os.path.join(srcDir, u'Metadata.xml'),
364                os.path.join(arcGISDir, u'Marine Geospatial Ecology Tools'),
365                u'GeoEco',
366                u'Scripts',
367                os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference')]
368       
369        print(u'Invoking %s' % u' '.join(args))
370        p = subprocess.Popen(args)
371        retcode = p.wait()
372        if retcode != 0:
373            sys.exit(u'CreateArcGISToolBox failed and returned exit code %i.' % retcode)
374
375        # Copy in binaries written by GeoEco developers.
376
377        shutil.copyfile(os.path.join(setupDir, u'..', u'VisualStudioSolutions', u'RegisterToolboxWithArcCatalog', u'bin', u'Release', u'RegisterToolboxWithArcCatalog.exe'), os.path.join(arcGISDir, u'Bin', u'RegisterToolboxWithArcCatalog.exe'))
378
379        # Ensure these files are provided to distutils.
380
381        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'*.*'))
382        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Bin', u'*.*'))
383        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Rasters', u'*.*'))
384        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Rasters', u'*', u'*.*'))
385        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Scripts', u'*.*'))
386
387    # If running on Windows, generate the COM type library for classes that can
388    # be invoked through COM.
389
390    if sys.platform.lower() == u'win32':
391        print(u'')
392        print(u'********************************************************************************')
393        print(u'* Generating the Microsoft COM type library')
394        print(u'********************************************************************************')
395        print(u'')
396
397        # Open the IDL file for writing.
398
399        comDir = os.path.join(srcDir, u'GeoEco', u'COM')
400        idlFilePath = os.path.join(comDir, u'GeoEco.idl')
401        print(u'Opening %s for writing.' % idlFilePath)
402        idlFile = file(idlFilePath, u'w')
403        idlFile.write('// IDL for GeoEco Type Library\n')
404        idlFile.write('//\n')
405        idlFile.write('// GeoEco is a Python package that provides geospatial ecology tools.\n')
406        idlFile.write('// Many of these tools are exposed as COM classes. Each class has a\n')
407        idlFile.write('// ProgID beginning with "GeoEco." and exposes a dual interface.\n')
408        idlFile.write('// Late-binding clients such as script engines can invoke the classes\n')
409        idlFile.write('// through the IDispatch interface, while early-binding clients such\n')
410        idlFile.write('// as C# can import the GeoEco Type Library and invoke the classes\n')
411        idlFile.write('// through the more-efficient vtable interface.\n')
412        idlFile.write('//\n')
413        idlFile.write('// This IDL file defines all of the interfaces and classes exported by\n')
414        idlFile.write('// the GeoEco type library.\n')
415        idlFile.write('\n')
416        idlFile.write('import "oaidl.idl";\n')
417        idlFile.write('import "ocidl.idl";\n')
418        idlFile.write('\n')
419
420        # Obtain a list of all of the classes flagged for exposure through COM.
421        # Validate the classes' metadata. Also validate that each
422        # class has a unique name across the entire set of classes, regardless
423        # of which module it appears in. This means developers cannot
424        # disambiguate classes with the same names using the Python module
425        # hierarchy. The benefit of this restriction is that we don't have to
426        # generate multiple type libraries, or a single type library that
427        # prepends the full list of module names to the class name.
428
429        print(u'Validating classes flagged for exposure by COM:')
430
431        import pythoncom
432        import GeoEco.COM
433
434        classDict = {}
435        guidDict = {}
436
437        comClasses = GetCOMClassesInModule(u'GeoEco', sys.modules[u'GeoEco'].__path__[0])
438        for cls in comClasses:
439            GeoEco.COM.ValidateClassMetadata(cls)
440            classMetadata = cls.__doc__.Obj
441           
442            if classDict.has_key(classMetadata.Name):
443                sys.exit(u'Two classes flagged for exposure by COM have the same class name: %s.%s and %s.%s. To be exposed by COM, each class must have a unique name, regardless of what module it appears in. Please change the name of one of these classes.' % (classMetadata.Module.Name, classMetadata.Name, classDict[classMetadata.Name].Module.Name, classDict[classMetadata.Name].Name))
444            classDict[classMetadata.Name] = classMetadata
445           
446            if guidDict.has_key(classMetadata.COMIID):
447                sys.exit(u'Two classes flagged for exposure by COM are using the same GUID %s: %s.%s is using it for COMIID and %s.%s is using it for %s. To be exposed by COM, each class must have a unique GUID for COMIID and COMCLSID. Here is a new unique GUID you can use: %s' % (classMetadata.COMIID, classMetadata.Module.Name, classMetadata.Name, guidDict[classMetadata.COMIID][1].Module.Name, guidDict[classMetadata.COMIID][1].Name, guidDict[classMetadata.COMIID][0], repr(unicode(pythoncom.CreateGuid()))))
448            guidDict[classMetadata.COMIID] = (u'COMIID', classMetadata)
449           
450            if guidDict.has_key(classMetadata.COMCLSID):
451                sys.exit(u'Two classes flagged for exposure by COM are using the same GUID %s: %s.%s is using it for COMCLSID and %s.%s is using it for %s. To be exposed by COM, each class must have a unique GUID for COMIID and COMCLSID. Here is a new unique GUID you can use: %s' % (classMetadata.COMIID, classMetadata.Module.Name, classMetadata.Name, guidDict[classMetadata.COMIID][1].Module.Name, guidDict[classMetadata.COMIID][1].Name, guidDict[classMetadata.COMIID][0], repr(unicode(pythoncom.CreateGuid()))))
452            guidDict[classMetadata.COMIID] = (u'COMCLSID', classMetadata)
453           
454            print('  %s.%s as ProgID %s' % (cls.__doc__.Obj.Module.Name, cls.__doc__.Obj.Name, cls.__doc__.Obj.COMVersionIndependentProgID))
455
456        # Write the interface definitions to the IDL file.
457
458        print(u'Writing interface definitions to %s.' % idlFilePath)
459        for cls in comClasses:
460            idlFile.write(GeoEco.COM.GetIDLInterfaceDefinitionFromMetadata(cls.__doc__.Obj))
461            idlFile.write('\n')
462
463        # Write type library header to the IDL file.
464
465        idlFile.write('[\n')
466        idlFile.write('    uuid(%s),\n' % GeoEco.COM.TypeLibraryGUID[1:-1])
467        idlFile.write('    version(%i.%i),\n' % (GeoEco.COM.TypeLibraryVersion[0], GeoEco.COM.TypeLibraryVersion[1]))
468        idlFile.write('    helpstring("GeoEco %i.%i Type Library")\n' % (GeoEco.COM.TypeLibraryVersion[0], GeoEco.COM.TypeLibraryVersion[1]))   
469        idlFile.write(']\n')
470        idlFile.write('library GeoEco\n')
471        idlFile.write('{\n')
472        idlFile.write('    importlib("stdole32.tlb");\n')
473        idlFile.write('    importlib("stdole2.tlb");\n')
474
475        # Write the coclass definitions to the IDL file.
476
477        for cls in comClasses:
478            idlFile.write('\n')
479            idlFile.write('    [\n')
480            idlFile.write('        uuid(%s),\n' % cls.__doc__.Obj.COMCLSID[1:-1])
481            idlFile.write('        helpstring("%s Class")\n' % cls.__doc__.Obj.Name)
482            idlFile.write('    ]\n')
483            idlFile.write('    coclass %s\n' % cls.__doc__.Obj.Name)
484            idlFile.write('    {\n')
485            idlFile.write('        [default] interface I%s;\n' % cls.__doc__.Obj.Name)
486            idlFile.write('    };\n')
487
488        # Close the IDL file.       
489
490        idlFile.write('};\n')
491        idlFile.close()
492
493        # Execute the MIDL compiler on the IDL file to produce the TLB file.
494
495        tlbFilePath = os.path.splitext(idlFilePath)[0] + u'.tlb'       
496
497        args = [u'midl.exe', u'/out', os.path.dirname(idlFilePath), u'/tlb', tlbFilePath, u'/win32', idlFilePath]
498        print('Invoking %s' % ' '.join(args))
499        try:
500            p = subprocess.Popen(args)
501        except WindowsError, e:
502            if e.errno == 2:
503                print(e.__class__.__name__ + u': ' + unicode(e))
504                sys.exit(u'The most common cause for failure here is forgetting to register the Microsoft Visual Studio environment variables. This is usually best accomplished by running "C:\\Program Files\\Microsoft Visual Studio 8\\Common7\\Tools\\vsvars32.bat" prior to executing setup.py.')
505            else:
506                raise
507        retcode = p.wait()
508        if retcode != 0:
509            sys.exit(u'midl.exe failed and returned exit code %i.' % retcode)
510        for ext in [u'_i.c', u'_p.c', u'.h']:
511            if os.path.exists(os.path.splitext(idlFilePath)[0] + ext):
512                os.remove(os.path.splitext(idlFilePath)[0] + ext)
513        if os.path.exists(os.path.join(os.path.dirname(idlFilePath), u'dlldata.c')):
514            os.remove(os.path.join(os.path.dirname(idlFilePath), u'dlldata.c'))
515
516        packageData[u'GeoEco'].append(os.path.join(u'COM', u'*'))
517
518    # If running on Windows, generate the postinstall script.
519
520    if sys.platform.lower() == u'win32':
521        print(u'')
522        print(u'********************************************************************************')
523        print(u'* Generating the Windows postinstall script')
524        print(u'********************************************************************************')
525        print(u'')
526
527        # Generate the list of classes that need COM registration.
528       
529        classesForCOMRegistration = '[' + ', '.join(map(lambda cls: '[' + repr(str(cls.__doc__.Obj.Module.Name)) + ', ' + repr(str(cls.__doc__.Obj.Name)) + ']', comClasses)) + ']'
530
531        # Write the install script.
532
533        postInstallFilePath = os.path.join(srcDir, u'GeoEcoPostInstall.py')
534        print(u'Opening %s for writing.' % postInstallFilePath)
535        f = file(postInstallFilePath, u'w')
536
537        f.write("""# GeoEco package post-install script.
538#
539# Do not delete this file! It runs during uninstallation of the GeoEco package.
540# It will be deleted automatically as part of uninstallation of GeoEco.
541
542import os
543import os.path
544import shutil
545import stat
546import sys
547import subprocess
548
549import GeoEco
550geoEcoRoot = os.path.dirname(sys.modules['GeoEco'].__file__)
551
552###############################################################################
553#  INPUT DATA
554###############################################################################
555
556classesForCOMRegistration = """ + classesForCOMRegistration + """
557
558###############################################################################
559#  HELPER FUNCTIONS
560###############################################################################
561   
562def ChmodRecursive(root, mode):
563    if os.path.isfile(root):
564        os.chmod(root, mode)
565    elif os.path.isdir(root):
566        os.chmod(root, mode)
567        for (root, dirs, files) in os.walk(root):
568            for name in files:
569                os.chmod(os.path.join(root, name), mode)
570            for name in dirs:
571                os.chmod(os.path.join(root, name), mode)
572
573def RegisterToolboxWithArcCatalog(action):
574    assert action == 'register' or action == 'unregister'
575
576    # Set/remove the read-only flag on the ArcGIS toolbox directory. This is
577    # recommended by the ArcGIS documentation, so that users do not accidentally
578    # tamper with the toolbox.
579
580    if action == 'register':
581        ChmodRecursive(os.path.join(geoEcoRoot, 'ArcGISToolbox'), stat.S_IREAD)
582    else:
583        ChmodRecursive(os.path.join(geoEcoRoot, 'ArcGISToolbox'), stat.S_IWRITE)
584
585    # Import pywin32 modules that we need.
586   
587    try:
588        import pywintypes
589        import win32api
590        import win32con
591        import win32file
592        import win32pipe
593        import win32process
594    except:
595        sys.exit('ERROR: Python for Windows Extensions (pywin32) is not installed. GeoEco requires this Python package. Please uninstall GeoEco, download pywin32 for your version of Python from http://sourceforge.net/projects/pywin32, install it, and install GeoEco again.')
596
597    # Ensure the ArcGIS is installed by reading the InstallDir from the registry.
598
599    try:
600        hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0')
601        try:
602            (arcGISInstallDir, installDirType) = win32api.RegQueryValueEx(hkey, 'InstallDir')
603        finally:
604            try:
605                win32api.RegCloseKey(hkey)
606            except:
607                pass
608        if not isinstance(arcGISInstallDir, basestring) or not os.path.isdir(arcGISInstallDir):
609            raise ValueError('The directory specified by the InstallDir value of the HKEY_LOCAL_MACHINE\\SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0 registry key does not exist.')
610        if not os.path.isfile(os.path.join(arcGISInstallDir, 'Bin', 'ArcCatalog.exe')):
611            raise ValueError('The ArcGIS file "%s" does not exist.', os.path.join(arcGISInstallDir, 'Bin', 'ArcCatalog.exe'))
612    except Exception, e:
613        if action == 'register':
614            print('WARNING: ArcGIS does not appear to be installed. If you install ArcGIS in the future and want to invoke GeoEco tools from geoprocessing models, you can manually add the toolbox from this location:')
615            print('')
616            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
617        return False
618
619    # Run RegisterToolboxWithArcCatalog.exe.
620
621    args = [os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Bin', 'RegisterToolboxWithArcCatalog.exe'),
622            action,
623            os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx')]
624   
625    try:       
626        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True)
627    except Exception, e:
628        if action == 'register':
629            print('WARNING: Failed to add the ArcGIS toolbox to ArcCatalog. When attempting to start the toolbox registration program "%s register \\'%s\\'", Python reported an error: %s' % (os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Bin', 'RegisterToolboxWithArcCatalog.exe'), os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'), str(e).strip()))
630            print('')
631            print('You can manually add the toolbox to ArcCatalog from this location:')
632            print('')
633            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
634        return False
635
636    # Wait for RegisterToolboxWithArcCatalog.exe to complete and
637    # capture any error message.
638
639    retcode = p.wait()
640    if retcode != 0:
641        if action == 'register':
642            print('WARNING: Failed to add the ArcGIS toolbox to ArcCatalog. The toolbox registration program "%s register \\'%s\\'" reported: %s' % (os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Bin', 'RegisterToolboxWithArcCatalog.exe'), os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'), p.stderr.read().strip()))
643            print('')
644            print('You can manually add the toolbox to ArcCatalog from this location:')
645            print('')
646            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
647        return False
648
649    # Return successfully.
650   
651    return True
652
653def RegisterClassesAsCOMServers():
654
655    # Import pywin32 modules that we need.
656   
657    try:
658        import pythoncom
659        import win32com.server.register
660    except:
661        sys.exit('ERROR: Python for Windows Extensions (pywin32) is not installed. GeoEco requires this Python package. Please uninstall GeoEco, download pywin32 for your version of Python from http://sourceforge.net/projects/pywin32, install it, and install GeoEco again.')
662
663    # Register the classes.
664
665    success = True
666    import GeoEco.COM
667    for [moduleName, className] in classesForCOMRegistration:
668        if not GeoEco.COM.RegisterCOMServerUsingClassMetadata(moduleName, className):
669            success = False
670
671    # Register the type library.
672
673    tlbFile = os.path.join(os.path.dirname(sys.modules['GeoEco.COM'].__file__), 'COM', 'GeoEco.tlb')   
674    try:
675        tli = pythoncom.LoadTypeLib(tlbFile)
676    except Exception, e:
677        print('WARNING: Failed to load the GeoEco COM type library. Since the library could not be loaded, it will not be registered with COM, and you will not be able to call GeoEco objects from early-bound programming languages such as C#. You can try registration again by uninstalling and reinstalling GeoEco. The pythoncom.LoadTypeLib(\\'%s\\') function reported: %s: %s' % (tlbFile, e.__class__.__name__, str(e)))
678        success = False
679
680    if success:       
681        try:
682            pythoncom.RegisterTypeLib(tli, tlbFile)
683        except Exception, e:
684            print('WARNING: Failed to register the GeoEco COM type library. You will not be able to call GeoEco objects from early-bound programming languages such as C#. You can try registration again by uninstalling and reinstalling GeoEco. The pythoncom.RegisterTypeLib(tli, \\'%s\\') function reported: %s: %s' % (tlbFile, e.__class__.__name__, str(e)))
685            success = False
686   
687    return success
688
689def UnregisterClassesAsCOMServers():
690
691    # Import pywin32 modules that we need.
692   
693    try:
694        import pythoncom
695        import win32com.server.register
696    except:
697        return
698
699    # Unregister the classes.
700
701    import GeoEco.COM
702    for [moduleName, className] in classesForCOMRegistration:
703        GeoEco.COM.UnregisterCOMServerUsingClassMetadata(moduleName, className)
704
705    # Unregister the type library:
706
707    try:
708        pythoncom.UnRegisterTypeLib('""" + GeoEco.COM.TypeLibraryGUID + '\', ' + str(GeoEco.COM.TypeLibraryVersion[0]) + ', ' + str(GeoEco.COM.TypeLibraryVersion[1]) + ', ' + str(GeoEco.COM.TypeLibraryLCID) + """, pythoncom.SYS_WIN32)
709    except:
710        pass
711
712def CreateShortcuts():
713
714    try:
715
716        # Delete GeoEco's directory in the All Users' Programs menu, if it exists.
717       
718        programsDir = get_special_folder_path("CSIDL_COMMON_PROGRAMS")
719        geoEcoProgramsDir = os.path.join(programsDir, 'Marine Geospatial Ecology Tools')
720        if os.path.exists(geoEcoProgramsDir):
721            shutil.rmtree(geoEcoProgramsDir)
722
723        # Create GeoEco's directory in the All Users' Programs menu.
724
725        os.mkdir(geoEcoProgramsDir)
726        directory_created(geoEcoProgramsDir)
727
728        # Create shortcuts for the documentation.       
729
730        docDir = os.path.join(geoEcoProgramsDir, 'Documentation')
731        os.mkdir(docDir)
732        directory_created(docDir)
733        create_shortcut(os.path.join(geoEcoRoot, 'Documentation', 'GettingStarted.html'), 'Getting Started With Marine Geospatial Ecology Tools', os.path.join(geoEcoProgramsDir, 'Documentation', 'Getting Started.lnk'))
734        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'Getting Started.lnk'))
735        create_shortcut(os.path.join(geoEcoRoot, 'Documentation', 'ArcGISReference', 'ArcGISReference.html'), 'Marine Geospatial Ecology Tools ArcGIS Geoprocessing Reference', os.path.join(geoEcoProgramsDir, 'Documentation', 'ArcGIS Geoprocessing Reference.lnk'))
736        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'ArcGIS Geoprocessing Reference.lnk'))
737        create_shortcut(os.path.join(geoEcoRoot, 'Documentation', 'PythonReference', 'PythonReference.html'), 'GeoEco Python Reference', os.path.join(geoEcoProgramsDir, 'Documentation', 'GeoEco Python Reference.lnk'))
738        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'GeoEco Python Reference.lnk'))
739        create_shortcut(os.path.join(geoEcoRoot, 'LICENSE.txt'), 'Marine Geospatial Ecology Tools Software License', os.path.join(geoEcoProgramsDir, 'Documentation', 'Software License.lnk'))
740        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'Software License.lnk'))
741
742    except Exception, e:
743        print('WARNING: Failed to create shortcuts in the Windows "Programs" menu.')
744        return False
745   
746    return True   
747
748
749###############################################################################
750#  MAIN SCRIPT
751###############################################################################
752
753# If argv[1] is '-install' run the installation script.
754
755if len(sys.argv) >= 2 and sys.argv[1].lower() == '-install':
756    shortcutsCreated = CreateShortcuts()
757    arcGISToolboxInstalled = RegisterToolboxWithArcCatalog('register')
758    allCOMClassesRegistered = RegisterClassesAsCOMServers()
759
760    if shortcutsCreated and arcGISToolboxInstalled and allCOMClassesRegistered:
761        print('All installation tasks completed successfully.')
762    else:
763        print('')
764        print('Some installation tasks failed. Please review the error messages above.')
765
766# If argv[1] is '-remove' run the uninstallation script. Hide any errors because
767# they are not critical.
768
769elif len(sys.argv) >= 2 and sys.argv[1].lower() == '-remove':
770    RegisterToolboxWithArcCatalog('unregister')
771    UnregisterClassesAsCOMServers()
772
773# If argv[1] is absent or unrecognized, report an error.
774
775else:
776    print('ERROR: Invalid command line arguments.')
777    print('')
778    print('USAGE: GeoEcoPostInstall.py {-install|-remove}')
779""")
780        f.close()
781        scripts.append(postInstallFilePath)
782
783    # Generate the documentation.
784
785    print(u'')
786    print(u'********************************************************************************')
787    print(u'* Generating documentation')
788    print(u'********************************************************************************')
789    print(u'')
790
791    docFiles = [u'LICENSE.txt', os.path.join(u'Documentation', u'GettingStarted.html'), os.path.join(u'Documentation', u'*.css')]
792
793    # Generate the Python reference documentation.
794
795    print('Generating Python reference documentation...')
796    RunXslTransform(setupDir, os.path.join(srcDir, u'Metadata.xml'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'PythonReference', u'PythonReference.xsl'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'PythonReference', u'PythonReference.html'))
797    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.css'))
798    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.png'))
799    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.html'))
800
801    # If running on Windows, generate ArcGIS reference documentation.
802
803    if sys.platform.lower() == u'win32':
804        print('Generating ArcGIS reference documentation...')
805
806        # Ensure that we include the other files that are referenced by the HTML
807        # files that make up the ArcGIS documentation.
808
809        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'*.css'))
810        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'*.gif'))
811
812        # Generate ArcGISReference.html.
813
814        RunXslTransform(setupDir, os.path.join(srcDir, u'Metadata.xml'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', u'ArcGISReference.xsl'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', u'ArcGISReference.html'))
815        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'ArcGISReference.html'))
816
817        # Get the path to the ArcGIS installation directory on this machine (the
818        # build machine), so we can locate an XSL stylesheet that comes with
819        # ArcGIS. We will use a modified version of this stylesheet to transform
820        # the XML metadata for each tool into HTML.
821
822        import win32api, win32con
823        hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0')
824        (arcGISInstallDir, installDirType) = win32api.RegQueryValueEx(hkey, 'InstallDir')
825        if not isinstance(arcGISInstallDir, basestring) or not os.path.isdir(arcGISInstallDir):
826            raise ValueError('The directory specified by the InstallDir value of the HKEY_LOCAL_MACHINE\\SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0 registry key does not exist.')
827
828        # Make some necessary changes ot the ArcGIS stylesheet using our own
829        # XSL transform.
830
831        RunXslTransform(setupDir, os.path.join(arcGISInstallDir, u'ArcToolbox', u'Stylesheets', u'geoprocessing_help.xsl'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', u'Tweak_geoprocessing_help.xsl'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', u'GeoEco_geoprocessing_help.xsl'))
832
833        # Generate the HTML file for each tool.
834
835        for method in arcGISMethods:       
836
837            # Transform the ESRI metadata XML into HTML, using the XSL
838            # stylesheet that comes with ArcGIS.
839
840            RunXslTransform(setupDir, os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', method.Class.Name + u'.' + method.Name + u'.xml'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', u'GeoEco_geoprocessing_help.xsl'), os.path.join(srcDir, u'GeoEco', u'Documentation', u'ArcGISReference', method.Class.Name + u'.' + method.Name + u'.html'))
841            docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', method.Class.Name + u'.' + method.Name + u'.html'))
842
843    packageData[u'GeoEco'].extend(docFiles)
844
845    # Run distutils setup.
846
847    print(u'')
848    print(u'********************************************************************************')
849    print(u'* Building/installing the python package')
850    print(u'********************************************************************************')
851    print(u'')
852
853    distutils.core.setup(name='GeoEco',
854                         version=str(GeoEco.__version__),
855                         description='Geospatial Ecology Tools',
856                         author='Jason Roberts',
857                         author_email='jason.roberts@duke.edu',
858                         url='http://code.env.duke.edu/projects/mget',
859                         package_dir={u'': srcDir},
860                         packages=packages,
861                         package_data=packageData,
862                         scripts=scripts,
863                         ext_modules=extensionModules)
864
865    # If running on Windows and Python 2.5, generate the Trac online
866    # documentation.
867
868    if sys.platform.lower() == u'win32' and sys.version_info[0] == 2 and sys.version_info[1] == 5:
869
870        print(u'')
871        print(u'********************************************************************************')
872        print(u'* Generating Trac online documentation...')
873        print(u'********************************************************************************')
874        print(u'')
875
876        # First run AdjustHtmlUrlsForTracWiki.xsl on all HTML files, so that the
877        # URLs within the files will resolve properly.
878
879        print(u'Running AdjustHtmlUrlsForTracWiki.xsl on HTML documentation files...')
880
881        for df in docFiles:
882            files = glob.glob(os.path.join(srcDir, u'GeoEco', df))
883            for src in files:
884                dest = src + '.tmp'
885                if src.endswith('.html') or src.endswith('.htm'):
886                    RunXslTransform(setupDir, src, os.path.join(srcDir, u'GeoEco', u'Documentation', u'AdjustHtmlUrlsForTracWiki.xsl'), dest)
887                else:
888                    shutil.copy2(src, dest)
889
890        # Now move the updated files to the dist directory.
891
892        print(u'Updating files in %s...' % os.path.join(setupDir, u'dist', 'TracOnlineDocumentation'))
893       
894        for df in docFiles:
895            files = glob.glob(os.path.join(srcDir, u'GeoEco', df))
896            for src in files:
897                tmpSrc = src + '.tmp'
898                dest = os.path.join(os.path.join(setupDir, u'dist', 'TracOnlineDocumentation'), src[len(os.path.join(srcDir, u'GeoEco')) + 1:])
899                if not os.path.isdir(os.path.dirname(dest)):
900                    os.mkdir(os.path.dirname(dest))
901                if os.path.isfile(dest):
902                    os.remove(dest)
903                shutil.move(tmpSrc, dest)
904
905    # Clean up temporary files.
906
907    print(u'')
908    print(u'********************************************************************************')
909    print(u'* Cleaning up')
910    print(u'********************************************************************************')
911    print(u'')
912
913    print(u'Removing directory %s...' % os.path.join(setupDir, u'build'))
914    for root, dirs, files in os.walk(os.path.join(setupDir, u'build')):
915        for d in dirs:
916            os.chmod(os.path.join(root, d), stat.S_IWRITE)
917        for f in files:
918            os.chmod(os.path.join(root, f), stat.S_IWRITE)
919    shutil.rmtree(os.path.join(setupDir, u'build'), False)
920    print(u'')
921    print(u'***** BUILD SUCCESSFUL *****')
922
923if __name__ == u'__main__':
924    main()
925
926sys.exit(0)
Note: See TracBrowser for help on using the browser.