summaryrefslogtreecommitdiffstats
path: root/native_client_sdk/src/main.scons
blob: 557333dfe978302efc2d85489581a6cfc41ea308 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
#! -*- python -*-
#
# Copyright (c) 2011 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

""" Main scons script for Native Client SDK builds.

Do not invoke this script directly, but instead use the scons or scons.bat
wrapper function.  E.g.

Linux or Mac:
  ./scons [Options...]

Windows:
  scons.bat [Options...]
"""

from __future__ import with_statement

import os
import platform
import subprocess
import sys
import toolchainbinaries
from build_tools import build_utils

# ----------------------------------------------------------------------------
HELP_STRING = """
===============================================================================
Help for NaCl SDK
===============================================================================

* cleaning:            ./scons -c
* build a target:      ./scons <target>

Supported targets:
  * bot                   Runs everything that the build and try bots run.
  * debug_server_x64      Build the out-of-process debug_server.
  * debug_server_Win32    Build the out-of-process debug_server.
  * docs                  Build all of the Doxygen documentation.
  * examples              Build the examples.
  * experimental          Build the experimental projects.
  * installer             Build the SDK installer.
  * nacl-bpad             Build the native client crash reporting tool.
  * sdk_tools             Build nacl_sdk.zip and sdk_tools.tgz
  * toolchain             Update the toolchain's headers and libraries.
  * vsx                   Build the Visual Studio Plugin.

Flags:
  * USE_EXISTING_INSTALLER=1        Do not rebuild the installer if it exists.
  * chrome_browser_path=<full_path> Download Chrome to <full_path>.
  * SHOW_BROWSER=1                  Don't suppress browser GUI while performing
                                    browser tests in Linux.

More targets are listed below in the automatically generated help section.

===============================================================================
Automatically generated help follows:
===============================================================================
"""

# ----------------------------------------------------------------------------
# Perform some environment checks before running.
# Note that scons should set NACL_SDK_ROOT before this script runs.

if os.getenv('NACL_SDK_ROOT') is None:
  sys.stderr.write('NACL_SDK_ROOT must be defined as the root directory'
                   ' of NaCl SDK.\n')
  sys.exit(1)

# By default, run with a parallel build (i.e. '-j num_jobs').
# Use a default value proportional to the number of cpu cores on the system.
# To run a serial build, explicitly type '-j 1' on the command line.
try:
  import multiprocessing
  CORE_COUNT = multiprocessing.cpu_count()
except (ImportError, NotImplementedError):
  CORE_COUNT = 2  # Our buildbots seem to be dual-core typically

SetOption('num_jobs', CORE_COUNT * 2)
print 'Building with', GetOption('num_jobs'), 'parallel jobs'

# ----------------------------------------------------------------------------
# The environment_list contains all the build environments that we want to
# specify.  Selecting a particular environment is done using the --mode option.
# Each environment that we support gets appended to this list.
environment_list = []

# ----------------------------------------------------------------------------
# Create the base environment, from which all other environments are derived.
base_env = Environment(
    tools = ['component_setup'],
    CPPPATH = ['$MAIN_DIR'],
    CPPDEFINES = [
      'BOOST_ALL_NO_LIB',
    ],
    NACL_TOOLCHAIN_ROOTS = {
        ('x86', 'newlib'):
          build_utils.NormalizeToolchain(arch='x86', variant='newlib'),
        ('x86', 'glibc'):
          build_utils.NormalizeToolchain(arch='x86', variant='glibc'),
    },
    ROOT_DIR = os.path.abspath(os.getcwd()),
    IS_WINDOWS = sys.platform in ['cygwin', 'win32'],
    IS_LINUX = sys.platform == 'linux2',
    IS_MAC = sys.platform == 'darwin',
    JOB_COUNT = GetOption('num_jobs')
)

# It is possible to override these values on the command line by typing
# something like this:
#   PYTHON=/path/to/my/python
base_env.SetDefault(
    PYTHON = ARGUMENTS.get('PYTHON', 'python'),
    USE_EXISTING_INSTALLER = ARGUMENTS.get('USE_EXISTING_INSTALLER', False),
    SHOW_BROWSER = ARGUMENTS.get('SHOW_BROWSER', False),
)

