1# Copyright 2015, VIXL authors
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are met:
6#
7#   * Redistributions of source code must retain the above copyright notice,
8#     this list of conditions and the following disclaimer.
9#   * Redistributions in binary form must reproduce the above copyright notice,
10#     this list of conditions and the following disclaimer in the documentation
11#     and/or other materials provided with the distribution.
12#   * Neither the name of ARM Limited nor the names of its contributors may be
13#     used to endorse or promote products derived from this software without
14#     specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND
17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27import glob
28import itertools
29import os
30from os.path import join
31import platform
32import subprocess
33import sys
34from collections import OrderedDict
35
36root_dir = os.path.dirname(File('SConstruct').rfile().abspath)
37sys.path.insert(0, join(root_dir, 'tools'))
38import config
39import util
40
41from SCons.Errors import UserError
42
43
44Help('''
45Build system for the VIXL project.
46See README.md for documentation and details about the build system.
47''')
48
49
50# We track top-level targets to automatically generate help and alias them.
51class VIXLTargets:
52  def __init__(self):
53    self.targets = []
54    self.help_messages = []
55  def Add(self, target, help_message):
56    self.targets.append(target)
57    self.help_messages.append(help_message)
58  def Help(self):
59    res = ""
60    for i in range(len(self.targets)):
61      res += '\t{0:<{1}}{2:<{3}}\n'.format(
62        'scons ' + self.targets[i],
63        len('scons ') + max(map(len, self.targets)),
64        ' : ' + self.help_messages[i],
65        len(' : ') + max(map(len, self.help_messages)))
66    return res
67
68top_level_targets = VIXLTargets()
69
70
71
72# Build options ----------------------------------------------------------------
73
74# Store all the options in a dictionary.
75# The SConstruct will check the build variables and construct the build
76# environment as appropriate.
77options = {
78    'all' : { # Unconditionally processed.
79      'CCFLAGS' : ['-Wall',
80                   '-Werror',
81                   '-fdiagnostics-show-option',
82                   '-Wextra',
83                   '-Wredundant-decls',
84                   '-pedantic',
85                   '-Wwrite-strings',
86                   '-Wunused'],
87      'CPPPATH' : [config.dir_src_vixl]
88      },
89#   'build_option:value' : {
90#     'environment_key' : 'values to append'
91#     },
92    'mode:debug' : {
93      'CCFLAGS' : ['-DVIXL_DEBUG', '-O0']
94      },
95    'mode:release' : {
96      'CCFLAGS' : ['-O3'],
97      },
98    'simulator:aarch64' : {
99      'CCFLAGS' : ['-DVIXL_INCLUDE_SIMULATOR_AARCH64'],
100      },
101    'symbols:on' : {
102      'CCFLAGS' : ['-g'],
103      'LINKFLAGS' : ['-g']
104      },
105    'negative_testing:on' : {
106      'CCFLAGS' : ['-DVIXL_NEGATIVE_TESTING']
107      },
108    'code_buffer_allocator:mmap' : {
109      'CCFLAGS' : ['-DVIXL_CODE_BUFFER_MMAP']
110      },
111    'code_buffer_allocator:malloc' : {
112      'CCFLAGS' : ['-DVIXL_CODE_BUFFER_MALLOC']
113      }
114    }
115
116
117# A `DefaultVariable` has a default value that depends on elements not known
118# when variables are first evaluated.
119# Each `DefaultVariable` has a handler that will compute the default value for
120# the given environment.
121def modifiable_flags_handler(env):
122  env['modifiable_flags'] = \
123      'on' if 'mode' in env and env['mode'] == 'debug' else 'off'
124
125
126def symbols_handler(env):
127  env['symbols'] = 'on' if 'mode' in env and env['mode'] == 'debug' else 'off'
128
129def Is32BitHost(env):
130  return env['host_arch'] in ['aarch32', 'i386']
131
132def IsAArch64Host(env):
133  return env['host_arch'] == 'aarch64'
134
135def CanTargetA32(env):
136  return 'a32' in env['target']
137
138def CanTargetT32(env):
139  return 't32' in env['target']
140
141def CanTargetAArch32(env):
142  return CanTargetA32(env) or CanTargetT32(env)
143
144def CanTargetA64(env):
145  return 'a64' in env['target']
146
147def CanTargetAArch64(env):
148  return CanTargetA64(env)
149
150
151# By default, include the simulator only if AArch64 is targeted and we are not
152# building VIXL natively for AArch64.
153def simulator_handler(env):
154  if not IsAArch64Host(env) and CanTargetAArch64(env):
155    env['simulator'] = 'aarch64'
156  else:
157    env['simulator'] = 'none'
158
159
160# 'mmap' is required for use with 'mprotect', which is needed for the tests
161# (when running natively), so we use it by default where we can.
162def code_buffer_allocator_handler(env):
163  directives = util.GetCompilerDirectives(env)
164  if '__linux__' in directives:
165    env['code_buffer_allocator'] = 'mmap'
166  else:
167    env['code_buffer_allocator'] = 'malloc'
168
169# A validator checks the consistency of provided options against the environment.
170def default_validator(env):
171  pass
172
173
174def simulator_validator(env):
175  if env['simulator'] == 'aarch64' and not CanTargetAArch64(env):
176    raise UserError('Building an AArch64 simulator implies that VIXL targets '
177                    'AArch64. Set `target` to include `aarch64` or `a64`.')
178
179
180# Default variables may depend on each other, therefore we need this dictionnary
181# to be ordered.
182vars_default_handlers = OrderedDict({
183    # variable_name    : [ 'default val', 'handler', 'validator']
184    'symbols'          : [ 'mode==debug', symbols_handler, default_validator ],
185    'modifiable_flags' : [ 'mode==debug', modifiable_flags_handler, default_validator],
186    'simulator'        : [ 'on if the target architectures include AArch64 but '
187                           'the host is not AArch64, else off',
188                           simulator_handler, simulator_validator ],
189    'code_buffer_allocator' : [ 'mmap with __linux__, malloc otherwise',
190                                code_buffer_allocator_handler, default_validator ]
191    })
192
193
194def DefaultVariable(name, help, allowed_values):
195  help = '%s (%s)' % (help, '|'.join(allowed_values))
196  default_value = vars_default_handlers[name][0]
197  def validator(name, value, env):
198    if value != default_value and value not in allowed_values:
199        raise UserError('Invalid value for option {name}: {value}.  '
200                        'Valid values are: {allowed_values}'.format(
201                            name, value, allowed_values))
202  return (name, help, default_value, validator)
203
204
205def AliasedListVariable(name, help, default_value, allowed_values, aliasing):
206  help = '%s (all|auto|comma-separated list) (any combination from [%s])' % \
207         (help, ', '.join(allowed_values))
208
209  def validator(name, value, env):
210    # Here list has been converted to space separated strings.
211    if value == '': return  # auto
212    for v in value.split():
213      if v not in allowed_values:
214        raise UserError('Invalid value for %s: %s' % (name, value))
215
216  def converter(value):
217    if value == 'auto': return []
218    if value == 'all':
219      translated = [aliasing[v] for v in allowed_values]
220      return list(set(itertools.chain.from_iterable(translated)))
221    # The validator is run later hence the get.
222    translated = [aliasing.get(v, v) for v in value.split(',')]
223    return list(set(itertools.chain.from_iterable(translated)))
224
225  return (name, help, default_value, validator, converter)
226
227
228vars = Variables()
229# Define command line build options.
230vars.AddVariables(
231    AliasedListVariable('target', 'Target ISA/Architecture', 'auto',
232                        ['aarch32', 'a32', 't32', 'aarch64', 'a64'],
233                        {'aarch32' : ['a32', 't32'],
234                         'a32' : ['a32'], 't32' : ['t32'],
235                         'aarch64' : ['a64'], 'a64' : ['a64']}),
236    EnumVariable('mode', 'Build mode',
237                 'release', allowed_values=config.build_options_modes),
238    EnumVariable('negative_testing',
239                  'Enable negative testing (needs exceptions)',
240                 'off', allowed_values=['on', 'off']),
241    DefaultVariable('symbols', 'Include debugging symbols in the binaries',
242                    ['on', 'off']),
243    DefaultVariable('simulator', 'Simulators to include', ['aarch64', 'none']),
244    DefaultVariable('code_buffer_allocator',
245                    'Configure the allocation mechanism in the CodeBuffer',
246                    ['malloc', 'mmap']),
247    ('std', 'C++ standard. The standards tested are: %s.' % \
248                                         ', '.join(config.tested_cpp_standards))
249    )
250
251# We use 'variant directories' to avoid recompiling multiple times when build
252# options are changed, different build paths are used depending on the options
253# set. These are the options that should be reflected in the build directory
254# path.
255options_influencing_build_path = [
256  'target', 'mode', 'symbols', 'CXX', 'std', 'simulator', 'negative_testing',
257  'code_buffer_allocator'
258]
259
260
261
262# Build helpers ----------------------------------------------------------------
263
264def RetrieveEnvironmentVariables(env):
265  for key in ['CC', 'CXX', 'AR', 'RANLIB', 'LD']:
266    if os.getenv(key): env[key] = os.getenv(key)
267  if os.getenv('LD_LIBRARY_PATH'): env['LIBPATH'] = os.getenv('LD_LIBRARY_PATH')
268  if os.getenv('CCFLAGS'):
269    env.Append(CCFLAGS = os.getenv('CCFLAGS').split())
270  if os.getenv('CXXFLAGS'):
271    env.Append(CXXFLAGS = os.getenv('CXXFLAGS').split())
272  if os.getenv('LINKFLAGS'):
273    env.Append(LINKFLAGS = os.getenv('LINKFLAGS').split())
274  # This allows colors to be displayed when using with clang.
275  env['ENV']['TERM'] = os.getenv('TERM')
276
277
278# The architecture targeted by default will depend on the compiler being
279# used. 'host_arch' is extracted from the compiler while 'target' can be
280# set by the user.
281# By default, we target both AArch32 and AArch64 unless the compiler targets a
282# 32-bit architecture. At the moment, we cannot build VIXL's AArch64 support on
283# a 32-bit platform.
284# TODO: Port VIXL to build on a 32-bit platform.
285def target_handler(env):
286  # Auto detect
287  if Is32BitHost(env):
288    # We use list(set(...)) to keep the same order as if it was specify as
289    # an option.
290    env['target'] = list(set(['a32', 't32']))
291  else:
292    env['target'] = list(set(['a64', 'a32', 't32']))
293
294
295def target_validator(env):
296  # TODO: Port VIXL64 to work on a 32-bit platform.
297  if Is32BitHost(env) and CanTargetAArch64(env):
298    raise UserError('Building VIXL for AArch64 in 32-bit is not supported. Set '
299                    '`target` to `aarch32`')
300
301
302# The target option is handled differently from the rest.
303def ProcessTargetOption(env):
304  if env['target'] == []: target_handler(env)
305
306  if 'a32' in env['target']: env['CCFLAGS'] += ['-DVIXL_INCLUDE_TARGET_A32']
307  if 't32' in env['target']: env['CCFLAGS'] += ['-DVIXL_INCLUDE_TARGET_T32']
308  if 'a64' in env['target']: env['CCFLAGS'] += ['-DVIXL_INCLUDE_TARGET_A64']
309
310  target_validator(env)
311
312
313def ProcessBuildOptions(env):
314  # 'all' is unconditionally processed.
315  if 'all' in options:
316    for var in options['all']:
317      if var in env and env[var]:
318        env[var] += options['all'][var]
319      else:
320        env[var] = options['all'][var]
321
322  # The target option *must* be processed before the options defined in
323  # vars_default_handlers.
324  ProcessTargetOption(env)
325
326  # Other build options must match 'option:value'
327  env_dict = env.Dictionary()
328
329  # First apply the default variables handlers in order.
330  for key, value in vars_default_handlers.items():
331    default = value[0]
332    handler = value[1]
333    if env_dict.get(key) == default:
334      handler(env_dict)
335
336  # Second, run the series of validators, to check for errors.
337  for _, value in vars_default_handlers.items():
338    validator = value[2]
339    validator(env)
340
341  for key in env_dict.keys():
342    # Then update the environment according to the value of the variable.
343    key_val_couple = key + ':%s' % env_dict[key]
344    if key_val_couple in options:
345      for var in options[key_val_couple]:
346        env[var] += options[key_val_couple][var]
347
348
349def ConfigureEnvironmentForCompiler(env):
350  if CanTargetA32(env) and CanTargetT32(env):
351    # When building for only one aarch32 isa, fixing the no-return is not worth
352    # the effort.
353    env.Append(CPPFLAGS = ['-Wmissing-noreturn'])
354
355  compiler = util.CompilerInformation(env)
356  if compiler == 'clang':
357    # These warnings only work for Clang.
358    # -Wimplicit-fallthrough only works when compiling the code base as C++11 or
359    # newer. The compiler does not complain if the option is passed when
360    # compiling earlier C++ standards.
361    env.Append(CPPFLAGS = ['-Wimplicit-fallthrough', '-Wshorten-64-to-32'])
362
363    # The '-Wunreachable-code' flag breaks builds for clang 3.4.
364    if compiler != 'clang-3.4':
365      env.Append(CPPFLAGS = ['-Wunreachable-code'])
366
367  # GCC 4.8 has a bug which produces a warning saying that an anonymous Operand
368  # object might be used uninitialized:
369  #   http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57045
370  # The bug does not seem to appear in GCC 4.7, or in debug builds with GCC 4.8.
371  if env['mode'] == 'release':
372    if compiler == 'gcc-4.8':
373      env.Append(CPPFLAGS = ['-Wno-maybe-uninitialized'])
374
375  # GCC 6 and higher is able to detect throwing from inside a destructor and
376  # reports a warning. However, if negative testing is enabled then assertions
377  # will throw exceptions.
378  if env['negative_testing'] == 'on' and env['mode'] == 'debug' \
379      and compiler >= 'gcc-6':
380    env.Append(CPPFLAGS = ['-Wno-terminate'])
381    # The C++11 compatibility warning will also be triggered for this case, as
382    # the behavior of throwing from desctructors has changed.
383    if 'std' in env and env['std'] == 'c++98':
384      env.Append(CPPFLAGS = ['-Wno-c++11-compat'])
385
386  # When compiling with c++98 (the default), allow long long constants.
387  if 'std' not in env or env['std'] == 'c++98':
388    env.Append(CPPFLAGS = ['-Wno-long-long'])
389  # When compiling with c++11, suggest missing override keywords on methods.
390  if 'std' in env and env['std'] in ['c++11', 'c++14']:
391    if compiler >= 'gcc-5':
392      env.Append(CPPFLAGS = ['-Wsuggest-override'])
393    elif compiler >= 'clang-3.6':
394      env.Append(CPPFLAGS = ['-Winconsistent-missing-override'])
395
396
397def ConfigureEnvironment(env):
398  RetrieveEnvironmentVariables(env)
399  env['host_arch'] = util.GetHostArch(env)
400  ProcessBuildOptions(env)
401  if 'std' in env:
402    env.Append(CPPFLAGS = ['-std=' + env['std']])
403    std_path = env['std']
404  ConfigureEnvironmentForCompiler(env)
405
406
407def TargetBuildDir(env):
408  # Build-time option values are embedded in the build path to avoid requiring a
409  # full build when an option changes.
410  build_dir = config.dir_build
411  for option in options_influencing_build_path:
412    option_value = ''.join(env[option]) if option in env else ''
413    build_dir = join(build_dir, option + '_'+ option_value)
414  return build_dir
415
416
417def PrepareVariantDir(location, build_dir):
418  location_build_dir = join(build_dir, location)
419  VariantDir(location_build_dir, location)
420  return location_build_dir
421
422
423def VIXLLibraryTarget(env):
424  build_dir = TargetBuildDir(env)
425  # Create a link to the latest build directory.
426  # Use `-r` to avoid failure when `latest` exists and is a directory.
427  subprocess.check_call(["rm", "-rf", config.dir_build_latest])
428  util.ensure_dir(build_dir)
429  subprocess.check_call(["ln", "-s", build_dir, config.dir_build_latest])
430  # Source files are in `src` and in `src/aarch64/`.
431  variant_dir_vixl = PrepareVariantDir(join('src'), build_dir)
432  sources = [Glob(join(variant_dir_vixl, '*.cc'))]
433  if CanTargetAArch32(env):
434    variant_dir_aarch32 = PrepareVariantDir(join('src', 'aarch32'), build_dir)
435    sources.append(Glob(join(variant_dir_aarch32, '*.cc')))
436  if CanTargetAArch64(env):
437    variant_dir_aarch64 = PrepareVariantDir(join('src', 'aarch64'), build_dir)
438    sources.append(Glob(join(variant_dir_aarch64, '*.cc')))
439  return env.Library(join(build_dir, 'vixl'), sources)
440
441
442
443# Build ------------------------------------------------------------------------
444
445# The VIXL library, built by default.
446env = Environment(variables = vars,
447                  BUILDERS = {
448                      'Markdown': Builder(action = 'markdown $SOURCE > $TARGET',
449                                          suffix = '.html')
450                  })
451# Abort the build if any command line option is unknown or invalid.
452unknown_build_options = vars.UnknownVariables()
453if unknown_build_options:
454  print 'Unknown build options:',  unknown_build_options.keys()
455  Exit(1)
456
457ConfigureEnvironment(env)
458Help(vars.GenerateHelpText(env))
459libvixl = VIXLLibraryTarget(env)
460Default(libvixl)
461env.Alias('libvixl', libvixl)
462top_level_targets.Add('', 'Build the VIXL library.')
463
464
465# Common test code.
466test_build_dir = PrepareVariantDir('test', TargetBuildDir(env))
467test_objects = [env.Object(Glob(join(test_build_dir, '*.cc')))]
468
469# AArch32 support
470if CanTargetAArch32(env):
471  # The examples.
472  aarch32_example_names = util.ListCCFilesWithoutExt(config.dir_aarch32_examples)
473  aarch32_examples_build_dir = PrepareVariantDir('examples/aarch32', TargetBuildDir(env))
474  aarch32_example_targets = []
475  for example in aarch32_example_names:
476    prog = env.Program(join(aarch32_examples_build_dir, example),
477                       join(aarch32_examples_build_dir, example + '.cc'),
478                       LIBS=[libvixl])
479    aarch32_example_targets.append(prog)
480  env.Alias('aarch32_examples', aarch32_example_targets)
481  top_level_targets.Add('aarch32_examples', 'Build the examples for AArch32.')
482
483  # The benchmarks
484  aarch32_benchmark_names = util.ListCCFilesWithoutExt(config.dir_aarch32_benchmarks)
485  aarch32_benchmarks_build_dir = PrepareVariantDir('benchmarks/aarch32', TargetBuildDir(env))
486  aarch32_benchmark_targets = []
487  for bench in aarch32_benchmark_names:
488    prog = env.Program(join(aarch32_benchmarks_build_dir, bench),
489                       join(aarch32_benchmarks_build_dir, bench + '.cc'),
490                       LIBS=[libvixl])
491    aarch32_benchmark_targets.append(prog)
492  env.Alias('aarch32_benchmarks', aarch32_benchmark_targets)
493  top_level_targets.Add('aarch32_benchmarks', 'Build the benchmarks for AArch32.')
494
495  # The tests.
496  test_aarch32_build_dir = PrepareVariantDir(join('test', 'aarch32'), TargetBuildDir(env))
497  test_objects.append(env.Object(
498      Glob(join(test_aarch32_build_dir, '*.cc')),
499      CPPPATH = env['CPPPATH'] + [config.dir_tests]))
500
501# AArch64 support
502if CanTargetAArch64(env):
503  # The benchmarks.
504  aarch64_benchmark_names = util.ListCCFilesWithoutExt(config.dir_aarch64_benchmarks)
505  aarch64_benchmarks_build_dir = PrepareVariantDir('benchmarks/aarch64', TargetBuildDir(env))
506  aarch64_benchmark_targets = []
507  for bench in aarch64_benchmark_names:
508    prog = env.Program(join(aarch64_benchmarks_build_dir, bench),
509                       join(aarch64_benchmarks_build_dir, bench + '.cc'),
510                       LIBS=[libvixl])
511    aarch64_benchmark_targets.append(prog)
512  env.Alias('aarch64_benchmarks', aarch64_benchmark_targets)
513  top_level_targets.Add('aarch64_benchmarks', 'Build the benchmarks for AArch64.')
514
515  # The examples.
516  aarch64_example_names = util.ListCCFilesWithoutExt(config.dir_aarch64_examples)
517  aarch64_examples_build_dir = PrepareVariantDir('examples/aarch64', TargetBuildDir(env))
518  aarch64_example_targets = []
519  for example in aarch64_example_names:
520    prog = env.Program(join(aarch64_examples_build_dir, example),
521                       join(aarch64_examples_build_dir, example + '.cc'),
522                       LIBS=[libvixl])
523    aarch64_example_targets.append(prog)
524  env.Alias('aarch64_examples', aarch64_example_targets)
525  top_level_targets.Add('aarch64_examples', 'Build the examples for AArch64.')
526
527  # The tests.
528  test_aarch64_build_dir = PrepareVariantDir(join('test', 'aarch64'), TargetBuildDir(env))
529  test_objects.append(env.Object(
530      Glob(join(test_aarch64_build_dir, '*.cc')),
531      CPPPATH = env['CPPPATH'] + [config.dir_tests]))
532
533  # The test requires building the example files with specific options, so we
534  # create a separate variant dir for the example objects built this way.
535  test_aarch64_examples_vdir = join(TargetBuildDir(env), 'test', 'aarch64', 'test_examples')
536  VariantDir(test_aarch64_examples_vdir, '.')
537  test_aarch64_examples_obj = env.Object(
538      [Glob(join(test_aarch64_examples_vdir, join('test', 'aarch64', 'examples/aarch64', '*.cc'))),
539       Glob(join(test_aarch64_examples_vdir, join('examples/aarch64', '*.cc')))],
540      CCFLAGS = env['CCFLAGS'] + ['-DTEST_EXAMPLES'],
541      CPPPATH = env['CPPPATH'] + [config.dir_aarch64_examples] + [config.dir_tests])
542  test_objects.append(test_aarch64_examples_obj)
543
544test = env.Program(join(test_build_dir, 'test-runner'), test_objects,
545                   LIBS=[libvixl])
546env.Alias('tests', test)
547top_level_targets.Add('tests', 'Build the tests.')
548
549
550env.Alias('all', top_level_targets.targets)
551top_level_targets.Add('all', 'Build all the targets above.')
552
553Help('\n\nAvailable top level targets:\n' + top_level_targets.Help())
554
555extra_targets = VIXLTargets()
556
557# Build documentation
558doc = [
559    env.Markdown('README.md'),
560    env.Markdown('doc/changelog.md'),
561    env.Markdown('doc/aarch32/getting-started-aarch32.md'),
562    env.Markdown('doc/aarch32/design/code-generation-aarch32.md'),
563    env.Markdown('doc/aarch32/design/literal-pool-aarch32.md'),
564    env.Markdown('doc/aarch64/supported-instructions-aarch64.md'),
565    env.Markdown('doc/aarch64/getting-started-aarch64.md'),
566    env.Markdown('doc/aarch64/topics/ycm.md'),
567    env.Markdown('doc/aarch64/topics/extending-the-disassembler.md'),
568    env.Markdown('doc/aarch64/topics/index.md'),
569]
570env.Alias('doc', doc)
571extra_targets.Add('doc', 'Convert documentation to HTML (requires the '
572                         '`markdown` program).')
573
574Help('\nAvailable extra targets:\n' + extra_targets.Help())
575