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