base_env.Append(
    BUILD_SCONSCRIPTS = [
        # Keep in alphabetical order
        'build_tools/build.scons',
        'debugger/build.scons',
        'documentation/build.scons',
        'experimental/visual_studio_plugin/build.scons',
        'experimental/webgtt/tests/nacltest/test.scons',
        'project_templates/test.scons',
    ],
)

base_env.Help(HELP_STRING)

KNOWN_SUITES = frozenset([
    'bot',
    ])


def HasBotTarget(env):
  if 'bot' in COMMAND_LINE_TARGETS:
    return True
  return False

base_env.AddMethod(HasBotTarget)


def CheckSuiteName(suite, node_name):
  '''Check whether a given test suite or alias name is a known name.

  If the suite name is not in the approved list, then this function throws
  an exception, with the node_name within the error message.

  Args:
    suite: a name of a suite that must be in the KNOWN_SUITES set
    node_name: The name of the node.  This is used for error messages
  '''
  if suite not in KNOWN_SUITES:
    raise Exception('Testsuite/Alias "%s" for target "%s" is unknown' %
                    (suite, node_name))


def AddNodeToTestSuite(env, node, suite_names, node_name, test_size='all'):
  '''Adds a test node to a given set of suite names

  These tests are automatically added to the run_all_tests target and are
  listed in the help screen.

  This function is loosely based on a function of the same name in the
  Native Client repository

  Args:
    env - The environment from which this function was called
    node - A scons node (e.g., file, command, etc) to be added to set suite
    suite_names - A list of test suite names.  For none, pass an empty list
    node_name - The target name used for running this test
    test_size - The relative run-time of this test: small, medium, or large
  '''

  # CommandTest can return an empty list when it silently discards a test
  if not node:
    return

  AlwaysBuild(node)

  for s in suite_names:
    CheckSuiteName(s, node_name)
    env.Alias(s, node)

  if test_size not in ['small', 'medium', 'large', 'all']:
    raise Exception('Invalid test size for %s' % node_name)

  # Note that COMPONENT_TEST_SIZE is set to 'large' by default, which
  # populates a largely redundant list of 'large' tests.  Note that all
  # tests are added to 'all', so setting test_size='all' is a no-op
  env.ComponentTestOutput(node_name, node, COMPONENT_TEST_SIZE=test_size)

base_env.AddMethod(AddNodeToTestSuite)


def ShouldBeCleaned(env, targets, suite_names, node_name):
  '''Determines whether a given set of targets require cleaning.

  Args:
    env - The calling environment.
    targets - Any build artifacts to which a cleaning step might apply.
              Any false object indicates that this check is skipped.
    suite_names - Any suites that might produce |targets|
    node_name - A node that might produce |targets|
  '''
  if not env.GetOption('clean'):
    return False

  if len(COMMAND_LINE_TARGETS) > 0:
    clean_this = False
    for cl_target in COMMAND_LINE_TARGETS:
      if cl_target in suite_names or cl_target == node_name:
        clean_this = True
        break
    if not clean_this:
      return False

  if not targets:
    return True
  for target in targets:
    if os.path.exists(target):
      return True
  return False


def AddCleanAction(env, targets, action, suite_names, node_name):
  '''Adds a cleanup action that scons cannot detect automatically.

  Cleaning will only occur if there is a match between the suite or nodes
  specified on the command line, and suite_names or node_name or if no
  suite or nodes are specified on the command line.  Also, at least one of the
  targets must exist on the file system.

  Args:
    env - The calling environment
    targets - Artifacts to be cleaned.
    action - The action to be performed.  It is up to the caller to ensure
             that |action| will actually remove |targets|
    suite_names - Any suites to which this cleanup target applies.
    node_name - Any nodes to which this cleanup target applies.
  '''
  if ShouldBeCleaned(env, targets, suite_names, node_name):
    env.Execute(action)

base_env.AddMethod(AddCleanAction)


def AddNodeAliases(env, node, suite_names, node_name):
  '''Allow a given node to be built under a different name or as a suite

  Args:
    env - The calling environment
    node - A target node to add to a known build alias (e.g., 'bot')
    suite_names - A list of suite names.  For none, pass an empty list.  This
                  node will be run whenever any of these suites are invoked.
                  Each suite name must match a string in KNOWN_SUITES.
    node_name - The name of this node, when run by itself
  '''

  if not node:
    return

  for s in suite_names:
    CheckSuiteName(s, node_name)
    env.Alias(s, node)

  env.Alias(node_name, node)

