blob: d468d9031f0038a0618bc68ecaab7a283aa2d4be [file] [log] [blame]
#! -*- python -*-
# Copyright (c) 2011 The Chromium 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...]
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 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.
* installer Build the SDK installer.
* nacl-bpad Build the native client crash reporting tool.
* sdk_tools Build and sdk_tools.tgz
* toolchain Update the toolchain's headers and libraries.
* vsx Build the Visual Studio Plugin.
* 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')
# 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.
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'],
('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()),
SRC_DIR = os.path.dirname(os.path.dirname(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
PYTHON = ARGUMENTS.get('PYTHON', 'python'),
# Keep in alphabetical order
KNOWN_SUITES = frozenset([
def HasBotTarget(env):
return True
return False
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.
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
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:
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)
def ShouldBeCleaned(env, targets, suite_names, node_name):
'''Determines whether a given set of targets require cleaning.
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
clean_this = False
for cl_target in COMMAND_LINE_TARGETS:
if cl_target in suite_names or cl_target == node_name:
clean_this = True
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.
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):
def AddNodeAliases(env, node, suite_names, node_name):
'''Allow a given node to be built under a different name or as a suite
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:
for s in suite_names:
CheckSuiteName(s, node_name)
env.Alias(s, node)
env.Alias(node_name, node)
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.
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
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
env: SCons's current environment.
target: Where to write the result of the test.
source: The command to run as the test.
None for good status
An error string for bad status
bot = build_utils.BotAnnotator()
if banner:
if disabled:
sys.stdout.write("Test %s is disabled.\n" % basename)
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),
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:
Write('\n-----Begin output for Test: %s\n' % basename)
if, 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
ret_val = None # Indicates success
Write('-----End output for Test: %s\n' % basename)
if buffered:
with open(target_str, 'r') as resultfile:
if ret_val:
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
# ----------------------------------------------------------------------------
# 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:
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'
arch = 'x86-64' if '64' in arch else 'x86-32'
return arch
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
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 = ''
raise Exception('Unsupported OS')
return env.File(os.path.join(
'%s_%s' % (os_name, env.ChromeArchitectureSpec()),
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(),
Returns: A File object representing a Chrome binary.
return env.File(ARGUMENTS.get('chrome_browser_path',
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
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', '',
base_env.download_chrome_node = env.Command(
'${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.Depends(dependency, base_env.download_chrome_node)
# ----------------------------------------------------------------------------
# Targets for updating sdk headers and libraries
# NACL_SDK_XXX vars are defined by site_scons/site_tools/
# 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])
# ----------------------------------------------------------------------------
# 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
def GetHeadersNode(env):
return sdk_headers
installer_prereqs_node = base_env.Alias('installer_prereqs', [])
def GetInstallerPrereqsNode(env):
return installer_prereqs_node
installer_test_node = base_env.Alias('installer_test_node', [])
def GetInstallerTestNode(env):
return installer_test_node
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.
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
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.
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')
n = env.Replicate(dir, nodes)
if is_platform:
env.Alias('extra_sdk_libs_platform', n)
env.Alias('extra_sdk_libs', n)
return n
def AddLibraryToSdk(env, nodes, is_platform=False):
return AddLibraryToSdkHelper(env, nodes, True, is_platform)
# ----------------------------------------------------------------------------
# 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',
# ----------------------------------------------------------------------------
# Get the appropriate build command depending on the environment.
def SconsBuildCommand(env):
'''Return the build command used to run separate scons instances.
env: The construction Environment() that is building using scons.
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')
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.
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'] = '.'
def CleanExamples(env, target, source):
'''Clean the examples.
This runs the clean command in the 'examples' directory.
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),
examples_builder = build_env.Alias('examples', [toolchain_node], BuildExamples)
build_env.AddCleanAction(['examples'], CleanExamples, ['bot'],
def DependsOnExamples(env, dependency):
env.Depends(dependency, examples_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.
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),
def CleanNaClTest(env):
'''Clean the test.
This runs the clean command in the test directory from which it is called.
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),
# The step above still leaves behind two empty 'opt' directories, so a second
# cleaning pass is necessary.
subprocess.check_call('%s --clean' % SconsBuildCommand(env),
# ----------------------------------------------------------------------------
# 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.
env: The construction Environment() that runs the browser.
for var_name in EXTRA_ENV:
if var_name in os.environ:
env['ENV'][var_name] = os.environ[var_name]
'third_party', 'pylib',
'tools', 'valgrind',
def PPAPIBrowserTester(env,
'''The main test wrapper for browser integration tests.
This constructs the command that invokes the script on an
existing Chrome binary (to be downloaded if necessary).
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()
python_tester_script = build_utils.JoinPathToNaClRepo(
'native_client', 'tools', 'browser_tester', '',
# 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,
# Set to 'huge' so that the browser tester's timeout
# takes precedence over the default of the test suite.
return cmd
def CommandTest(env,
'''The wrapper for testing execution of a command and logging details.
This constructs the command that invokes the script on a
given command.
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
Returns: A command node that executes the command test.
'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 = ['${PYTHON}', test_script] + script_flags + command
return AutoDepsCommand(env, name, command)
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.
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
raise AssertionError('Argument to AutoDepsCommand() actually contains '
'multiple (or zero) arguments: %r' % arg)
command[index] = '${SOURCES[%d].abspath}' % len(deps)
return env.Command(name, deps, ' '.join(command))
def BuildVSSolution(env, target_name, solution, project_config=None):
"""BuildVSSolution() Builds a Visual Studio solution.
env: The construction Environment() that runs the command.
target_name: The name of the target. Build output will be written to
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:
build_command = env.Command(target='%s_build_output.txt' % target_name,
action=' '.join(vs_build_action))
env.AddNodeAliases(build_command, ['bot'], target_name)
return build_command
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.
env: The construction Environment() that runs the command.
target_name: The name of the target which builds whatever should be cleaned
solution_dir: The directory under which VS build artifacts are to be
expected. This function will look for Debug, Release, and x64 build
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]
Action(' '.join(clean_action)),
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.
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
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',
'/testcontainer:%s' % test_container,
# 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,
env.Depends(test_command, build_cmd)
return test_command
# ----------------------------------------------------------------------------
# Require specifying an explicit target only when not cleaning
if not GetOption('clean'):