BASIS  version 1.2.3 (revision 2104)
basisproject.py
00001 #! /sbiasfw/external/python/2.5.2/bin/python
00002 ##############################################################################
00003 # @file  basisproject.py
00004 # @brief Project tool used to create and/or modify a BASIS project.
00005 #
00006 # Copyright (c) 2011, 2012 University of Pennsylvania. All rights reserved.<br />
00007 # See https://www.cbica.upenn.edu/sbia/software/license.html or COPYING file.
00008 #
00009 # Contact: SBIA Group <sbia-software at uphs.upenn.edu>
00010 #
00011 # @ingroup Tools
00012 ##############################################################################
00013 
00014 # ============================================================================
00015 # modules
00016 # ============================================================================
00017 
00018 
00019 def _basis_init_sys_path():
00020     import os
00021     import sys
00022     module_dir  = os.path.dirname(os.path.realpath(__file__))
00023     sitelib_dir = os.path.normpath(os.path.join(module_dir, '../lib/python'))
00024     if sitelib_dir not in sys.path:
00025         sys.path.insert(0, sitelib_dir)
00026     sitelib_dir = os.path.normpath(os.path.join(module_dir, '../lib/python'))
00027     if sitelib_dir not in sys.path:
00028         sys.path.insert(0, sitelib_dir)
00029 
00030 _basis_init_sys_path()
00031 from sbia.basis import basis
00032 
00033 
00034 import os                       # file manipulation
00035 import sys                      # system functions
00036 import re                       # regular expressions
00037 import datetime                 # now(), to get current year for copyright
00038 import shutil                   # copyfile()
00039 from sbia.basis import argparse # command-line parsing
00040 from sbia.basis import diff3    # three-way diff algorithm
00041 
00042 # ============================================================================
00043 # constants
00044 # ============================================================================
00045 
00046 # executable information
00047 _EXEC_DIR  = basis.get_executable_directory()
00048 _EXEC_NAME = basis.get_executable_name()
00049 
00050 # constants used as values for the template options
00051 DEL = -1 # remove feature/file if present
00052 ADD =  1 # add feature/file if missing
00053 
00054 # root directory of project template
00055 _template = os.path.abspath(os.path.join(_EXEC_DIR, '../share/template'))
00056 
00057 # ============================================================================
00058 # auxiliary functions
00059 # ============================================================================
00060 
00061 # ----------------------------------------------------------------------------
00062 def get_template_options(op=None):
00063     return {'config-settings'     : op, # add/remove general project settings file
00064             'config-components'   : op, # add/remove components configuration file
00065             'config-package'      : op, # add/remove package configuration file
00066             'config-find'         : op, # add/remove find package configuration file
00067             'config-find_version' : op, # add/remove find package configuration version file
00068             'config-script'       : op, # add/remove script configuration file
00069             'config-test'         : op, # add/remove testing configuration file
00070             'config-use'          : op, # add/remove package configuration use file
00071             'data'                : op, # add/remove auxiliary data files
00072             'doc'                 : op, # add/remove documentation
00073             'example'             : op, # add/remove example
00074             'include'             : op, # add/remove public includes tree
00075             'src'                 : op, # add/remove directory for source files
00076             'modules'             : op, # add/remove support for modularization
00077             'test'                : op} # add/remove testing configuration file
00078  
00079 # ----------------------------------------------------------------------------
00080 def get_standard_template_options(opts, overwrite=False):
00081     """Select standard project template."""
00082     if overwrite or opts.get('config-settings')     == None: opts['config-settings'] = DEL
00083     if overwrite or opts.get('config-components')   == None: opts['config-components'] = DEL
00084     if overwrite or opts.get('config-package')      == None: opts['config-package'] = DEL
00085     if overwrite or opts.get('config-find')         == None: opts['config-find'] = DEL
00086     if overwrite or opts.get('config-find_version') == None: opts['config-find_version'] = DEL
00087     if overwrite or opts.get('config-script')       == None: opts['config-script'] = DEL
00088     if overwrite or opts.get('config-test')         == None: opts['config-test'] = DEL
00089     if overwrite or opts.get('config-use')          == None: opts['config-use'] = DEL
00090     if overwrite or opts.get('data')                == None: opts['data'] = DEL
00091     if overwrite or opts.get('doc')                 == None: opts['doc'] = ADD
00092     if overwrite or opts.get('example')             == None: opts['example'] = ADD
00093     if overwrite or opts.get('include')             == None: opts['include'] = DEL
00094     if overwrite or opts.get('test')                == None: opts['test'] = ADD
00095     if (overwrite
00096             or (opts.get('src') == None and opts.get('modules') == None)
00097             or (opts.get('src') == None and opts.get('modules') == DEL)
00098             or (opts.get('src') == DEL  and opts.get('modules') == None)):
00099         if overwrite or opts.get('src') != DEL:
00100             opts['src'] = ADD
00101             opts['modules'] = DEL
00102         else:
00103             opts['src'] = DEL
00104             opts['modules'] = ADD
00105     return opts
00106 
00107 # ----------------------------------------------------------------------------
00108 def _update_dflt_opt(root, opts, opt, paths):
00109     if not type(paths) is list:
00110         paths = [paths]
00111     for path in paths:
00112         if opt not in opts:
00113             opts[opt] = None
00114         if opts[opt] == None and os.path.exists(os.path.join(root, path)):
00115             opts[opt] = ADD
00116     return opts
00117 
00118 # ----------------------------------------------------------------------------
00119 def get_default_template_options(root, opts, create):
00120     """Select default options."""
00121     if create:
00122         opts = get_standard_template_options(opts, overwrite=False)
00123     else:
00124         if os.path.isdir(os.path.join(root, 'config')):
00125             opts = _update_dflt_opt(root, opts, 'config-components',   'config/Components.cmake')
00126             opts = _update_dflt_opt(root, opts, 'config-find',         'config/Config.cmake.in')
00127             opts = _update_dflt_opt(root, opts, 'config-find_version', 'config/ConfigVersion.cmake.in')
00128             opts = _update_dflt_opt(root, opts, 'config-test',         'config/CTestCustom.cmake.in')
00129             opts = _update_dflt_opt(root, opts, 'config-package',      'config/Package.cmake')
00130             opts = _update_dflt_opt(root, opts, 'config-script',       'config/ScriptConfig.cmake.in')
00131             opts = _update_dflt_opt(root, opts, 'config-settings',     'config/Settings.cmake')
00132             opts = _update_dflt_opt(root, opts, 'config-use',          'config/ConfigUse.cmake.in')
00133         opts = _update_dflt_opt(root, opts, 'data',    'data')
00134         opts = _update_dflt_opt(root, opts, 'doc',     'doc')
00135         opts = _update_dflt_opt(root, opts, 'example', 'example')
00136         opts = _update_dflt_opt(root, opts, 'include', 'include')
00137         opts = _update_dflt_opt(root, opts, 'src',     'src')
00138         opts = _update_dflt_opt(root, opts, 'modules', 'modules')
00139         opts = _update_dflt_opt(root, opts, 'test',    'test')
00140     return opts
00141 
00142 # ----------------------------------------------------------------------------
00143 def get_project_name(cmake_file):
00144     """Extract project name from BasisProject.cmake file of existing project."""
00145     fp = open(cmake_file, 'rt')
00146     txt = fp.read()
00147     fp.close()
00148     m = re.search(r'\s*basis_project\s*\(\s*NAME\s+\"?(?P<name>\w+)\"?', txt)
00149     if m: return m.group('name')
00150     return None
00151 
00152 # ----------------------------------------------------------------------------
00153 def get_template_version(cmake_file):
00154     """Extract template version from root CMakeLists.cmake file of existing project."""
00155     fp = open(cmake_file, 'rt')
00156     txt = fp.read()
00157     fp.close()
00158     m = re.search(r'\s*set\s*\(\s*TEMPLATE_VERSION\s+\"?(?P<version>[0-9.]+)\"?', txt)
00159     if m: return m.group('version')
00160     return None
00161 
00162 # ============================================================================
00163 # add / remove project file
00164 # ============================================================================
00165 
00166 # ----------------------------------------------------------------------------
00167 def add_dependencies(txt, deps, required=True, test=False):
00168     """Add dependencies to basis_project() command in BasisProject.cmake file."""
00169     if test:
00170         if required:
00171             placeholder = '#<test-dependency>'
00172         else:
00173             placeholder = '#<optional-test-dependency>'
00174     else:
00175         if required:
00176             placeholder = '#<dependency>'
00177         else:
00178             placeholder = '#<optional-dependency>'
00179     for dep in deps:
00180         txt = txt.replace(placeholder, dep + '\n    ' + placeholder, 1)
00181     return txt
00182 
00183 # ----------------------------------------------------------------------------
00184 def alter(filename, args, backup=True, update=False):
00185     """Alter project file, i.e., substitute placeholders.
00186 
00187     @param [in] filename Path of text file to alter.
00188     @param [in] args     Command-line arguments.
00189     @param [in] backup   Whether to backup file before overwriting it.
00190     @param [in] update   Whether template file is generated for the update
00191                          of an existing project file. In this case, certain
00192                          modifications should not be applied to avoid conflicts.
00193 
00194     @retval -1 on error
00195     @retval  0 if file was not modified
00196     @retval  1 if file has been modified
00197 
00198     """
00199     # read file content
00200     fp = open(filename, 'rt')
00201     txt = fp.read()
00202     fp.close()
00203     # alter file content
00204     out = txt
00205     out = out.replace('<template-version>', '1.3')
00206     # <project>
00207     if hasattr(args, 'name') and args.name:
00208         out = out.replace('<project>', args.name)
00209     # <description>
00210     if hasattr(args, 'description') and args.description:
00211         out = out.replace('<description>', args.description)
00212     # <author>
00213     if hasattr(args, 'author') and args.author:
00214         start = out.find('<author>')
00215         if start != -1:
00216             linestart = out.rfind('\n', 0, start)
00217             if linestart == -1: linestart  = 0
00218             else:               linestart += 1
00219             authors = [a.strip() for a in args.author.split(',')]
00220             authors = ('\n' + out[linestart:start]).join(authors)
00221             out = out.replace('<author>', authors)
00222     # <year>
00223     year = None
00224     if hasattr(args, 'year') and args.year:
00225         year = args.year
00226     elif not update:
00227         year = datetime.datetime.now().year
00228     if year:
00229         out = out.replace('<year>', str(year))
00230     # dependencies
00231     if filename.endswith('BasisProject.cmake'):
00232         if hasattr(args, 'use') and args.use:
00233             out = add_dependencies(out, args.use, required=True, test=False)
00234         if hasattr(args, 'useopt') and args.useopt:
00235             out = add_dependencies(out, args.useopt, required=False, test=False)
00236         if hasattr(args, 'usetest') and args.usetest:
00237             out = add_dependencies(out, args.usetest, required=True, test=True)
00238         if hasattr(args, 'useopttest') and args.useopttest:
00239             out = add_dependencies(out, args.useopttest, required=False, test=True)
00240     # return if file content is unchanged
00241     if out == txt: return 0
00242     # otherwise, backup file first
00243     if backup:
00244         try:
00245             shutil.copy(filename, filename + '~')
00246         except Exception, e:
00247             sys.stderr.write("E %s - failed to backup file before altering it" % filename)
00248             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00249             sys.stderr.write('\n')
00250             return -1
00251     # then overwrite it
00252     fp = open(filename, 'wt')
00253     fp.write(out)
00254     fp.close()
00255     return 1
00256 
00257 # ----------------------------------------------------------------------------
00258 def add(path, args, isdir=False):
00259     """Add or modify project directory or file.
00260 
00261     @param [in] path  The path of the directory or file relative to the
00262                       template or project root, respectively.
00263     @param [in] args  Namespace with command-line arguments.
00264     @param [in] isdir Whether @p path is a directory that may not have
00265                       a corresponding template.
00266 
00267     @retval -1 on error
00268     @retval  0 if existing file is up to date
00269     @retval  1 if project file has been added/updated
00270     @retval  2 if project file has been updated, but with conflicts
00271 
00272     """
00273     retval = 0
00274 
00275     template_path          = os.path.join(args.template, path)
00276     original_template_path = os.path.join(args.original_template, path)
00277     project_path           = os.path.join(args.root, path)
00278 
00279     # check existence of template
00280     if not isdir and not os.path.exists(template_path):
00281         sys.stderr.write("E %s - template missing\n" % project_path)
00282         return -1
00283 
00284     # handle case that path in project exists already
00285     if os.path.exists(project_path):
00286         if isdir or os.path.isdir(template_path):
00287             if not os.path.isdir(project_path):
00288                 # template is directory, but there is a file in the project
00289                 sys.stderr.write("E %s - not a directory\n" % project_path)
00290                 return -1
00291             else:
00292                 # directory already exists, nothing to do
00293                 return 0
00294         elif os.path.isfile(template_path):
00295             if not os.path.isfile(project_path):
00296                 # template is file, but there is a directory in the project
00297                 sys.stderr.write("E %s - not a file\n" % project_path)
00298                 return -1
00299             # if update of existing files is disabled, only alter file
00300             # to add further dependencies or specify an author...
00301             if not args.update:
00302                 rv = alter(project_path, args, backup=args.backup)
00303                 if rv == 1: print "M %s" % project_path
00304                 return rv
00305 
00306     # create (intermediate) directory
00307     if isdir or os.path.isdir(template_path):
00308         path_dir = path
00309     else:
00310         path_dir = os.path.dirname(path)
00311     project_dir = os.path.join(args.root, path_dir)
00312     if not os.path.isdir(project_dir):
00313         try:
00314             os.makedirs(project_dir)
00315             print "A %s" % project_dir
00316         except Exception, e:
00317             sys.stderr.write("E %s - failed to make directory" % project_dir)
00318             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00319             sys.stderr.write('\n')
00320             return -1
00321 
00322     # add/update file
00323     if os.path.isfile(template_path):
00324         # project file does not exist yet
00325         if not os.path.isfile(project_path):
00326             # copy template
00327             try:
00328                 shutil.copyfile(template_path, project_path)
00329             except Exception, e:
00330                 sys.stderr.write("E %s - failed to add file" % project_path)
00331                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00332                 sys.stderr.write('\n')
00333                 return -1
00334             # alter project file, e.g., substitute for project name
00335             try:
00336                 if alter(project_path, args, backup=False) == -1:
00337                     retval = -1
00338             except Exception, e:
00339                 retval = -1
00340             if retval != -1:
00341                 print "A %s" % project_path
00342                 retval = 1
00343             else:
00344                 sys.stderr.write("E %s - failed to alter file, may require manual edits" % project_path)
00345                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00346                 sys.stderr.write('\n')
00347         # project file exists already
00348         else:
00349             # update project file if copy of previous template exists
00350             if os.path.isfile(original_template_path):
00351                 # alter new template
00352                 try:
00353                     shutil.copyfile(template_path, project_path + '.template')
00354                     if alter(project_path + '.template', args, backup=False, update=True) == -1:
00355                         raise Exception('failed to alter template file')
00356                 except Exception, e:
00357                     sys.stderr.write("E %s - failed to create temporary template" % project_path)
00358                     if args.verbose > 0: sys.stderr.write(': ' + str(e))
00359                     sys.stderr.write('\n')
00360                     return -1
00361                 # merge new template with project file using three-way diff
00362                 fp = open(project_path, 'rt')
00363                 current = fp.readlines()
00364                 fp.close()
00365                 fp = open(project_path + '.template', 'rt')
00366                 template = fp.readlines()
00367                 fp.close()
00368                 fp = open(original_template_path, 'rt')
00369                 initial = fp.readlines()
00370                 fp.close()
00371                 merge = diff3.merge(current, initial, template)
00372                 # check if anything has changed at all
00373                 if merge['body'] != current:
00374                     # in case of conflicts, backup file using .mine suffix
00375                     if merge['conflict']:
00376                         try:
00377                             shutil.copy(project_path, project_path + '.mine')
00378                         except Exception, e:
00379                             sys.stderr.write("E %s - failed to backup file" % project_path)
00380                             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00381                             sys.stderr.write('\n')
00382                             return -1
00383                     # otherwise, backup current project file using ~ suffix
00384                     elif args.backup:
00385                         try:
00386                             shutil.copyfile(project_path, project_path + '~')
00387                         except Exception, e:
00388                             sys.stderr.write("E %s - failed to backup file" % project_path)
00389                             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00390                             sys.stderr.write('\n')
00391                             return -1
00392                     # replace project file by merged file
00393                     try:
00394                         fp = open(project_path, 'wt')
00395                         fp.writelines(merge['body'])
00396                         fp.close()
00397                     except Exception, e:
00398                         sys.stderr.write("E %s - failed to update file" % project_path)
00399                         if args.verbose > 0: sys.stderr.write(': ' + str(e))
00400                         sys.stderr.write('\n')
00401                         return -1
00402                     if retval != -1:
00403                         if merge['conflict']: retval = 2
00404                         else:                 retval = 1
00405                 # remove altered template again if update was successful
00406                 if not merge['conflict']:
00407                     try:
00408                         os.remove(project_path + '.template')
00409                     except Exception, e:
00410                         sys.stderr.write("E %s - failed to remove temporary file" % project_path)
00411                         if args.verbose > 0: sys.stderr.write(': ' + str(e))
00412                         sys.stderr.write('\n')
00413                         retval = -1
00414             # cannot update files without copy of original template
00415             else:
00416                 try:
00417                     # copy current template for reference next to the corresponding
00418                     # project file
00419                     shutil.copyfile(template_path, project_path + '.template')
00420                 except:
00421                     pass
00422                 sys.stderr.write("S %s - cannot update file: missing original template\n" % project_path)
00423                 retval = -1
00424             # print status message if file was updated
00425             if retval > 0:
00426                 if merge['conflict']: print "C %s" % project_path
00427                 else:                 print "G %s" % project_path
00428     # done
00429     return retval
00430 
00431 # ----------------------------------------------------------------------------
00432 def remove_directory(path):
00433     isempty = True
00434     for filename in os.listdir(path):
00435         if filename != '.svn' and filename != '.git' and filename != '.hg':
00436             isempty = False
00437             break
00438     if isempty:
00439         shutil.rmtree(path)
00440         return True
00441     return False
00442 
00443 # ----------------------------------------------------------------------------
00444 def delete(path, args, isdir=False):
00445     """Delete file or empty directory.
00446 
00447     @param [in] path  Path relative to template or project root, respectively.
00448     @param [in] args  Namespace with command-line arguments.
00449     @param [in] isdir Whether @p path is a directory path that may not have
00450                       a corresponding template.
00451 
00452     @retval -1 on error
00453     @retval  0 if file/directory did not exist
00454     @retval  1 if file/directory was removed
00455 
00456     """
00457     retval = 0
00458 
00459     original_template_path = os.path.join(args.original_template, path)
00460     project_path = os.path.join(args.root, path)
00461 
00462     # delete existing directory
00463     if os.path.isdir(project_path):
00464         if args.force:
00465             try:
00466                 shutil.rmtree(project_path)
00467             except Exception, e:
00468                 sys.stderr.write("E %s - failed to remove directory" % project_path)
00469                 if verbose > 0: sys.stderr.write(': ' + str(e))
00470                 sys.stderr.write('\n')
00471                 retval = -1
00472         else:
00473             try:
00474                 os.rmdir(project_path)
00475             except Exception, e:
00476                 sys.stderr.write("E %s - failed to remove directory;"
00477                                  " use --force to force deletion of non-empty directory" % project_path)
00478                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00479                 sys.stderr.write('\n')
00480                 retval = -1
00481         if retval != -1:
00482             print "D %s" % project_path
00483             retval = 1
00484     # delete existing file and copy of template
00485     elif os.path.isfile(project_path):
00486         # check if project file differs from template
00487         if not args.force:
00488             if not os.path.isfile(original_template_path):
00489                 sys.stderr.write("E %s - original template missing, use --force to force deletion\n" % project_path)
00490                 return -1
00491             # alter new template
00492             try:
00493                 shutil.copyfile(original_template_path, project_path + '.template')
00494                 if alter(project_path + '.template', args, backup=False) == -1:
00495                     raise Exception('failed to alter template file')
00496             except Exception, e:
00497                 sys.stderr.write("E %s - failed to create temporary template,"
00498                                  " use --force to force deletion")
00499                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00500                 sys.stderr.write('\n')
00501                 return -1
00502             # compare files
00503             fp = open(project_path, 'rt')
00504             current = fp.read()
00505             fp.close()
00506             fp = open(project_path + '.template', 'rt')
00507             template = fp.read()
00508             fp.close()
00509             # remove altered template file
00510             try:
00511                 os.remove(project_path + '.template')
00512             except Exception, e:
00513                 sys.stderr.write("E %s - failed to remove temporary file" % project_path)
00514                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00515                 sys.stderr.write('\n')
00516                 retval = -1
00517             # check for differences
00518             if current != template:
00519                 if retval != -1:
00520                     sys.stderr.write("S %s - file was modified, use --force to force deletion\n" % project_path)
00521                 return -1
00522         # delete project file
00523         try:
00524             os.remove(project_path)
00525         except Exception, e:
00526             sys.stderr.write("E %s - failed to remove file" % project_path)
00527             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00528             sys.stderr.write('\n')
00529             return -1
00530         # print status message
00531         if retval != -1:
00532             print "D %s" % project_path
00533             retval = 1
00534         # remove now empty directories
00535         subdir = os.path.dirname(project_path)
00536         while subdir != args.root:
00537             try:
00538                 if remove_directory(subdir):
00539                     print "D %s" % subdir
00540             except Exception, e:
00541                 sys.stderr.write("W %s - failed to remove directory" % subdir)
00542                 if args.verbose > 0: sys.stderr.write(': ' + str(e))
00543                 sys.stderr.write('\n')
00544                 retval = -1
00545                 break
00546             subdir = os.path.dirname(subdir)
00547     # done
00548     return retval
00549 
00550 # ----------------------------------------------------------------------------
00551 def addordel(op, path, args, isdir=False):
00552     """Add/update or delete file or directory depending on operation specified.
00553 
00554     @param [in] op    Operations. Either one of ADD, None, or DEL.
00555     @param [in] path  File path relative to tempate or project root, respectively.
00556     @param [in] args  Namespace with command-line arguments.
00557     @param [in] isdir Whether @p path is a directory path that may not have
00558                       a corresponding template.
00559 
00560     @retval -1 on error
00561     @retval  0 if nothing was done
00562     @retval  1 if operation was successful
00563 
00564     """
00565     # add file/directory to project
00566     if op == ADD:
00567         return add(path, args, isdir=isdir)
00568     # delete file/directory from project
00569     elif op == DEL:
00570         return delete(path, args, isdir=isdir)
00571     # nothing to do otherwise
00572     return 0
00573 
00574 # ----------------------------------------------------------------------------
00575 class AccumulatorForNumbersOfChangesAndErrors(object):
00576     """Helper class used to accumulate number of changes, conflicts, and
00577        errors when calling either one of the add(), delete(), or addordel()
00578        functions."""
00579     # ------------------------------------------------------------------------
00580     changes   = 0
00581     conflicts = 0
00582     errors    = 0
00583     # ------------------------------------------------------------------------
00584     def call(self, func, *args, **kwargs):
00585         """Call the specified function with the given arguments."""
00586         rt = func(*args, **kwargs)
00587         if rt == -1: self.errors += 1
00588         else:
00589             if rt  > 0: self.changes   += 1
00590             if rt == 2: self.conflicts += 1
00591 
00592 # ============================================================================
00593 # main
00594 # ============================================================================
00595 
00596 # ----------------------------------------------------------------------------
00597 if __name__ == '__main__':
00598     ok = True
00599 
00600     # ------------------------------------------------------------------------
00601     # program help
00602     parser = argparse.ArgumentParser(prog='basisproject', description="""
00603   This command-line tool, also referred to as project tool, can be used to
00604   create a new project from the BASIS project template version 1.3 or to modify a
00605   previously created BASIS project.
00606 
00607   Depending on the grade of customization or optional inclusion of template
00608   components, different subsets of the fully featured project template can be
00609   selected. Additional template files and directories can be added to an existing
00610   project at any time. Further, if the --no* options are given explicitly,
00611   project files which were previously copied from the template are deleted.
00612   Files are, however, only deleted if they were not modified by the project
00613   developer since their creation and hence do not contain project related changes.
00614   Similarly are directories deleted by this tool only if empty. The deletion of
00615   modified files can be forced by supplying the --force option.
00616 
00617   Besides the name of the new project and a brief description, names of external
00618   packages required or optionally used by this project can be specified. For each
00619   such package, an entry in the list of dependencies given as argument to either
00620   one of the DEPENDS* options of the basis_project() command is added.
00621 
00622   An additional feature of this tool is, that it can upgrade an existing project
00623   to a newer project template version, given that the existing directory structure
00624   and file names were preserved. User changes to previously added template files
00625   are preserved and merged with the changes of the template using a so-called
00626   three-way diff similar to the Subversion tool svn. If the automatic file merge
00627   is not successful, a copy of the original project file (*.mine) as well as the
00628   new template file (*.template) are written to the project directory next to the
00629   project file which has been overwritten with the merged content which includes
00630   markers to indicate where the conflicts occurred. The project file has to be
00631   edited manually in this case to resolve any conflicts. Once the conflicts have
00632   been resolved, the *.mine and *.template files must be removed before this
00633   tool can be used for another update of the project files. This can be done
00634   either manually or by running this program with the --cleanup option.""",
00635         formatter_class=argparse.ArgumentDefaultsHelpFormatter)
00636 
00637     # ----------------------------------------------------------------------------
00638     # define command-line arguments
00639 
00640     parser.set_defaults(opts=get_template_options())
00641 
00642     # --version
00643     parser.add_argument('--version', action='version',
00644             help="Show version information and exit.",
00645             version="""%(prog)s (BASIS) version 1.2.3 (revision 2104)
00646 Copyright (c) 2011, 2012 University of Pennsylvania. All rights reserved.
00647 See https://www.cbica.upenn.edu/sbia/software/license.html or COPYING file.""")
00648 
00649     # --verbose
00650     parser.add_argument('-v', '--verbose', action='count', default=0,
00651             help="Increase verbosity of output messages.")
00652 
00653     # --name/--description/--author
00654     parser.add_argument('--name', metavar='<name>',
00655             help="Name of new project.")
00656 
00657     parser.add_argument('--description', metavar='<text>',
00658             help="Brief project description.")
00659 
00660     parser.add_argument('--author', metavar='<author>',
00661             help="Name of original author of the software.")
00662 
00663     # --root/--template
00664     parser.add_argument('--root', metavar='<dir>', type=os.path.abspath,
00665             help="""Project root directory. Defaults to a subdirectory named after the
00666 project created in the current working directory. In order to update
00667 an existing project, specify the root directory of this project
00668 using this option.""")
00669 
00670     parser.add_argument('--template', metavar='<dir>',
00671             type=os.path.abspath, default=_template + '-1.3',
00672             help="""Root directory of project template. Defaults to the template
00673 corresponding to this BASIS installation.""")
00674 
00675     parser.add_argument('--original', metavar='<dir>',
00676             type=os.path.abspath, dest='original_template',
00677             help="""Root directory of project template which the already existing project
00678 was created from or last updated to. By default, the version of the original template
00679 is extracted from the root CMakeLists.txt file of the project. Given this version,
00680 this tool determines the path of the corresponding project template which is part of
00681 the BASIS installation. If this previous template is not available, an automatic
00682 update of project files to a newer template version is not feasible.""")
00683 
00684     # --use et al.
00685     parser.add_argument('--use', metavar='<pkg>', action='append', default=[],
00686             help="""Name of external package used by this project.
00687 Note that the package name is case sensitive.""")
00688 
00689     parser.add_argument('--useopt', metavar='<pkg>', action='append', default=[],
00690             help="""Name of external package optionally used by this project.
00691 Note that the package name is case sensitive.""")
00692 
00693 
00694     parser.add_argument('--usetest', metavar='<pkg>', action='append', default=[],
00695             help="""Name of external package required by tests of this project.
00696 Note that the package name is case sensitive.""")
00697 
00698     parser.add_argument('--useopttest', metavar='<pkg>', action='append', default=[],
00699             help="""Name of external package optionally used by tests of this project.
00700 Note that the package name is case sensitive.""")
00701 
00702     # --minimal
00703     class MinimalTemplateAction(argparse.Action):
00704         def __call__(self, parser, namespace, value, option_string):
00705             setattr(namespace, 'opts', get_template_options(DEL))
00706 
00707     parser.add_argument('--minimal', nargs=0, action=MinimalTemplateAction,
00708             help="""Choose minimal project template. Corresponds to not
00709 selecting any of the additional template files.""")
00710 
00711     # --standard
00712     class StandardTemplateAction(argparse.Action):
00713         def __call__(self, parser, namespace, value, option_string):
00714             setattr(namespace, 'opts', get_standard_template_options({}))
00715 
00716     parser.add_argument('--standard', nargs=0, action=StandardTemplateAction,
00717             help="""Choose standard project template. This is the default
00718 project template if no template was selected explicitly.
00719 Corresponds to:
00720     --doc
00721     --example
00722     --noconfig
00723     --nodata
00724     --test""")
00725 
00726     # --full
00727     class FullTemplateAction(argparse.Action):
00728         def __call__(self, parser, namespace, value, option_string):
00729             setattr(namespace, 'opts', get_template_options(ADD))
00730 
00731     parser.add_argument('--full', nargs=0, action=FullTemplateAction,
00732             help="""Choose full-featured project template.""")
00733 
00734     # --toplevel
00735     class TopLevelTemplateAction(argparse.Action):
00736         def __call__(self, parser, namespace, value, option_string):
00737             opts = get_standard_template_options({})
00738             opts['modules'] = ADD
00739             opts['src']     = DEL
00740             opts['test']    = DEL
00741             setattr(namespace, 'opts', opts)
00742 
00743     parser.add_argument('--toplevel', nargs=0, action=TopLevelTemplateAction,
00744             help="""Choose top-level project template.""")
00745 
00746     # --module
00747     class ModuleTemplateAction(argparse.Action):
00748         def __call__(self, parser, namespace, value, option_string):
00749             opts = get_template_options(DEL)
00750             opts['include'] = ADD
00751             opts['src']     = ADD
00752             opts['test']    = ADD
00753             setattr(namespace, 'opts', opts)
00754 
00755     parser.add_argument('--module', nargs=0, action=ModuleTemplateAction,
00756             help="""Choose project module template.""")
00757 
00758     # --[no]config
00759     class ConfigAction(argparse.Action):
00760         def __call__(self, parser, namespace, values, option_string):
00761             if option_string == '--config': opts = self._get_options(ADD)
00762             else:                           opts = self._get_options(DEL)
00763             setattr(namespace, 'opts', opts)
00764 
00765         def _get_options(self, op):
00766             return {'config-settings':     op,
00767                     'config-components':   op,
00768                     'config-package':      op,
00769                     'config-find':         op,
00770                     'config-find_version': op,
00771                     'config-script':       op,
00772                     'config-test':         op,
00773                     'config-use':          op}
00774 
00775     parser.add_argument('--config', '--noconfig',
00776             nargs=0, action=ConfigAction,
00777             help="""Include/exclude all custom configuration files.""")
00778 
00779     # granular template options
00780     class OptAction(argparse.Action):
00781         def __call__(self, parser, namespace, values, option_string):
00782             if not hasattr(namespace, 'opts'):
00783                 setattr(namespace, 'opts', {})
00784             if option_string.startswith('--no'):
00785                 op  = DEL
00786                 opt = option_string[4:]
00787             else:
00788                 op  = ADD
00789                 opt = option_string[2:]
00790             namespace.opts[opt] = op
00791 
00792     parser.add_argument('--config-components', '--noconfig-components',
00793             nargs=0, action=OptAction,
00794             help="Include/exclude custom Components.cmake file.")
00795 
00796     parser.add_argument('--config-find', '--noconfig-find',
00797             nargs=0, action=OptAction,
00798             help="Include/exclude custom Config.cmake.in file.")
00799 
00800     parser.add_argument('--config-find-version', '--noconfig-find-version',
00801             nargs=0, action=OptAction,
00802             help="Include/exclude custom ConfigVersion.cmake.in file.")
00803 
00804     parser.add_argument('--config-package', '--noconfig-package',
00805             nargs=0, action=OptAction,
00806             help="Include/exclude custom Package.cmake file.")
00807 
00808     parser.add_argument('--config-script', '--noconfig-script',
00809             nargs=0, action=OptAction,
00810             help="Include/exclude custom ScriptConfig.cmake.in file.")
00811 
00812     parser.add_argument('--config-settings', '--noconfig-settings',
00813             nargs=0, action=OptAction,
00814             help="Include/exclude custom Settings.cmake file.")
00815 
00816     parser.add_argument('--config-test', '--noconfig-test',
00817             nargs=0, action=OptAction,
00818             help="Include/exclude custom CTestCustom.cmake.in file.")
00819 
00820     parser.add_argument('--config-use', '--noconfig-use',
00821             nargs=0, action=OptAction,
00822             help="Include/exclude custom ConfigUse.cmake.in file.")
00823 
00824     parser.add_argument('--data', '--nodata',
00825             nargs=0, action=OptAction,
00826             help="Add/remove directory for auxiliary data files.")
00827 
00828     parser.add_argument('--doc', '--nodoc',
00829             nargs=0, action=OptAction,
00830             help="Add/remove directory for documentation files.")
00831 
00832     parser.add_argument('--example', '--noexample',
00833             nargs=0, action=OptAction,
00834             help="Add/remove directory for example files.")
00835 
00836     parser.add_argument('--include', '--noinclude',
00837             nargs=0, action=OptAction,
00838             help="Add/remove directory for public header files.")
00839 
00840     parser.add_argument('--modules', '--nomodules',
00841             nargs=0, action=OptAction,
00842             help="Add/remove support for modularization.")
00843 
00844     parser.add_argument('--src', '--nosrc',
00845             nargs=0, action=OptAction,
00846             help="Add/remove directory for project source files.")
00847 
00848     parser.add_argument('--test', '--notest',
00849             nargs=0, action=OptAction,
00850             help="Add/remove support for testing.")
00851 
00852     # --update
00853     parser.add_argument('-u', '--update', action='store_true', default=False,
00854             help="""Enable update of existing project files. If this option is given,
00855 changes of the project file are merged with those of the
00856 corresponding template file if the template has been modified,
00857 for example, as part of a new release of BASIS.""")
00858 
00859     # --cleanup
00860     parser.add_argument('-c', '--cleanup', action='store_true', default=False,
00861             help="""Remove files resulting from merge conflicts of a previous update
00862 of existing project files and backups. The conflicts should have
00863 been manually resolved before using this option.""")
00864 
00865     # --force
00866     parser.add_argument('-f', '--force', action='store_true', default=False,
00867             help="""Enable removal of non-empty directories and modified project files.
00868 By default, only empty directories and project files which have not
00869 been edited since their creation are removed.""")
00870 
00871     # --nobackup
00872     parser.add_argument('--nobackup', action='store_false', default=True, dest='backup',
00873             help="""Disable backup of existing project files. By default, whenever an
00874 existing project file is modified, a backup of this file is made and saved under the
00875 same filename, but with the ~ character as suffix. If this option is given, such backup
00876 files are not made. In case of a merge conflict, however, existing project files are
00877 always backed-up, using .mine as suffix for the file name of the backup in this case.""")
00878 
00879     # ----------------------------------------------------------------------------
00880     # parse command-line arguments
00881     if len(sys.argv) == 1:
00882         parser.print_help()
00883         sys.exit(1)
00884     else:
00885         args = parser.parse_args()
00886 
00887     # ------------------------------------------------------------------------
00888     # create new project or update existing one?
00889     # request to create new project
00890     if args.name:
00891         create = True
00892         # create subdirectory in current working directory by default
00893         if not args.root:
00894             args.root = os.path.join(os.getcwd(), args.name)
00895         # ensure that root directory is empty
00896         if os.path.exists(args.root) and len(os.listdir(args.root)) != 0:
00897             sys.stderr.write("Directory " + args.root + " exists already and is not empty.\n")
00898             sys.stderr.write("Please choose another project name or a different root directory using the --root option.\n")
00899             sys.stderr.write('\n')
00900             sys.stderr.write("If you want to modify an existing project, please specify the root directory\n")
00901             sys.stderr.write("of the existing project using the --root option and remove the --name option.\n")
00902             sys.exit(1)
00903         # require a project description
00904         if not args.description:
00905             sys.stderr.write("No project description given!\n")
00906             sys.exit(1)
00907         # set path of original template to current template
00908         args.original_template = args.template
00909         # verify that project name is valid
00910         if not re.match(r'^[a-zA-Z0-9]+$', args.name):
00911             sys.stderr.write("Invalid project name: " + args.name + "\n")
00912             sys.stderr.write("Project name may only consist of alphanumeric characters!\n")
00913             sys.stderr.write("If you are attempting to modify an existent project, check whether the\n")
00914             sys.stderr.write("project name is correctly extracted from the root CMakeLists.txt file.\n")
00915             sys.exit(1)
00916     # request to modify existing project
00917     else:
00918         create = False
00919         # use current working directory if root not specified
00920         if not args.root:
00921             args.root = os.getcwd()
00922         # remove trailing slashes from root
00923         if args.root[-1] == '/' or args.root[-1] == '\\':
00924             args.root = args.root[0:-1]
00925         # check existence of root directory
00926         if not os.path.isdir(args.root):
00927             sys.stderr.write("Project directory " + args.root + " does not exist!\n")
00928             sys.stderr.write("If you want to create a new project, please specify a project name.\n")
00929             sys.exit(1)
00930         # is project a BASIS project?
00931         if not os.path.isfile(os.path.join(args.root, 'BasisProject.cmake')):
00932             sys.stderr.write("Could not find a BasisProject.cmake file in " + args.root +
00933                     "! Are you sure this is a BASIS project?\n")
00934             sys.exit(1)
00935         # notify user that --description option is invalid
00936         if args.description:
00937             sys.stderr.write("Cannot modify description of existing project. Please edit file BasisProject.cmake.\n")
00938             sys.stderr.write("Do not use option --description when attempting to modify an existing project.\n")
00939             sys.exit(1)
00940         # get project name
00941         if not args.name:
00942             args.name = get_project_name(os.path.join(args.root, 'BasisProject.cmake'))
00943             if not args.name:
00944                 sys.stderr.write("Failed to determine project name!\n")
00945                 sys.exit(1)
00946         # get original template directory
00947         if not args.original_template:
00948             original_template_version = get_template_version(os.path.join(args.root, 'CMakeLists.txt'))
00949             if not original_template_version:
00950                 sys.stderr.write("Failed to determine original template version!\n")
00951                 sys.exit(1)
00952             args.original_template = _template + '-' + original_template_version
00953 
00954     # print template and root path
00955     if args.verbose > 1:
00956         print "Project:  " + args.root
00957         print "Template: " + args.template
00958         print "Original: " + args.original_template
00959         print
00960 
00961     # ------------------------------------------------------------------------
00962     # adjust template options
00963     if not hasattr(args, 'opts'):
00964         setattr(args, 'opts', {})
00965     opts = get_default_template_options(args.root, args.opts, create)
00966 
00967     # ------------------------------------------------------------------------
00968     # do not update exiting project if previous conflicts are not resolved
00969     if not create:
00970         for root, dirs, files in os.walk(args.root):
00971             for filename in files:
00972                 if (filename.endswith('.mine')
00973                         or filename.endswith('.template')
00974                         or filename.endswith('~')):
00975                     if args.cleanup:
00976                         file_path = os.path.join(root, filename)
00977                         try:
00978                             os.remove(file_path)
00979                             print "D %s" % file_path
00980                         except Exception, e:
00981                             sys.stderr.write("E %s - failed to remove file" % file_path)
00982                             if args.verbose > 0: sys.stderr.write(': ' + str(e))
00983                             sys.stderr.write('\n')
00984                             ok = False
00985                     elif args.update and not filename.endswith('~'):
00986                         sys.stderr.write(
00987 """Their occurred conflicts when updating the project files before.
00988 
00989 Please resolve these conflicts first by editing the conflicted files, then remove
00990 the *.mine and *.template files or run this program with the --cleanup option.
00991 Once these conflicts have been resolved, try updating again.
00992 
00993 Note that if you want to revert the previous udpate entirely, you should use
00994 the revert functionality of the revision control system that you are hopefully
00995 using to manage your project files. This should also revert the copies of the
00996 template files in the .basis/ subdirectory.
00997 """)
00998                         sys.exit(1)
00999         if args.cleanup and not ok:
01000             sys.stderr.write("\nFailed to cleanup backup files and temporary files\n")
01001             sys.exit(1)
01002 
01003     # ------------------------------------------------------------------------
01004     # create/modify project
01005     acc = AccumulatorForNumbersOfChangesAndErrors()
01006 
01007     try:
01008         # ---------------------------------------------------------------------
01009         # add/remove project files/directories
01010 
01011         # required project files
01012         acc.call(add, 'AUTHORS.txt',        args)
01013         acc.call(add, 'README.txt',         args)
01014         acc.call(add, 'INSTALL.txt',        args)
01015         acc.call(add, 'COPYING.txt',        args)
01016         acc.call(add, 'BasisProject.cmake', args)
01017         acc.call(add, 'CMakeLists.txt',     args)
01018         # additional configuration files
01019         acc.call(addordel, opts.get('config-settings',     None), 'config/Settings.cmake',         args)
01020         acc.call(addordel, opts.get('config-components',   None), 'config/Components.cmake',       args)
01021         acc.call(addordel, opts.get('config-package',      None), 'config/Package.cmake',          args)
01022         acc.call(addordel, opts.get('config-find',         None), 'config/Config.cmake.in',        args)
01023         acc.call(addordel, opts.get('config-find',         None), 'config/ConfigSettings.cmake',   args)
01024         acc.call(addordel, opts.get('config-find_version', None), 'config/ConfigVersion.cmake.in', args)
01025         acc.call(addordel, opts.get('config-script',       None), 'config/ScriptConfig.cmake.in',  args)
01026         acc.call(addordel, opts.get('config-test',         None), 'config/CTestCustom.cmake.in',   args)
01027         acc.call(addordel, opts.get('config-use',          None), 'config/ConfigUse.cmake.in',     args)
01028         # software configuration data
01029         acc.call(addordel, opts.get('data', None), 'data/CMakeLists.txt', args)
01030         # documentation
01031         acc.call(addordel, opts.get('doc', None), 'doc/CMakeLists.txt', args)
01032         # modularization
01033         acc.call(addordel, opts.get('modules', None), 'modules', args, isdir=True)
01034         # source files
01035         acc.call(addordel, opts.get('include', None), 'include', args, isdir=True)
01036         acc.call(addordel, opts.get('src',     None), 'src/CMakeLists.txt', args)
01037         # testing tree
01038         acc.call(addordel, opts.get('test', None), 'CTestConfig.cmake', args)
01039         acc.call(addordel, opts.get('test', None), 'test/CMakeLists.txt', args)
01040         # example
01041         acc.call(addordel, opts.get('example', None), 'example/CMakeLists.txt', args)
01042 
01043     except Exception, e:
01044         sys.stderr.write("Failed to ")
01045         if create: sys.stderr.write("create")
01046         else:      sys.stderr.write("modify")
01047         sys.stderr.write(" project: " + str(e) + '\n')
01048         ok = False
01049 
01050     # ------------------------------------------------------------------------
01051     # done
01052     if ok:
01053         if create:
01054             sys.stdout.write("\nCreated project\n")
01055         else:
01056             if acc.changes > 0:
01057                 sys.stdout.write('\n' + str(acc.changes) + " file")
01058                 if acc.changes > 1: sys.stdout.write('s')
01059                 sys.stdout.write(" added (A), removed (D), or modified (G, C)\n")
01060             elif acc.errors == 0:
01061                 if args.update:
01062                     sys.stdout.write("Project is up to date\n")
01063                 else:
01064                     sys.stdout.write("No project files added, removed, or modified\n")
01065         if acc.errors > 0:
01066             if acc.changes == 0: sys.stderr.write('\n')
01067             sys.stderr.write(str(acc.errors) + " error")
01068             if acc.errors > 1: sys.stderr.write('s')
01069             sys.stderr.write(" encountered\n")
01070         if acc.conflicts > 0:
01071             sys.stderr.write(str(acc.conflicts) + " conflicting change")
01072             if acc.conflicts > 1: sys.stderr.write('s')
01073             sys.stderr.write(" (C) encountered\n")
01074             sys.stderr.write(
01075 """
01076 Resolve conflicts by editing the updated files manually, using the corresonding *.mine
01077 and *.template files as reference. Once a conflict is resolved, remove these files.
01078 """)