base_env.AddMethod(AddNodeAliases)


def CreatePythonUnitTest(env, filename, dependencies=None, disabled=False,
                         params=None, buffered=True, banner=None):
  """Returns a new build command that will run a unit test with a given file.

  Args:
    env: SCons environment
    filename: The python file that contains the unit test
    dependencies: An optional list of other files that this unit test uses
    disabled: Setting this to True will prevent the test from running
    params: Optional additional parameters for python command
    buffered: True=stdout is buffered until entirely complete;
              False=stdout is immediately displayed as it occurs.
    banner: (optional) annotation banner for build/try bots

  Returns:
    A SCons command node
  """
  dependencies = dependencies or []
  params = params or []

  basename = os.path.splitext(os.path.basename(filename))[0]
  outfilename = "%s_output.txt" % basename


  def RunPythonUnitTest(env, target, source):
    """Runs unit tests using the given target as a command.

    The argument names of this test are not very intuitive but match what is
    used conventionally throughout scons.  If the string "PASSED" does not
    occur in target when this exits, the test has failed; also a scons
    convention.

    Args:
      env: SCons's current environment.
      target: Where to write the result of the test.
      source: The command to run as the test.

    Returns:
      None for good status
      An error string for bad status
    """
    bot = build_utils.BotAnnotator()
    if banner:
      bot.BuildStep(banner)

    if disabled:
      sys.stdout.write("Test %s is disabled.\n" % basename)
      sys.stdout.flush()
      return None   # return with good status

    import subprocess

    app = [str(env['PYTHON']), str(source[0].abspath)] + map(
        lambda param: param if type(param) is str else str(param.abspath),
        params)
    bot.Print('Running: %s' % app)
    app_env = os.environ.copy()
    # We have to do this because scons overrides PYTHONPATH and does
    # not preserve what is provided by the OS.
    python_path = [env['ROOT_DIR'], app_env['PYMOX'], app_env['PYTHONPATH']]
    app_env['PYTHONPATH'] = os.pathsep.join(python_path)
    ret_val = 'Error: General Test Failure'  # Indicates failure, by default
    target_str = str(target[0])
    with open(target_str, 'w') as outfile:
      def Write(str):
        if buffered:
          outfile.write(str)
          outfile.flush()
        else:
          sys.stdout.write(str)
          sys.stdout.flush()
      Write('\n-----Begin output for Test: %s\n' % basename)
      if subprocess.call(app, env=app_env,
                         stdout=outfile if buffered else None,
                         stderr=outfile if buffered else None):
        Write('-----Error: unit test failed\n')
        ret_val = 'Error: Test Failure in %s' % basename
      else:
        ret_val = None  # Indicates success

      Write('-----End output for Test: %s\n' % basename)
    if buffered:
      with open(target_str, 'r') as resultfile:
        sys.stdout.write(resultfile.read())
      sys.stdout.flush()

    if ret_val:
      bot.BuildStepFailure()

    return ret_val

  cmd = env.Command(outfilename, filename, RunPythonUnitTest)
  env.Depends(cmd, dependencies)
  # Create dependencies for all the env.File parameters and other scons nodes
  for param in params:
    if type(param) is not str:
      env.Depends(cmd, param)

  return cmd

base_env.AddMethod(CreatePythonUnitTest)


# ----------------------------------------------------------------------------
# Support for running Chrome.  These functions access the construction
# Environment() to produce a path to Chrome.

# A Dir object representing the directory where the Chrome binaries are kept.
# You can use chrome_binaries_dir= to set this on the command line.  Defaults
# to chrome_binaries.
base_env['CHROME_DOWNLOAD_DIR'] = \
    base_env.Dir(ARGUMENTS.get('chrome_binaries_dir', '#chrome_binaries'))


def ChromeArchitectureSpec(env):
  '''Determine the architecture spec for the Chrome binary.

  The architecture spec is a string that represents the host architecture.
  Possible values are:
    x86-32
    x86-64
  On Mac and Windows, the architecture spec is always x86-32, because there are
  no 64-bit version available.

  Returns: An architecture spec string for the host CPU.
  '''
  arch, _ = platform.architecture();
  # On Mac and Windows, always use a 32-bit version of Chrome (64-bit versions
  # are not available).
  if env['IS_WINDOWS'] or env['IS_MAC']:
    arch = 'x86-32'
  else:
    arch = 'x86-64' if '64' in arch else 'x86-32'
  return arch

