1#!/usr/bin/env python2
2"""SeqAn code generation from templates / skeletons.
3
4This module contains code to help the creation of modules, tests, apps etc.
5It can be called directly or imported and the main() function can be called.
6
7It will perform the following replacements:
8
9  %(AUTHOR)s  will be replaced by the author's name, either given on command
10              line or taken from environment variable SEQAN_AUTHOR.
11
12  %(NAME)s    will be replaced by the name of the generated code.
13  %(TITLE)s   will be replaced by the name of the generated, but centered in
14              74 characters, to be used in the file header comment.
15
16  %(YEAR)d    will be replaced by the current year.
17  %(DATE)s    will be replaced by the current date.
18  %(TIME)s    will be replaced by the current time.
19
20  %(HEADER_GUARD)s  will be replaced by the UPPER_CASE_PATH_H_ to the file.
21
22  %(CMAKE_PROJECT_NAME)s  will be replaced by lower_case_path to the target
23                          directory.
24
25  %(CMAKE_PROJECT_PATH)s  will be replaced by the path to the target directory.
26
27Copyright: (c) 2010, Knut Reinert, FU Berlin
28License:   3-clause BSD (see LICENSE)
29"""
30
31from __future__ import with_statement
32
33__author__ = 'Manuel Holtgrewe <manuel.holtgrewe@fu-berlin.de>'
34
35import datetime
36import optparse
37import os
38import os.path
39import sys
40import string
41
42import paths
43
44# Add os.path.relpath if it is not already there, so we can use Python 2.5, too.
45# TODO(holtgrew): This could go into a "compatibility" module.
46if not 'relpath' in dir(os.path):
47    import posixpath
48    from posixpath import curdir, sep, pardir, join
49
50    def relpath(path, start=curdir):
51        """Return a relative version of a path"""
52        if not path:
53            raise ValueError("no path specified")
54        start_list = posixpath.abspath(start).split(sep)
55        path_list = posixpath.abspath(path).split(sep)
56        # Work out how much of the filepath is shared by start and path.
57        i = len(posixpath.commonprefix([start_list, path_list]))
58        rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
59        if not rel_list:
60            return curdir
61        return join(*rel_list)
62    os.path.relpath = relpath
63
64# Length of the header comment.
65HEADER_CENTER_WIDTH = 74
66
67# Fallback for author string if neither given on command line or environment
68# Variable SEQAN_AUTHOR.
69DEFAULT_AUTHOR = 'Your Name <your.email@example.net>'
70
71# Program usage string for command line parser.
72USAGE = """
73Usage: %prog [options] repository NAME
74       %prog [options] [module|test|app|demo|header|lheader] NAME LOCATION
75       %prog [options] app_tests LOCATION
76""".strip()
77
78# Program description, used for command line parser.  Will be wrapped by, though.
79DESCRIPTION = """
80The SeqAn code generator.
81
82The first version ("repository") is to be be called to create your new entries
83below the directory sandbox.  The second version is to be called to create new
84library modules, tests, apps, app tests, and demos inside a sandbox.
85""".strip()
86#"""
87#Example:
88#
89#  %prog repository sandbox/john_doe
90#
91#The second version is to be called to create new library modules, tests, apps,
92#and demos inside a sandbox.  Example:
93#
94#  %prog module my_module sandbox/john_doe
95#
96#This command creates a new library module in sandbox/john_doe/include/seqan.
97#It consists of the directory my_module, the files my_module.h and
98#my_module/my_module_base.h.
99#
100#  %prog test my_module sandbox/john_doe
101#
102#This command creates the tests for module "my_module" in sandbox/john_doe.
103#
104#  %prog app my_app sandbox/john_doe
105#
106#This command creates a new application named my_app in sandbox/john_doe/apps.
107#
108#  %prog demo my_demo sandbox/john_doe
109#
110#This command creates a new demo in sandbox/john_doe/demos.
111#""".strip()
112
113def createDirectory(path, dry_run=False):
114    print 'mkdir(%s)' % path
115    print
116    if not dry_run:
117        if not os.path.exists(path):
118            os.mkdir(path)
119
120def configureFile(target_file, source_file, replacements, dry_run, options):
121    print 'Configuring file.'
122    print '  Source:', source_file
123    print '  Target:', target_file
124    print
125    if os.path.exists(target_file) and not options.force:
126        msg = 'Target file already exists.  Move it away and call the script again.'
127        print >>sys.stderr, msg
128        return 1
129
130    with open(source_file, 'rb') as f:
131        contents = f.read()
132    target_contents = contents % replacements
133    if dry_run:
134        print 'The contents of the target file are:'
135        print '-' * 78
136        print target_contents
137        print '-' * 78
138    else:
139        with open(target_file, 'wb') as f:
140            f.write(target_contents)
141    return 0
142
143def _pathToIdentifier(relative_path):
144    result = relative_path.replace('/', '_')
145    result = result.replace('\\', '_')
146    result = result.replace('-', '_')
147    result = result.replace('.', '_')
148    result = result.replace(' ', '_')
149    return result
150
151def buildReplacements(type_, name, location, target_file, options):
152    result = {}
153    result['AUTHOR'] = options.author
154    result['YEAR'] = datetime.date.today().year
155    result['TIME'] = datetime.datetime.now().strftime('%H:%M')
156    result['DATE'] = datetime.date.today().strftime('%Y-%m-%d')
157    result['NAME'] = name
158    result['TITLE'] = name.center(HEADER_CENTER_WIDTH).rstrip()
159    path = os.path.relpath(target_file, paths.repositoryRoot())
160    guard = _pathToIdentifier(path).upper()
161    result['HEADER_GUARD'] = guard + '_'
162    path = os.path.relpath(os.path.dirname(target_file),
163                           paths.repositoryRoot())
164    cmake_project_name = _pathToIdentifier(path)
165    result['CMAKE_PROJECT_NAME'] = cmake_project_name
166    result['CMAKE_PROJECT_PATH'] = path.replace('\\', '\\\\')
167    if type_ == 'repository':
168        result['REPOSITORY_PSEUDO_TARGET_NAME'] = name.replace('/', '_').replace('\\', '_').replace(' ', '_')
169    if type_ == 'app_tests':
170        result['APP_NAME'] = os.path.split(os.path.split(location)[0])[1]
171        result['APP_NAME_U'] = result['APP_NAME'].upper()
172        result['LOCATION'] = os.path.join(os.path.split(os.path.normpath(location))[0])
173    return result
174
175def _checkTargetPaths(target_path, options):
176    """Check that the path does not exist but its parent does."""
177    # Check that the given path does not exist yet.
178    if os.path.exists(target_path) and not options.force:
179        msg = 'The path %s already exists. Move it and call this script again.'
180        print >>sys.stderr, msg % target_path
181        return False
182    # Check that the parent path already exists.
183    if not os.path.exists(os.path.dirname(target_path)):
184        msg = 'The parent of the target path does not exist yet: %s'
185        print >>sys.stderr, msg % os.path.dirname(target_path)
186        print >>sys.stderr, 'Please create it and call this script again.'
187        return False
188    return True
189
190def createModule(name, location, options):
191    include_path = paths.pathToInclude(location)
192    seqan_path = os.path.join(include_path, 'seqan')
193    module_path = os.path.join(seqan_path, name)
194    header_path = os.path.join(seqan_path, '%s.h' % name)
195    print 'Creating module in %s' % module_path
196    if options.create_dirs and not _checkTargetPaths(module_path, options):
197        return 1
198    if options.create_dirs and not _checkTargetPaths(header_path, options):
199        return 1
200    print '  Module path is: %s' % module_path
201    print '  Module header path is: %s' % header_path
202    print ''
203    if options.create_dirs:
204        # Create directory.
205        createDirectory(module_path, options.dry_run)
206    if options.create_programs:
207        # Copy over module header.
208        source_file = paths.pathToTemplate('module_template', 'module.h')
209        target_file = header_path
210        replacements = buildReplacements('module', name, seqan_path, target_file, options)
211        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
212        if res: return res
213        # Copy over header inside module.
214        source_file = paths.pathToTemplate('module_template', 'header.h')
215        target_file = os.path.join(module_path, '%s_base.h' % name)
216        replacements = buildReplacements('module', name, seqan_path, target_file, options)
217        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
218        if res: return res
219    return 0
220
221def createTest(name, location, options):
222    target_path = paths.pathToTest(location, name)
223    print 'Creating test in %s' % target_path
224    if options.create_dirs and not _checkTargetPaths(target_path, options):
225        return 1
226    print '  Target path is: %s' % target_path
227    print ''
228    if options.create_dirs:
229        # Create directory.
230        createDirectory(target_path, options.dry_run)
231    if options.create_programs:
232        # Copy over .cpp file for test and perform replacements.
233        source_file = paths.pathToTemplate('test_template', 'test.cpp')
234        target_file = os.path.join(target_path, 'test_%s.cpp' % name)
235        replacements = buildReplacements('test', name, location, target_file, options)
236        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
237        if res: return res
238        # Copy over .h file for test and perform replacements.
239        source_file = paths.pathToTemplate('test_template', 'test.h')
240        target_file = os.path.join(target_path, 'test_%s.h' % name)
241        replacements = buildReplacements('test', name, location, target_file, options)
242        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
243        if res: return res
244    if options.create_cmakelists:
245        # Copy over CMakeLists.txt file for test and perform replacements.
246        source_file = paths.pathToTemplate('test_template', 'CMakeLists.txt')
247        target_file = os.path.join(target_path, 'CMakeLists.txt')
248        replacements = buildReplacements('test', name, location, target_file, options)
249        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
250        if res: return res
251    return 0
252
253def createApp(name, location, options):
254    target_path = paths.pathToApp(location, name)
255    print 'Creating app in %s' % target_path
256    if options.create_dirs and not _checkTargetPaths(target_path, options):
257        return 1
258    print '  Target path is: %s' % target_path
259    print ''
260    if options.create_programs:
261        # Create directory.
262        createDirectory(target_path, options.dry_run)
263        # Copy over .cpp file for app and perform replacements.
264        source_file = paths.pathToTemplate('app_template', 'app.cpp')
265        target_file = os.path.join(target_path, '%s.cpp' % name)
266        replacements = buildReplacements('app', name, location, target_file, options)
267        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
268        if res: return res
269    if options.create_cmakelists:
270        # Copy over CMakeLists.txt file for app and perform replacements.
271        source_file = paths.pathToTemplate('app_template', 'CMakeLists.txt')
272        target_file = os.path.join(target_path, 'CMakeLists.txt')
273        replacements = buildReplacements('app', name, location, target_file, options)
274        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
275        if res: return res
276    return 0
277
278def createDemo(name, location, options):
279    target_path = paths.pathToDemo(location, name)
280    print 'Creating demo in %s' % target_path
281    if options.create_dirs and not _checkTargetPaths(target_path, options):
282        return 1
283    print '  Target path is: %s' % target_path
284    print ''
285    if options.create_programs:
286        # Copy over .cpp file for app and perform replacements.
287        source_file = paths.pathToTemplate('demo_template', 'demo.cpp')
288        target_file = os.path.join(target_path)
289        replacements = buildReplacements('demo', name, location, target_file, options)
290        res = configureFile(target_file, source_file, replacements, options.dry_run, options)
291        if res: return res
292    return 0
293
294def createHeader(name, location, options):
295    target_path = paths.pathToHeader(location, name)
296    print 'Creating (non-library) header in %s' % target_path
297    if not _checkTargetPaths(target_path, options):
298        return 1
299    print '  Target path is: %s' % target_path
300    print ''
301    # Copy over .h file for app and perform replacements.
302    source_file = paths.pathToTemplate('header_template', 'header.h')
303    target_file = os.path.join(target_path)
304    replacements = buildReplacements('header', name, location, target_file, options)
305    res = configureFile(target_file, source_file, replacements, options.dry_run, options)
306    if res: return res
307    print 'NOTE: Do not forget to add the header to the CMakeLists.txt file!'
308    return 0
309
310def createLibraryHeader(name, location, options):
311    target_path = paths.pathToHeader(location, name)
312    print 'Creating library header in %s' % target_path
313    if not _checkTargetPaths(target_path, options):
314        return 1
315    print '  Target path is: %s' % target_path
316    print ''
317    # Copy over .h file for app and perform replacements.
318    source_file = paths.pathToTemplate('header_template', 'library_header.h')
319    target_file = os.path.join(target_path)
320    replacements = buildReplacements('library_header', name, location, target_file, options)
321    res = configureFile(target_file, source_file, replacements, options.dry_run, options)
322    if res: return res
323    return 0
324
325def createRepository(location, options):
326    print 'Creating module %s' % location
327    target_path = paths.pathToRepository(location)
328    if options.create_dirs and not _checkTargetPaths(target_path, options):
329        return 1
330    print '  Target path is: %s' % target_path
331    print ''
332    if options.create_dirs:
333        # Create directories.
334        createDirectory(target_path, options.dry_run)
335        createDirectory(os.path.join(target_path, 'apps'), options.dry_run)
336        createDirectory(os.path.join(target_path, 'demos'), options.dry_run)
337        createDirectory(os.path.join(target_path, 'include'), options.dry_run)
338        createDirectory(os.path.join(target_path, 'include', 'seqan'), options.dry_run)
339        createDirectory(os.path.join(target_path, 'tests'), options.dry_run)
340    if options.create_cmakelists:
341        # Copy over file ${REPOSITORY}/CMakeLists.txt.
342        target_file = os.path.join(target_path, 'CMakeLists.txt')
343        source_file = paths.pathToTemplate('repository_template', 'CMakeLists.txt')
344        replacements = buildReplacements('repository', location, target_path, target_file, options)
345        configureFile(target_file, source_file, replacements, options.dry_run, options)
346        # Copy over file ${REPOSITORY}/apps/CMakeLists.txt.
347        target_file = os.path.join(target_path, 'apps', 'CMakeLists.txt')
348        source_file = paths.pathToTemplate('repository_template', 'apps_CMakeLists.txt')
349        replacements = buildReplacements('repository', location, target_path, target_file, options)
350        configureFile(target_file, source_file, replacements, options.dry_run, options)
351        # Copy over file ${REPOSITORY}/tests/CMakeLists.txt.
352        target_file = os.path.join(target_path, 'tests', 'CMakeLists.txt')
353        source_file = paths.pathToTemplate('repository_template', 'tests_CMakeLists.txt')
354        replacements = buildReplacements('repository', location, target_path, target_file, options)
355        configureFile(target_file, source_file, replacements, options.dry_run, options)
356        # Copy over file ${REPOSITORY}/demos/CMakeLists.txt.
357        target_file = os.path.join(target_path, 'demos', 'CMakeLists.txt')
358        source_file = paths.pathToTemplate('repository_template', 'demos_CMakeLists.txt')
359        replacements = buildReplacements('repository', location, target_path, target_file, options)
360        configureFile(target_file, source_file, replacements, options.dry_run, options)
361    return 0
362
363def createAppTests(location, options):
364    print 'Creating app tests at %s' % location
365    tests_location = os.path.join(location, 'tests')
366    target_path = paths.pathToRepository(tests_location)
367    if options.create_dirs and not _checkTargetPaths(target_path, options):
368        return 1
369    print '  Target path is: %s' % target_path
370    print ''
371
372    # Create directories.
373    if options.create_dirs:
374        createDirectory(target_path, options.dry_run)
375
376    # Copy over file ${APP}/tests/generate_outputs.sh
377    target_file = os.path.join(target_path, 'generate_outputs.sh')
378    source_file = paths.pathToTemplate('app_tests_template', 'generate_outputs.sh')
379    replacements = buildReplacements('app_tests', location, target_path, target_file, options)
380    configureFile(target_file, source_file, replacements, options.dry_run, options)
381    # Copy over file ${APP}/tests/run_tests.py
382    target_file = os.path.join(target_path, 'run_tests.py')
383    source_file = paths.pathToTemplate('app_tests_template', 'run_tests.py')
384    replacements = buildReplacements('app_tests', location, target_path, target_file, options)
385    configureFile(target_file, source_file, replacements, options.dry_run, options)
386
387    print '=' * 80
388    print 'Do not forget to add the tests in %s:' % os.path.join(location, 'CMakeLists.txt')
389    print ''
390    print '# Add app tests if Python interpreter could be found.'
391    print 'if(PYTHONINTERP_FOUND)'
392    print '  add_test(NAME app_test_%s COMMAND ${PYTHON_EXECUTABLE}' % os.path.split(location)[-1]
393    print '    ${CMAKE_CURRENT_SOURCE_DIR}/tests/run_tests.py ${CMAKE_SOURCE_DIR}'
394    print '    ${CMAKE_BINARY_DIR})'
395    print 'endif(PYTHONINTERP_FOUND)'
396    print '=' * 80
397
398    return 0
399
400def main():
401    # Parse arguments.
402    parser = optparse.OptionParser(usage=USAGE, description=DESCRIPTION)
403    parser.add_option('-s', '--skel-root', dest='skel_root',
404                      help=('Set path to the directory where the skeletons '
405                            'live in.  Taken from environment variable '
406                            'SEQAN_SKELS if available.'),
407                      default=os.environ.get('SEQAN_SKELS',
408                                             paths.pathToSkeletons()))
409    parser.add_option('-a', '--author', dest='author',
410                      help=('Set author to use.  Should have the format USER '
411                            '<EMAIL>.  Taken from environment variable '
412                            'SEQAN_AUTHOR if it exists.'),
413                      default=os.environ.get('SEQAN_AUTHOR', DEFAULT_AUTHOR))
414    parser.add_option('-d', '--dry-run', dest='dry_run', action='store_true',
415                      help='Do not change anything, just simulate.',
416                      default=False)
417    parser.add_option('-c', '--cmakelists-only', dest='cmakelists_only',
418                      action='store_true',
419                      help='Only create CMakeLists.txt files',
420                      default=False)
421    parser.add_option('--force', dest='force', action='store_true',
422                      help='Overwrite existing files and directories.',
423                      default=False)
424    options, args = parser.parse_args()
425    options.create_cmakelists = True
426    options.create_infos = True
427    options.create_dirs = True
428    options.create_programs = True
429    if options.cmakelists_only:
430        options.create_dirs = False
431        options.create_programs = False
432
433    if not args:
434        parser.print_help(file=sys.stderr)
435        return 1
436    if len(args) < 2:
437        print >>sys.stderr, 'Invalid argument count!'
438        return 1
439    if args[0] not in ['module', 'test', 'app', 'demo', 'repository',
440                       'header', 'lheader', 'app_tests']:
441        print >>sys.stderr, 'Invalid template "%s".' % args[0]
442        return 1
443    if args[0] in['repository', 'app_tests']:
444        if len(args) != 2:
445            print >>sys.stderr, 'Invalid argument count!'
446            return 1
447
448    if args[0] == 'repository':
449        return createRepository(args[1], options)
450    elif args[0] == 'app_tests':
451        return createAppTests(args[1], options)
452    elif len(args) != 3:
453        print >>sys.stderr, 'Invalid argument count!'
454        return 1
455    create_methods = {
456        'module' : createModule,
457        'test': createTest,
458        'app': createApp,
459        'demo': createDemo,
460        'header': createHeader,
461        'lheader': createLibraryHeader,
462        }
463    return create_methods[args[0]](args[1], args[2], options)
464
465if __name__ == '__main__':
466   sys.exit(main())
467
468