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

Revision 68, 45.2 KB (checked in by jjr8, 6 years ago)

Final checkin to fix #51. The Python packages now install properly on systems that have Arc 9.1 or 9.2. I performed minimal testing on Arc 9.2.

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'RegisterToolboxWithArcCatalog91', u'bin', u'Release', u'RegisterToolboxWithArcCatalog91.exe'), os.path.join(arcGISDir, u'Bin', u'RegisterToolboxWithArcCatalog91.exe'))
378        shutil.copyfile(os.path.join(setupDir, u'..', u'VisualStudioSolutions', u'RegisterToolboxWithArcCatalog92', u'bin', u'Release', u'RegisterToolboxWithArcCatalog92.exe'), os.path.join(arcGISDir, u'Bin', u'RegisterToolboxWithArcCatalog92.exe'))
379
380        # Ensure these files are provided to distutils.
381
382        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'*.*'))
383        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Bin', u'*.*'))
384        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Rasters', u'*.*'))
385        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Rasters', u'*', u'*.*'))
386        packageData[u'GeoEco'].append(os.path.join(u'ArcGISToolbox', u'Scripts', u'*.*'))
387
388    # If running on Windows, generate the COM type library for classes that can
389    # be invoked through COM.
390
391    if sys.platform.lower() == u'win32':
392        print(u'')
393        print(u'********************************************************************************')
394        print(u'* Generating the Microsoft COM type library')
395        print(u'********************************************************************************')
396        print(u'')
397
398        # Open the IDL file for writing.
399
400        comDir = os.path.join(srcDir, u'GeoEco', u'COM')
401        idlFilePath = os.path.join(comDir, u'GeoEco.idl')
402        print(u'Opening %s for writing.' % idlFilePath)
403        idlFile = file(idlFilePath, u'w')
404        idlFile.write('// IDL for GeoEco Type Library\n')
405        idlFile.write('//\n')
406        idlFile.write('// GeoEco is a Python package that provides geospatial ecology tools.\n')
407        idlFile.write('// Many of these tools are exposed as COM classes. Each class has a\n')
408        idlFile.write('// ProgID beginning with "GeoEco." and exposes a dual interface.\n')
409        idlFile.write('// Late-binding clients such as script engines can invoke the classes\n')
410        idlFile.write('// through the IDispatch interface, while early-binding clients such\n')
411        idlFile.write('// as C# can import the GeoEco Type Library and invoke the classes\n')
412        idlFile.write('// through the more-efficient vtable interface.\n')
413        idlFile.write('//\n')
414        idlFile.write('// This IDL file defines all of the interfaces and classes exported by\n')
415        idlFile.write('// the GeoEco type library.\n')
416        idlFile.write('\n')
417        idlFile.write('import "oaidl.idl";\n')
418        idlFile.write('import "ocidl.idl";\n')
419        idlFile.write('\n')
420
421        # Obtain a list of all of the classes flagged for exposure through COM.
422        # Validate the classes' metadata. Also validate that each
423        # class has a unique name across the entire set of classes, regardless
424        # of which module it appears in. This means developers cannot
425        # disambiguate classes with the same names using the Python module
426        # hierarchy. The benefit of this restriction is that we don't have to
427        # generate multiple type libraries, or a single type library that
428        # prepends the full list of module names to the class name.
429
430        print(u'Validating classes flagged for exposure by COM:')
431
432        import pythoncom
433        import GeoEco.COM
434
435        classDict = {}
436        guidDict = {}
437
438        comClasses = GetCOMClassesInModule(u'GeoEco', sys.modules[u'GeoEco'].__path__[0])
439        for cls in comClasses:
440            GeoEco.COM.ValidateClassMetadata(cls)
441            classMetadata = cls.__doc__.Obj
442           
443            if classDict.has_key(classMetadata.Name):
444                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))
445            classDict[classMetadata.Name] = classMetadata
446           
447            if guidDict.has_key(classMetadata.COMIID):
448                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()))))
449            guidDict[classMetadata.COMIID] = (u'COMIID', classMetadata)
450           
451            if guidDict.has_key(classMetadata.COMCLSID):
452                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()))))
453            guidDict[classMetadata.COMIID] = (u'COMCLSID', classMetadata)
454           
455            print('  %s.%s as ProgID %s' % (cls.__doc__.Obj.Module.Name, cls.__doc__.Obj.Name, cls.__doc__.Obj.COMVersionIndependentProgID))
456
457        # Write the interface definitions to the IDL file.
458
459        print(u'Writing interface definitions to %s.' % idlFilePath)
460        for cls in comClasses:
461            idlFile.write(GeoEco.COM.GetIDLInterfaceDefinitionFromMetadata(cls.__doc__.Obj))
462            idlFile.write('\n')
463
464        # Write type library header to the IDL file.
465
466        idlFile.write('[\n')
467        idlFile.write('    uuid(%s),\n' % GeoEco.COM.TypeLibraryGUID[1:-1])
468        idlFile.write('    version(%i.%i),\n' % (GeoEco.COM.TypeLibraryVersion[0], GeoEco.COM.TypeLibraryVersion[1]))
469        idlFile.write('    helpstring("GeoEco %i.%i Type Library")\n' % (GeoEco.COM.TypeLibraryVersion[0], GeoEco.COM.TypeLibraryVersion[1]))   
470        idlFile.write(']\n')
471        idlFile.write('library GeoEco\n')
472        idlFile.write('{\n')
473        idlFile.write('    importlib("stdole32.tlb");\n')
474        idlFile.write('    importlib("stdole2.tlb");\n')
475
476        # Write the coclass definitions to the IDL file.
477
478        for cls in comClasses:
479            idlFile.write('\n')
480            idlFile.write('    [\n')
481            idlFile.write('        uuid(%s),\n' % cls.__doc__.Obj.COMCLSID[1:-1])
482            idlFile.write('        helpstring("%s Class")\n' % cls.__doc__.Obj.Name)
483            idlFile.write('    ]\n')
484            idlFile.write('    coclass %s\n' % cls.__doc__.Obj.Name)
485            idlFile.write('    {\n')
486            idlFile.write('        [default] interface I%s;\n' % cls.__doc__.Obj.Name)
487            idlFile.write('    };\n')
488
489        # Close the IDL file.       
490
491        idlFile.write('};\n')
492        idlFile.close()
493
494        # Execute the MIDL compiler on the IDL file to produce the TLB file.
495
496        tlbFilePath = os.path.splitext(idlFilePath)[0] + u'.tlb'       
497
498        args = [u'midl.exe', u'/out', os.path.dirname(idlFilePath), u'/tlb', tlbFilePath, u'/win32', idlFilePath]
499        print('Invoking %s' % ' '.join(args))
500        try:
501            p = subprocess.Popen(args)
502        except WindowsError, e:
503            if e.errno == 2:
504                print(e.__class__.__name__ + u': ' + unicode(e))
505                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.')
506            else:
507                raise
508        retcode = p.wait()
509        if retcode != 0:
510            sys.exit(u'midl.exe failed and returned exit code %i.' % retcode)
511        for ext in [u'_i.c', u'_p.c', u'.h']:
512            if os.path.exists(os.path.splitext(idlFilePath)[0] + ext):
513                os.remove(os.path.splitext(idlFilePath)[0] + ext)
514        if os.path.exists(os.path.join(os.path.dirname(idlFilePath), u'dlldata.c')):
515            os.remove(os.path.join(os.path.dirname(idlFilePath), u'dlldata.c'))
516
517        packageData[u'GeoEco'].append(os.path.join(u'COM', u'*'))
518
519    # If running on Windows, generate the postinstall script.
520
521    if sys.platform.lower() == u'win32':
522        print(u'')
523        print(u'********************************************************************************')
524        print(u'* Generating the Windows postinstall script')
525        print(u'********************************************************************************')
526        print(u'')
527
528        # Generate the list of classes that need COM registration.
529       
530        classesForCOMRegistration = '[' + ', '.join(map(lambda cls: '[' + repr(str(cls.__doc__.Obj.Module.Name)) + ', ' + repr(str(cls.__doc__.Obj.Name)) + ']', comClasses)) + ']'
531
532        # Write the install script.
533
534        postInstallFilePath = os.path.join(srcDir, u'GeoEcoPostInstall.py')
535        print(u'Opening %s for writing.' % postInstallFilePath)
536        f = file(postInstallFilePath, u'w')
537
538        f.write("""# GeoEco package post-install script.
539#
540# Do not delete this file! It runs during uninstallation of the GeoEco package.
541# It will be deleted automatically as part of uninstallation of GeoEco.
542
543import os
544import os.path
545import shutil
546import stat
547import sys
548import subprocess
549
550import GeoEco
551geoEcoRoot = os.path.dirname(sys.modules['GeoEco'].__file__)
552
553###############################################################################
554#  INPUT DATA
555###############################################################################
556
557classesForCOMRegistration = """ + classesForCOMRegistration + """
558
559###############################################################################
560#  HELPER FUNCTIONS
561###############################################################################
562   
563def ChmodRecursive(root, mode):
564    if os.path.isfile(root):
565        os.chmod(root, mode)
566    elif os.path.isdir(root):
567        os.chmod(root, mode)
568        for (root, dirs, files) in os.walk(root):
569            for name in files:
570                os.chmod(os.path.join(root, name), mode)
571            for name in dirs:
572                os.chmod(os.path.join(root, name), mode)
573
574def RegisterToolboxWithArcCatalog(action):
575    assert action == 'register' or action == 'unregister'
576
577    # Set/remove the read-only flag on the ArcGIS toolbox directory. This is
578    # recommended by the ArcGIS documentation, so that users do not accidentally
579    # tamper with the toolbox.
580
581    if action == 'register':
582        ChmodRecursive(os.path.join(geoEcoRoot, 'ArcGISToolbox'), stat.S_IREAD)
583    else:
584        ChmodRecursive(os.path.join(geoEcoRoot, 'ArcGISToolbox'), stat.S_IWRITE)
585
586    # Import pywin32 modules that we need.
587   
588    try:
589        import pywintypes
590        import win32api
591        import win32con
592        import win32file
593        import win32pipe
594        import win32process
595    except:
596        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.')
597
598    # Ensure the ArcGIS is installed by reading the InstallDir from the registry.
599    # Also read the ArcGIS version, so we can determine which version of
600    # RegisterToolboxWithArcCatalog we need to run.
601
602    try:
603        hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0')
604        try:
605            (arcGISInstallDir, installDirType) = win32api.RegQueryValueEx(hkey, 'InstallDir')
606            (realVersion, realVersionType) = win32api.RegQueryValueEx(hkey, 'RealVersion')
607        finally:
608            try:
609                win32api.RegCloseKey(hkey)
610            except:
611                pass
612        if not isinstance(arcGISInstallDir, basestring) or not os.path.isdir(arcGISInstallDir):
613            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.')
614        if not os.path.isfile(os.path.join(arcGISInstallDir, 'Bin', 'ArcCatalog.exe')):
615            raise ValueError('The ArcGIS file "%s" does not exist.', os.path.join(arcGISInstallDir, 'Bin', 'ArcCatalog.exe'))
616    except Exception, e:
617        if action == 'register':
618            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:')
619            print('')
620            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
621        return False
622
623    # Run RegisterToolboxWithArcCatalog.exe.
624
625    if str(realVersion) == '9.1':
626        executable = 'RegisterToolboxWithArcCatalog91.exe'
627    else:
628        executable = 'RegisterToolboxWithArcCatalog92.exe'
629
630    args = [os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Bin', executable),
631            action,
632            os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx')]
633   
634    try:       
635        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True)
636    except Exception, e:
637        if action == 'register':
638            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', executable), os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'), str(e).strip()))
639            print('')
640            print('You can manually add the toolbox to ArcCatalog from this location:')
641            print('')
642            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
643        return False
644
645    # Wait for RegisterToolboxWithArcCatalog.exe to complete and
646    # capture any error message.
647
648    retcode = p.wait()
649    if retcode != 0:
650        if action == 'register':
651            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', executable), os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'), p.stderr.read().strip()))
652            print('')
653            print('You can manually add the toolbox to ArcCatalog from this location:')
654            print('')
655            print(os.path.join(geoEcoRoot, 'ArcGISToolbox', 'Marine Geospatial Ecology Tools.tbx'))
656        return False
657
658    # Return successfully.
659   
660    return True
661
662def RegisterClassesAsCOMServers():
663
664    # Import pywin32 modules that we need.
665   
666    try:
667        import pythoncom
668        import win32com.server.register
669    except:
670        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.')
671
672    # Register the classes.
673
674    success = True
675    import GeoEco.COM
676    for [moduleName, className] in classesForCOMRegistration:
677        if not GeoEco.COM.RegisterCOMServerUsingClassMetadata(moduleName, className):
678            success = False
679
680    # Register the type library.
681
682    tlbFile = os.path.join(os.path.dirname(sys.modules['GeoEco.COM'].__file__), 'COM', 'GeoEco.tlb')   
683    try:
684        tli = pythoncom.LoadTypeLib(tlbFile)
685    except Exception, e:
686        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)))
687        success = False
688
689    if success:       
690        try:
691            pythoncom.RegisterTypeLib(tli, tlbFile)
692        except Exception, e:
693            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)))
694            success = False
695   
696    return success
697
698def UnregisterClassesAsCOMServers():
699
700    # Import pywin32 modules that we need.
701   
702    try:
703        import pythoncom
704        import win32com.server.register
705    except:
706        return
707
708    # Unregister the classes.
709
710    import GeoEco.COM
711    for [moduleName, className] in classesForCOMRegistration:
712        GeoEco.COM.UnregisterCOMServerUsingClassMetadata(moduleName, className)
713
714    # Unregister the type library:
715
716    try:
717        pythoncom.UnRegisterTypeLib('""" + GeoEco.COM.TypeLibraryGUID + '\', ' + str(GeoEco.COM.TypeLibraryVersion[0]) + ', ' + str(GeoEco.COM.TypeLibraryVersion[1]) + ', ' + str(GeoEco.COM.TypeLibraryLCID) + """, pythoncom.SYS_WIN32)
718    except:
719        pass
720
721def CreateShortcuts():
722
723    try:
724
725        # Delete GeoEco's directory in the All Users' Programs menu, if it exists.
726       
727        programsDir = get_special_folder_path("CSIDL_COMMON_PROGRAMS")
728        geoEcoProgramsDir = os.path.join(programsDir, 'Marine Geospatial Ecology Tools')
729        if os.path.exists(geoEcoProgramsDir):
730            shutil.rmtree(geoEcoProgramsDir)
731
732        # Create GeoEco's directory in the All Users' Programs menu.
733
734        os.mkdir(geoEcoProgramsDir)
735        directory_created(geoEcoProgramsDir)
736
737        # Create shortcuts for the documentation.       
738
739        docDir = os.path.join(geoEcoProgramsDir, 'Documentation')
740        os.mkdir(docDir)
741        directory_created(docDir)
742        create_shortcut(os.path.join(geoEcoRoot, 'Documentation', 'GettingStarted.html'), 'Getting Started With Marine Geospatial Ecology Tools', os.path.join(geoEcoProgramsDir, 'Documentation', 'Getting Started.lnk'))
743        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'Getting Started.lnk'))
744        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'))
745        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'ArcGIS Geoprocessing Reference.lnk'))
746        create_shortcut(os.path.join(geoEcoRoot, 'Documentation', 'PythonReference', 'PythonReference.html'), 'GeoEco Python Reference', os.path.join(geoEcoProgramsDir, 'Documentation', 'GeoEco Python Reference.lnk'))
747        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'GeoEco Python Reference.lnk'))
748        create_shortcut(os.path.join(geoEcoRoot, 'LICENSE.txt'), 'Marine Geospatial Ecology Tools Software License', os.path.join(geoEcoProgramsDir, 'Documentation', 'Software License.lnk'))
749        file_created(os.path.join(geoEcoProgramsDir, 'Documentation', 'Software License.lnk'))
750
751    except Exception, e:
752        print('WARNING: Failed to create shortcuts in the Windows "Programs" menu.')
753        return False
754   
755    return True   
756
757
758###############################################################################
759#  MAIN SCRIPT
760###############################################################################
761
762# If argv[1] is '-install' run the installation script.
763
764if len(sys.argv) >= 2 and sys.argv[1].lower() == '-install':
765    shortcutsCreated = CreateShortcuts()
766    arcGISToolboxInstalled = RegisterToolboxWithArcCatalog('register')
767    allCOMClassesRegistered = RegisterClassesAsCOMServers()
768
769    if shortcutsCreated and arcGISToolboxInstalled and allCOMClassesRegistered:
770        print('All installation tasks completed successfully.')
771    else:
772        print('')
773        print('Some installation tasks failed. Please review the error messages above.')
774
775# If argv[1] is '-remove' run the uninstallation script. Hide any errors because
776# they are not critical.
777
778elif len(sys.argv) >= 2 and sys.argv[1].lower() == '-remove':
779    RegisterToolboxWithArcCatalog('unregister')
780    UnregisterClassesAsCOMServers()
781
782# If argv[1] is absent or unrecognized, report an error.
783
784else:
785    print('ERROR: Invalid command line arguments.')
786    print('')
787    print('USAGE: GeoEcoPostInstall.py {-install|-remove}')
788""")
789        f.close()
790        scripts.append(postInstallFilePath)
791
792    # Generate the documentation.
793
794    print(u'')
795    print(u'********************************************************************************')
796    print(u'* Generating documentation')
797    print(u'********************************************************************************')
798    print(u'')
799
800    docFiles = [u'LICENSE.txt', os.path.join(u'Documentation', u'GettingStarted.html'), os.path.join(u'Documentation', u'*.css')]
801
802    # Generate the Python reference documentation.
803
804    print('Generating Python reference documentation...')
805    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'))
806    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.css'))
807    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.png'))
808    docFiles.append(os.path.join(u'Documentation', u'PythonReference', u'*.html'))
809
810    # If running on Windows, generate ArcGIS reference documentation.
811
812    if sys.platform.lower() == u'win32':
813        print('Generating ArcGIS reference documentation...')
814
815        # Ensure that we include the other files that are referenced by the HTML
816        # files that make up the ArcGIS documentation.
817
818        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'*.css'))
819        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'*.gif'))
820
821        # Generate ArcGISReference.html.
822
823        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'))
824        docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', u'ArcGISReference.html'))
825
826        # Get the path to the ArcGIS installation directory on this machine (the
827        # build machine), so we can locate an XSL stylesheet that comes with
828        # ArcGIS. We will use a modified version of this stylesheet to transform
829        # the XML metadata for each tool into HTML.
830
831        import win32api, win32con
832        hkey = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\ESRI\\ArcInfo\\Desktop\\8.0')
833        (arcGISInstallDir, installDirType) = win32api.RegQueryValueEx(hkey, 'InstallDir')
834        if not isinstance(arcGISInstallDir, basestring) or not os.path.isdir(arcGISInstallDir):
835            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.')
836
837        # Make some necessary changes ot the ArcGIS stylesheet using our own
838        # XSL transform.
839
840        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'))
841
842        # Generate the HTML file for each tool.
843
844        for method in arcGISMethods:       
845
846            # Transform the ESRI metadata XML into HTML, using the XSL
847            # stylesheet that comes with ArcGIS.
848
849            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'))
850            docFiles.append(os.path.join(u'Documentation', u'ArcGISReference', method.Class.Name + u'.' + method.Name + u'.html'))
851
852    packageData[u'GeoEco'].extend(docFiles)
853
854    # Run distutils setup.
855
856    print(u'')
857    print(u'********************************************************************************')
858    print(u'* Building/installing the python package')
859    print(u'********************************************************************************')
860    print(u'')
861
862    distutils.core.setup(name='GeoEco',
863                         version=str(GeoEco.__version__),
864                         description='Geospatial Ecology Tools',
865                         author='Jason Roberts',
866                         author_email='jason.roberts@duke.edu',
867                         url='http://code.env.duke.edu/projects/mget',
868                         package_dir={u'': srcDir},
869                         packages=packages,
870                         package_data=packageData,
871                         scripts=scripts,
872                         ext_modules=extensionModules)
873
874    # If running on Windows and Python 2.5, generate the Trac online
875    # documentation.
876
877    if sys.platform.lower() == u'win32' and sys.version_info[0] == 2 and sys.version_info[1] == 5:
878
879        print(u'')
880        print(u'********************************************************************************')
881        print(u'* Generating Trac online documentation...')
882        print(u'********************************************************************************')
883        print(u'')
884
885        # First run AdjustHtmlUrlsForTracWiki.xsl on all HTML files, so that the
886        # URLs within the files will resolve properly.
887
888        print(u'Running AdjustHtmlUrlsForTracWiki.xsl on HTML documentation files...')
889
890        for df in docFiles:
891            files = glob.glob(os.path.join(srcDir, u'GeoEco', df))
892            for src in files:
893                dest = src + '.tmp'
894                if src.endswith('.html') or src.endswith('.htm'):
895                    RunXslTransform(setupDir, src, os.path.join(srcDir, u'GeoEco', u'Documentation', u'AdjustHtmlUrlsForTracWiki.xsl'), dest)
896                else:
897                    shutil.copy2(src, dest)
898
899        # Now move the updated files to the dist directory.
900
901        print(u'Updating files in %s...' % os.path.join(setupDir, u'dist', 'TracOnlineDocumentation'))
902       
903        for df in docFiles:
904            files = glob.glob(os.path.join(srcDir, u'GeoEco', df))
905            for src in files:
906                tmpSrc = src + '.tmp'
907                dest = os.path.join(os.path.join(setupDir, u'dist', 'TracOnlineDocumentation'), src[len(os.path.join(srcDir, u'GeoEco')) + 1:])
908                if not os.path.isdir(os.path.dirname(dest)):
909                    os.mkdir(os.path.dirname(dest))
910                if os.path.isfile(dest):
911                    os.remove(dest)
912                shutil.move(tmpSrc, dest)
913
914    # Clean up temporary files.
915
916    print(u'')
917    print(u'********************************************************************************')
918    print(u'* Cleaning up')
919    print(u'********************************************************************************')
920    print(u'')
921
922    print(u'Removing directory %s...' % os.path.join(setupDir, u'build'))
923    for root, dirs, files in os.walk(os.path.join(setupDir, u'build')):
924        for d in dirs:
925            os.chmod(os.path.join(root, d), stat.S_IWRITE)
926        for f in files:
927            os.chmod(os.path.join(root, f), stat.S_IWRITE)
928    shutil.rmtree(os.path.join(setupDir, u'build'), False)
929    print(u'')
930    print(u'***** BUILD SUCCESSFUL *****')
931
932if __name__ == u'__main__':
933    main()
934
935sys.exit(0)
Note: See TracBrowser for help on using the browser.