base_env.AddMethod(ChromeArchitectureSpec)


def GetDefaultChromeBinary(env):
  '''Get a File object that represents a Chrome binary.

  By default, the test infrastructure will download a copy of Chrome that can
  be used for testing.  This method returns a File object that represents the
  downloaded Chrome binary that can be run by tests.  Note that the path to the
  binary is specific to the host platform, for example the path on Linux
  is <chrome_dir>/linux/<arch>/chrome, while on Mac it's
    <chrome_dir>/mac/<arch>/Chromium.app/Contents.MacOS/Chromium.

  Returns: A File object representing the Chrome binary.
  '''
  if env['IS_LINUX']:
    os_name = 'linux'
    binary = 'chrome'
  elif env['IS_WINDOWS']:
    os_name = 'windows'
    binary = 'chrome.exe'
  elif env['IS_MAC']:
    os_name = 'mac'
    binary = 'Chromium.app/Contents/MacOS/Chromium'
  else:
    raise Exception('Unsupported OS')

  return env.File(os.path.join(
      '${CHROME_DOWNLOAD_DIR}',
      '%s_%s' % (os_name, env.ChromeArchitectureSpec()),
      binary)) 

base_env.AddMethod(GetDefaultChromeBinary)


def GetChromeBinary(env):
  '''Return a File object that represents the downloaded Chrome binary.

  If chrome_browser_path is specified on the command line, then return a File
  object that represents that path.  Otherwise, return a File object
  representing the default downloaded Chrome (see GetDefaultChromeBinary(),
  above).

  Returns: A File object representing a Chrome binary.
  '''
  return env.File(ARGUMENTS.get('chrome_browser_path',
                                env.GetDefaultChromeBinary()))

base_env.AddMethod(GetChromeBinary)


def DependsOnChrome(env, dependency):
  '''Create a dependency on the download of Chrome.

  Creates a dependency in |env| such that Chrome gets downloaded (if necessary)
  whenever |dependency| changes.  Uses the Chrome downloader scripts built
  into NaCl; this script expects NaCl to be DEPS'ed into
  third_party/native_client/native_client.

  The Chrome binary is added as a precious node to the base Environment.  If
  we added it to the build environment env, then downloading chrome would in
  effect be specified for multiple environments.
  '''
  if not hasattr(base_env, 'download_chrome_node'):
    chrome_binary = env.GetDefaultChromeBinary()
    download_chrome_script = build_utils.JoinPathToNaClRepo(
        'native_client', 'build', 'download_chrome.py',
        root_dir=env['ROOT_DIR'])
    base_env.download_chrome_node = env.Command(
        chrome_binary,
        [],
        '${PYTHON} %s --arch=%s --dst=${CHROME_DOWNLOAD_DIR}' %
            (download_chrome_script, env.ChromeArchitectureSpec()))
    # This stops Scons from deleting the file before running the step above.
    env.NoClean(chrome_binary)
    env.Precious(chrome_binary)
  env.Depends(dependency, base_env.download_chrome_node)

base_env.AddMethod(DependsOnChrome)


# ----------------------------------------------------------------------------
# Targets for updating sdk headers and libraries
# NACL_SDK_XXX vars are defined by  site_scons/site_tools/naclsdk.py
# NOTE: Our task here is complicated by the fact that there might already be
#       some (outdated) headers/libraries at the new location
#       One of the hacks we employ here is to make every library depend
#       on the installation on ALL headers (sdk_headers)

# Contains all the headers to be installed
sdk_headers = base_env.Alias('extra_sdk_update_header', [])
# Contains all the libraries and .o files to be installed
libs_platform = base_env.Alias('extra_sdk_libs_platform', [])
libs = base_env.Alias('extra_sdk_libs', [])
base_env.Alias('extra_sdk_update', [libs, libs_platform])

AlwaysBuild(sdk_headers)

# ----------------------------------------------------------------------------
# The following section contains proxy nodes which can be used to create
# dependencies between targets that are not in the same scope or environment.
toolchain_node = base_env.Alias('toolchain', [])


def GetToolchainNode(env):
  '''Returns the node associated with the toolchain build target'''
  return toolchain_node

base_env.AddMethod(GetToolchainNode)


