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