def GetHeadersNode(env):
  return sdk_headers

base_env.AddMethod(GetHeadersNode)

installer_prereqs_node = base_env.Alias('installer_prereqs', [])


def GetInstallerPrereqsNode(env):
  return installer_prereqs_node

base_env.AddMethod(GetInstallerPrereqsNode)

installer_test_node = base_env.Alias('installer_test_node', [])


def GetInstallerTestNode(env):
  return installer_test_node

base_env.AddMethod(GetInstallerTestNode)


def AddHeaderToSdk(env, nodes, subdir = 'nacl/', base_dirs = None):
  """Add a header file to the toolchain.  By default, Native Client-specific
  headers go under nacl/, but there are non-specific headers, such as
  the OpenGLES2 headers, that go under their own subdir.

  Args:
    env: Environment in which we were called.
    nodes: A list of node objects to add to the toolchain
    subdir: This is appended to each base_dir
    base_dirs: A list of directories to install the node to"""
  if not base_dirs:
    # TODO(mball): This won't work for PNaCl:
    base_dirs = [os.path.join(dir, 'x86_64-nacl', 'include')
                 for dir in env['NACL_TOOLCHAIN_ROOTS'].values()]

  for base_dir in base_dirs:
    node = env.Replicate(os.path.join(base_dir, subdir), nodes)
    env.Depends(sdk_headers, node)
    env.Depends(toolchain_node, node)
  return node

base_env.AddMethod(AddHeaderToSdk)


def AddLibraryToSdkHelper(env, nodes, is_lib, is_platform):
  """"Helper function to install libs/objs into the toolchain
  and associate the action with the extra_sdk_update.

  Args:
    env: Environment in which we were called.
    nodes: list of libc/objs
    is_lib: treat nodes as libs
    is_platform: nodes are truly platform specific
  """
  env.Requires(nodes, sdk_headers)

  dir = ARGUMENTS.get('extra_sdk_lib_destination')
  if not dir:
    dir = '${NACL_SDK_LIB}/'

  if is_lib:
    n = env.ReplicatePublished(dir, nodes, 'link')
  else:
    n = env.Replicate(dir, nodes)

  if is_platform:
    env.Alias('extra_sdk_libs_platform', n)
  else:
    env.Alias('extra_sdk_libs', n)
  return n


def AddLibraryToSdk(env, nodes, is_platform=False):
  return AddLibraryToSdkHelper(env, nodes, True, is_platform)

base_env.AddMethod(AddLibraryToSdk)


# ----------------------------------------------------------------------------
# This is a simple environment that is primarily for targets that aren't built
# directly by scons, and therefore don't need any special environment setup.
build_env = base_env.Clone(
    BUILD_TYPE = 'build',
    BUILD_GROUPS = ['default', 'all'],
    BUILD_TYPE_DESCRIPTION = 'Default build environment',
    HOST_PLATFORMS = '*',
    )

environment_list.append(build_env)

# ----------------------------------------------------------------------------
# Get the appropriate build command depending on the environment.


def SconsBuildCommand(env):
  '''Return the build command used to run separate scons instances.
  Args:
    env: The construction Environment() that is building using scons.
  Returns:
    A string representing the platform-specific build command that will run the
    scons instances.
  '''
  if env['IS_WINDOWS']:
    return 'scons.bat --jobs=%s' % GetOption('num_jobs')
  else:
    return './scons --jobs=%s' % GetOption('num_jobs')

# ----------------------------------------------------------------------------
# Add a builder for examples.  This adds an Alias() node named 'examples' that
# is always built.  There is some special handling for the clean mode, since
# SCons relies on actual build products for its clean processing and will not
# run Alias() actions during clean unless they actually produce something.


def BuildExamples(env, target, source):
  '''Build the examples.

  This runs the build command in the 'examples' directory.

  Args:
    env: The construction Environment() that is building the examples.
    target: The target that triggered this build.  Not used.
    source: The sources used for this build.  Not used.
  '''
  os_env = os.environ.copy()
  os_env['NACL_TARGET_PLATFORM'] = '.'
  subprocess.check_call(SconsBuildCommand(env),
                        cwd='examples',
                        env=os_env,
                        shell=True)


def CleanExamples(env, target, source):
  '''Clean the examples.

  This runs the clean command in the 'examples' directory.

  Args:
    env: The construction Environment() that is building the examples.
  '''
  os_env = os.environ.copy()
  os_env['NACL_TARGET_PLATFORM'] = '.'
  subprocess.check_call('%s --clean' % SconsBuildCommand(env),
                        cwd='examples',
                        env=os_env,
                        shell=True)


examples_builder = build_env.Alias('examples', [toolchain_node], BuildExamples)
build_env.AlwaysBuild(examples_builder)
build_env.AddCleanAction(['examples'], CleanExamples, ['bot'],
                         examples_builder)


def DependsOnExamples(env, dependency):
  env.Depends(dependency, examples_builder)

build_env.AddMethod(DependsOnExamples)


# ----------------------------------------------------------------------------
# Add a builder for experimental projects.  This adds an Alias() node named
# 'experimental' that is always built.  There is some special handling for the
# clean mode, since SCons relies on actual build products for its clean
# processing and will not run Alias() actions during clean unless they actually
# produce something.


def BuildExperimental(env, target, source):
  '''Build the experimental projects.

  This runs the build command in the 'experimental' directory.

  Args:
    env: The construction Environment() that is building the experimental
         projects.
    target: The target that triggered this build.  Not used.
    source: The sources used for this build.  Not used.
  '''
  subprocess.check_call(SconsBuildCommand(env),
                        cwd='experimental',
                        shell=True)


def CleanExperimental(env, target, source):
  '''Clean the experimental projects.

  This runs the clean command in the 'experimental' directory.

  Args:
    env: The construction Environment() that is building the experimental
         projects.
  '''
  subprocess.check_call('%s --clean' % SconsBuildCommand(env),
                        cwd='experimental',
                        shell=True)


experimental_builder = build_env.Alias('experimental', [toolchain_node],
                                       BuildExperimental)
build_env.AlwaysBuild(experimental_builder)
build_env.AddCleanAction(['experimental'], CleanExperimental, ['bot'],
                         experimental_builder)

# ----------------------------------------------------------------------------
# Add helper functions that build/clean a test by invoking scons under the
# test directory (|cwd|, when specified).  These functions are meant to be
# called from corresponding project-specific 'Build<project>Test' and
# 'Clean<project>Test' functions in the local test.scons scripts.  Note that the
# CleanNaClTest does not require a |cwd| because its cwd is always '.'


def BuildNaClTest(env, cwd):
  '''Build the test.

  This runs the build command in the test directory from which it is called.

  Args:
    env: The construction Environment() that is building the test.
    cwd: The directory under which the test's build.scons rests.
  '''
  subprocess.check_call('%s stage' % SconsBuildCommand(env),
                        cwd=cwd,
                        shell=True)

build_env.AddMethod(BuildNaClTest)


def CleanNaClTest(env):
  '''Clean the test.

  This runs the clean command in the test directory from which it is called.

  Args:
    env: The construction Environment() that is building the test.
    cwd: The directory under which the test's build.scons rests.
  '''
  subprocess.check_call('%s stage --clean' % SconsBuildCommand(env),
                        shell=True)
  # The step above still leaves behind two empty 'opt' directories, so a second
  # cleaning pass is necessary.
  subprocess.check_call('%s --clean' % SconsBuildCommand(env),
                        shell=True)

build_env.AddMethod(CleanNaClTest)

# ----------------------------------------------------------------------------
# Enable PPAPIBrowserTester() functionality using nacltest.js
# NOTE: The three main functions in this section: PPAPIBrowserTester(),
# CommandTest(), and AutoDepsCommand() are 'LITE' versions of their counterparts
# provided by Native Client @ third_party/native_client/native_client/SConstruct


def SetupBrowserEnv(env):
  '''Set up the environment for running the browser.

  This copies environment parameters provided by the OS in order to run the
  browser reliably.

  Args:
    env: The construction Environment() that runs the browser.
  '''
  EXTRA_ENV = ['XAUTHORITY', 'HOME', 'DISPLAY', 'SSH_TTY', 'KRB5CCNAME']
  for var_name in EXTRA_ENV:
    if var_name in os.environ:
      env['ENV'][var_name] = os.environ[var_name]

  env.Append(
    PYTHONPATH = [
        build_utils.JoinPathToNaClRepo(
            'third_party', 'pylib',
            root_dir=os.getenv('NACL_SDK_ROOT')),
        build_utils.JoinPathToNaClRepo(
            'tools', 'valgrind',
            root_dir=os.getenv('NACL_SDK_ROOT')),
        ]
    )


def PPAPIBrowserTester(env,
                       target,
                       url,
                       files,
                       timeout=20):
  '''The main test wrapper for browser integration tests.

  This constructs the command that invokes the browser_tester.py script on an
  existing Chrome binary (to be downloaded if necessary).

  Args:
    env: The construction Environment() that runs the browser.
    target: The output file to which the output of the test is to be written.
    url: The test web page.
    files: The files necessary for the web page to be served.
    timeout: How long to wait for a response before concluding failure.

  Returns: A command node that executes the browser test.
  '''

  env = env.Clone()
  SetupBrowserEnv(env)

  python_tester_script = build_utils.JoinPathToNaClRepo(
      'native_client', 'tools', 'browser_tester', 'browser_tester.py',
      root_dir=env['ROOT_DIR'])

  # Check if browser GUI needs to be suppressed (possible only in Linux)
  headless_prefix = []
  if not env['SHOW_BROWSER'] and env['IS_LINUX']:
    headless_prefix = ['xvfb-run', '--auto-servernum']

  command = headless_prefix + [
      '${PYTHON}', python_tester_script,
      '--browser_path', env.GetChromeBinary(),
      '--url', url,
      # Fail if there is no response for X seconds.
      '--timeout', str(timeout)]

  for dep_file in files:
    command.extend(['--file', dep_file])

  cmd = env.CommandTest(target,
                         command,
                         # Set to 'huge' so that the browser tester's timeout
                         # takes precedence over the default of the test suite.
                         size='huge',
                         capture_output=False)
  env.DependsOnChrome(cmd)

  return cmd

build_env.AddMethod(PPAPIBrowserTester)


def CommandTest(env,
                name,
                command,
                size='small',
                capture_output=True):
  '''The wrapper for testing execution of a command and logging details.

  This constructs the command that invokes the command_tester.py script on a
  given command.

  Args:
    env: The construction Environment() that runs the command.
    name: The output file to which the output of the tester is to be written.
    command: The command to be tested.
    size: This dictates certain timeout thresholds.
    capture_output: This specifies whether the command's output needs to be
                    captured for further processing. When this option is False,
                    stdout and stderr will be streamed out. For more info, see
                    <NACL_REPO>/native_client/tools/command_tester.py

  Returns: A command node that executes the command test.
  '''
  TEST_TIME_THRESHOLD = {
    'small':   2,
    'medium': 10,
    'large':  60,
    'huge': 1800,
    }

  if not name.endswith('out') or name.startswith('$'):
    raise Exception('ERROR: bad test filename for test output %r' % name)

  arch_string = env.ChromeArchitectureSpec();
  if env['IS_LINUX']:
    platform_string = 'linux'
  elif env['IS_MAC']:
    platform_string = 'mac'
  elif env['IS_WINDOWS']:
    platform_string = 'windows'

  name = '${TARGET_ROOT}/test_results/' + name
  max_time = TEST_TIME_THRESHOLD[size]

  script_flags = ['--name', name,
                  '--report', name,
                  '--time_warning', str(max_time),
                  '--time_error', str(10 * max_time),
                  '--perf_env_description', platform_string + '_' + arch_string,
                  '--arch', 'x86',
                  '--subarch', arch_string[-2:],
                  ]
  if not capture_output:
    script_flags.extend(['--capture_output', '0'])

  test_script = build_utils.JoinPathToNaClRepo(
      'native_client', 'tools', 'command_tester.py',
      root_dir=env['ROOT_DIR'])
  command = ['${PYTHON}', test_script] + script_flags + command
  return AutoDepsCommand(env, name, command)

build_env.AddMethod(CommandTest)


def AutoDepsCommand(env, name, command):
  """AutoDepsCommand() takes a command as an array of arguments.  Each
  argument may either be:

   * a string, or
   * a Scons file object, e.g. one created with env.File() or as the
     result of another build target.

  In the second case, the file is automatically declared as a
  dependency of this command.

  Args:
    env: The construction Environment() that runs the command.
    name: The target file to which the output is to be written.
    command: The command to be executed.

  Returns: A command node in the standard SCons format.
  """
  deps = []
  for index, arg in enumerate(command):
    if not isinstance(arg, str):
      if len(Flatten(arg)) != 1:
        # Do not allow this, because it would cause "deps" to get out
        # of sync with the indexes in "command".
        # See http://code.google.com/p/nativeclient/issues/detail?id=1086
        raise AssertionError('Argument to AutoDepsCommand() actually contains '
                             'multiple (or zero) arguments: %r' % arg)
      command[index] = '${SOURCES[%d].abspath}' % len(deps)
      deps.append(arg)

  return env.Command(name, deps, ' '.join(command))

build_env.AddMethod(AutoDepsCommand)


def BuildVSSolution(env, target_name, solution, project_config=None):
  """BuildVSSolution() Builds a Visual Studio solution.

  Args:
    env: The construction Environment() that runs the command.
    target_name: The name of the target.  Build output will be written to
      [target_name]_build_output.txt.
    solution: The solution to build.
    project_config: A valid project configuration string to pass into devenv.
      This provides support for building specific configurations, i.e.
      'Debug|Win32', 'Debug|x64', 'Release|Win32', 'Release|x64'.  Note that the
      string must be in quotes to work.  devenv will default to Win32 if this
      is not provided.

  Returns the Command() used to build the target, so other targets can be made
  to depend on it.
  """
  vs_build_action = ['vcvarsall', '&&', 'devenv', '${SOURCE}', '/build']
  if project_config:
    vs_build_action.extend([project_config])

  build_command = env.Command(target='%s_build_output.txt' % target_name,
                              source=solution,
                              action=' '.join(vs_build_action))
  env.AddNodeAliases(build_command, ['bot'], target_name)
  return build_command

build_env.AddMethod(BuildVSSolution)


def CleanVSSolution(env, target_name, solution_dir):
  """CleanVSSolution() Cleans up a Visual Studio solution's build results.
  
  The clean target created by this function is added to the 'bot' target as
  well as the target specified.  The function will clean any build artifacts
  that could possibly be generated under the solution directory.

  Args:
    env: The construction Environment() that runs the command.
    target_name: The name of the target which builds whatever should be cleaned
      up.
    solution_dir: The directory under which VS build artifacts are to be
      expected.  This function will look for Debug, Release, and x64 build
      targets.
  """
  clean_targets = [os.path.join(solution_dir, 'Debug'),
                   os.path.join(solution_dir, 'Release'),
                   os.path.join(solution_dir, 'x64')]

  for target in clean_targets:
    clean_action = ['rmdir', '/Q', '/S', target]
    env.AddCleanAction([target],
                       Action(' '.join(clean_action)),
                       ['bot'],
                       target_name)

build_env.AddMethod(CleanVSSolution)


def TestVSSolution(env, target_name, test_container, type, size, build_cmd):
  """Defines a set of tests to be added to the scons test set.

  This function adds a test solution generated by Visual Studio.  It can either
  run mstest or, for natively compiled solutions, it can run an executable.

  Args:
    env: The construction Environment() that runs the command.
    target_name: The name of the target which resulted in the test container.
      A name that clearly marks the target as a test is recommended here.
    test_container: The fully qualified path to the dll or exe that contains
      the tests.
    type: The type test package to expect as a string.  This can be 'dll' or
      'exe'.
    size: Which test harness to add the tests to; small, medium, or large
    build_cmd: The command which builds the target being tested.
  """
  test_action = test_container
  if type is 'dll':
    test_action = ' '.join(['vcvarsall',
                            '&&',
                            'mstest',
                            '/testcontainer:%s' % test_container,
                            '/resultsfile:${TARGET}'])

  # Can't use the test container as SOURCE because it is generated indirectly
  # and using it as source would declare an explicit dependency on a target,
  # generated by scons.  Files that exist in the environment can be used in that
  # way, but only if they exist at the time when scons starts to run, or if
  # the are explicit Command targets.  As a result, source is empty.
  test_command = env.Command(
      target='%s_test_results.trx' % target_name,
      source='',
      action=test_action)

  env.Depends(test_command, build_cmd)
  env.AddNodeToTestSuite(test_command,
                         ['bot'],
                         target_name,
                         size)
  return test_command

build_env.AddMethod(TestVSSolution)


# ----------------------------------------------------------------------------
BuildComponents(environment_list)

# Require specifying an explicit target only when not cleaning
if not GetOption('clean'):
  Default(None)