diff options
author | bradnelson@google.com <bradnelson@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-11-04 06:10:23 +0000 |
---|---|---|
committer | bradnelson@google.com <bradnelson@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-11-04 06:10:23 +0000 |
commit | 1283ba4fe600d3c8cb32b494cbba96fb1eb106e4 (patch) | |
tree | 984323cbc7d5db84ecb9317f1c5e10dd07874026 /site_scons | |
parent | 7199517ad5241497f9571f93b1f71b95aea95e0b (diff) | |
download | chromium_src-1283ba4fe600d3c8cb32b494cbba96fb1eb106e4.zip chromium_src-1283ba4fe600d3c8cb32b494cbba96fb1eb106e4.tar.gz chromium_src-1283ba4fe600d3c8cb32b494cbba96fb1eb106e4.tar.bz2 |
Adding in new software construction toolkit version.
Review URL: http://codereview.chromium.org/9094
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@4566 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'site_scons')
-rw-r--r-- | site_scons/site_init.py | 209 | ||||
-rw-r--r-- | site_scons/site_tools/component_builders.py | 50 | ||||
-rw-r--r-- | site_scons/site_tools/component_setup.py | 83 | ||||
-rwxr-xr-x | site_scons/site_tools/component_targets.py | 264 | ||||
-rwxr-xr-x | site_scons/site_tools/component_targets_msvs.py | 461 | ||||
-rwxr-xr-x | site_scons/site_tools/component_targets_xml.py | 126 | ||||
-rw-r--r-- | site_scons/site_tools/concat_source.py | 14 | ||||
-rw-r--r-- | site_scons/site_tools/defer.py | 162 | ||||
-rw-r--r-- | site_scons/site_tools/environment_tools.py | 106 | ||||
-rw-r--r-- | site_scons/site_tools/publish.py | 34 | ||||
-rw-r--r-- | site_scons/site_tools/replicate.py | 3 | ||||
-rw-r--r-- | site_scons/site_tools/target_platform_mac.py | 4 | ||||
-rw-r--r-- | site_scons/site_tools/visual_studio_solution.py | 27 |
13 files changed, 1299 insertions, 244 deletions
diff --git a/site_scons/site_init.py b/site_scons/site_init.py index 490a1db..369b886 100644 --- a/site_scons/site_init.py +++ b/site_scons/site_init.py @@ -37,16 +37,10 @@ for the target environment. """ import __builtin__ -import os import sys import SCons -# List of target groups for printing help; modified by AddTargetGroup(); used -# by BuildEnvironments(). -__target_groups = {} - - def _HostPlatform(): """Returns the current host platform. @@ -118,29 +112,17 @@ def _CheckBuildModes(build_modes, environments, host_platform): all_build_groups[g].append(e['BUILD_TYPE']) # Add help for build types - xml_help = SCons.Script.GetOption('xml_help') - if xml_help: - help_mode_format = ' <build_mode name="%s"><![CDATA[%s]]></build_mode>\n' - help_text = '<mode_list>\n' - else: - help_text = ''' + help_text = ''' Use --mode=type to specify the type of build to perform. The following types may be specified: ''' - help_mode_format = ' %-16s %s\n' for build_type in all_build_types: if build_type not in all_build_groups: - help_text += help_mode_format % ( + help_text += ' %-16s %s\n' % ( build_type, build_desc.get(build_type, '')) - if xml_help: - help_group_format = (' <build_group name="%s"><![CDATA[%s]]>' - '</build_group>\n') - help_text += '</mode_list>\n<group_list>\n' - else: - help_group_format = ' %-16s %s\n' - help_text += ''' + help_text += ''' The following build groups may also be specified via --mode. Build groups build one or more of the other build types. The available build groups are: ''' @@ -148,12 +130,9 @@ build one or more of the other build types. The available build groups are: groups_sorted = all_build_groups.keys() groups_sorted.sort() for g in groups_sorted: - help_text += help_group_format % (g, ','.join(all_build_groups[g])) + help_text += ' %-16s %s\n' % (g, ','.join(all_build_groups[g])) - if xml_help: - help_text += '</group_list>\n' - else: - help_text += ''' + help_text += ''' Multiple modes may be specified, separated by commas: --mode=mode1,mode2. If no mode is specified, the default group will be built. This is equivalent to specifying --mode=default. @@ -168,39 +147,62 @@ specifying --mode=default. 'platform.' % mode) -def _AddTargetHelp(): - """Adds help for the target groups from the global __target_groups.""" - xml_help = SCons.Script.GetOption('xml_help') - help_text = '' - - for alias, description in __target_groups.items(): - items = map(str, SCons.Script.Alias(alias)[0].sources) - # Remove duplicates from multiple environments - items = list(set(items)) - - if items: - colwidth = max(map(len, items)) + 2 - cols = 77 / colwidth - if cols < 1: - cols = 1 # If target names are really long, one per line - rows = (len(items) + cols - 1) / cols - items.sort() - if xml_help: - help_text += '<target_group name="%s">\n' % alias - for i in items: - help_text += ' <build_target name="%s"/>\n' % i - help_text += '</target_group>\n' - else: - help_text += '\nThe following %s:' % description - for row in range(0, rows): - help_text += '\n ' - for i in range(row, len(items), rows): - help_text += '%-*s' % (colwidth, items[i]) - help_text += '\n %s (do all of the above)\n' % alias +#------------------------------------------------------------------------------ - SCons.Script.Help(help_text) -#------------------------------------------------------------------------------ +def BuildEnvironmentSConscripts(env): + """Evaluates SConscripts for the environment. + + Called by BuildEnvironments(). + """ + # Read SConscript for each component + # TODO(rspangler): Remove BUILD_COMPONENTS once all projects have + # transitioned to the BUILD_SCONSCRIPTS nomenclature. + for c in env.SubstList2('$BUILD_SCONSCRIPTS', '$BUILD_COMPONENTS'): + # Clone the environment so components can't interfere with each other + ec = env.Clone() + + if ec.Entry(c).isdir(): + # The component is a directory, so assume it contains a SConscript + # file. + c_dir = ec.Dir(c) + + # Use 'build.scons' as the default filename, but if that doesn't + # exist, fall back to 'SConscript'. + c_script = c_dir.File('build.scons') + if not c_script.exists(): + c_script = c_dir.File('SConscript') + else: + # The component is a SConscript file. + c_script = ec.File(c) + c_dir = c_script.dir + + # Make c_dir a string. + c_dir = str(c_dir) + + # Use build_dir differently depending on where the SConscript is. + if not ec.RelativePath('$TARGET_ROOT', c_dir).startswith('..'): + # The above expression means: if c_dir is $TARGET_ROOT or anything + # under it. Going from c_dir to $TARGET_ROOT and dropping the not fails + # to include $TARGET_ROOT. + # We want to be able to allow people to use addRepository to back things + # under $TARGET_ROOT/$OBJ_ROOT with things from above the current + # directory. When we are passed a SConscript that is already under + # $TARGET_ROOT, we should not use build_dir. + ec.SConscript(c_script, exports={'env': ec}, duplicate=0) + elif not ec.RelativePath('$MAIN_DIR', c_dir).startswith('..'): + # The above expression means: if c_dir is $MAIN_DIR or anything + # under it. Going from c_dir to $TARGET_ROOT and dropping the not fails + # to include $MAIN_DIR. + # Also, if we are passed a SConscript that + # is not under $MAIN_DIR, we should fail loudly, because it is unclear how + # this will correspond to things under $OBJ_ROOT. + ec.SConscript(c_script, build_dir='$OBJ_ROOT/' + c_dir, + exports={'env': ec}, duplicate=0) + else: + raise SCons.Error.UserError( + 'Bad location for a SConscript. "%s" is not under ' + '\$TARGET_ROOT or \$MAIN_DIR' % c_script) def BuildEnvironments(environments): @@ -223,7 +225,6 @@ def BuildEnvironments(environments): List of environments which were actually evaluated (built). """ # Get options - xml_help = SCons.Script.GetOption('xml_help') build_modes = SCons.Script.GetOption('build_mode') # TODO(rspangler): Remove support legacy MODE= argument, once everyone has # transitioned to --mode. @@ -239,9 +240,6 @@ def BuildEnvironments(environments): # Check build modes _CheckBuildModes(build_modes, environments, host_platform) - if xml_help: - SCons.Script.Help('<help_from_sconscripts>\n<![CDATA[\n') - environments_to_evaluate = [] for e in environments: if not e.Overlap(e['HOST_PLATFORMS'], [host_platform, '*']): @@ -251,56 +249,19 @@ def BuildEnvironments(environments): environments_to_evaluate.append(e) for e in environments_to_evaluate: - # Set up for deferred functions and published resources - e._InitializeComponentBuilders() - e._InitializeDefer() - e._InitializePublish() - - # Read SConscript for each component - # TODO(rspangler): Remove BUILD_COMPONENTS once all projects have - # transitioned to the BUILD_SCONSCRIPTS nomenclature. - for c in e.get('BUILD_COMPONENTS', []) + e.get('BUILD_SCONSCRIPTS', []): - # Clone the environment so components can't interfere with each other - ec = e.Clone() - - if ec.Entry(c).isdir(): - # The component is a directory, so assume it contains a SConscript - # file. - c_dir = ec.Dir(c) - - # Use 'build.scons' as the default filename, but if that doesn't - # exist, fall back to 'SConscript'. - c_script = c_dir.File('build.scons') - if not c_script.exists(): - c_script = c_dir.File('SConscript') - else: - # The component is a SConscript file. - c_script = ec.File(c) - c_dir = c_script.dir - - # TODO(bradnelson): this hack is not in mainline. - # Need to unify how to do this sort of thing. - c_dir = str(c_dir) - if os.path.isabs(c_dir): - build_dir = None - else: - build_dir = '$OBJ_ROOT/' + c_dir - ec.SConscript(c_script, - build_dir=build_dir, - exports={'env': ec}, - duplicate=0) - - # Execute deferred functions - e._ExecuteDefer() + # Make this the root environment for deferred functions, so they don't + # execute until our call to ExecuteDefer(). + e.SetDeferRoot() - if xml_help: - SCons.Script.Help(']]>\n</help_from_sconscripts>\n') + # Defer building the SConscripts, so that other tools can do + # per-environment setup first. + e.Defer(BuildEnvironmentSConscripts) - _AddTargetHelp() + # Execute deferred functions + e.ExecuteDefer() - # End final help tag - if xml_help: - SCons.Script.Help('</help>\n') + # Add help on targets. + AddTargetHelp() # Return list of environments actually evaluated return environments_to_evaluate @@ -353,17 +314,6 @@ def AddSiteDir(site_dir): SCons.Node.FS.get_default_fs().SConstruct_dir, site_dir) -def AddTargetGroup(target_group, description): - """Adds a target group, used for printing help. - - Args: - target_group: Name of target group. This should be the name of an alias - which points to other aliases for the specific targets. - description: Description of the target group. - """ - - __target_groups[target_group] = description - #------------------------------------------------------------------------------ @@ -380,7 +330,6 @@ Additional options for SCons: --site-path=DIRLIST Comma-separated list of additional site directory paths; each is processed as if passed to --site-dir. - --xml-help Print help in XML format. ''' def SiteInitMain(): @@ -394,7 +343,6 @@ def SiteInitMain(): # Let people use new global methods directly. __builtin__.AddSiteDir = AddSiteDir __builtin__.BuildEnvironments = BuildEnvironments - __builtin__.AddTargetGroup = AddTargetGroup # Legacy method names # TODO(rspangler): Remove these once they're no longer used anywhere. __builtin__.BuildComponents = BuildEnvironments @@ -402,11 +350,16 @@ def SiteInitMain(): # Set list of default tools for component_setup __builtin__.component_setup_tools = [ + # Defer must be first so other tools can register environment + # setup/cleanup functions. + 'defer', + # Component_targets must precede component_builders so builders can + # define target groups. + 'component_targets', 'command_output', 'component_bits', 'component_builders', 'concat_source', - 'defer', 'environment_tools', 'publish', 'replicate', @@ -442,16 +395,8 @@ def SiteInitMain(): action='store', metavar='PATH', help='comma-separated list of site directories') - SCons.Script.AddOption( - '--xml-help', - dest='xml_help', - action='store_true', - help='print help in XML format') - - if SCons.Script.GetOption('xml_help'): - SCons.Script.Help('<?xml version="1.0" encoding="UTF-8" ?>\n<help>\n') - else: - SCons.Script.Help(_new_options_help) + + SCons.Script.Help(_new_options_help) # Check for site path. This is a list of site directories which each are # processed as if they were passed to --site-dir. diff --git a/site_scons/site_tools/component_builders.py b/site_scons/site_tools/component_builders.py index 00de6a7..9580d7e 100644 --- a/site_scons/site_tools/component_builders.py +++ b/site_scons/site_tools/component_builders.py @@ -37,13 +37,13 @@ import SCons __component_list = {} -def _InitializeComponentBuilders(self): +def _InitializeComponentBuilders(env): """Re-initializes component builders module. Args: - self: Parent environment. + env: Environment context """ - self = self # Silence gpylint + env = env # Silence gpylint __component_list.clear() @@ -187,6 +187,9 @@ def ComponentPackage(self, package_name, dest_dir, **kwargs): # Store list of components for this program env._StoreComponents(package_name) + # Let component_targets know this target is available in the current mode + env.SetTargetProperty(package_name, TARGET_PATH=dest_dir) + # Set up deferred call to replicate resources env.Defer(ComponentPackageDeferred) @@ -267,7 +270,7 @@ def ComponentLibrary(self, lib_name, *args, **kwargs): # Install library in intermediate directory, so other libs and programs can # link against it - all_outputs += env.Replicate('$COMPONENT_LIBRARY_DIR', need_for_link) + all_outputs += env.Replicate('$LIB_DIR', need_for_link) # Publish output env.Publish(lib_name, 'run', need_for_run) @@ -281,6 +284,9 @@ def ComponentLibrary(self, lib_name, *args, **kwargs): # Store list of components for this library env._StoreComponents(lib_name) + # Let component_targets know this target is available in the current mode. + env.SetTargetProperty(lib_name, TARGET_PATH=lib_outputs[0]) + # If library should publish itself, publish as if it was a program if env.get('COMPONENT_LIBRARY_PUBLISH'): env['PROGRAM_BASENAME'] = lib_name @@ -343,6 +349,17 @@ def ComponentTestProgramDeferred(env): env.Depends(test_out, all_outputs) env.ComponentTestOutput('run_' + prog_name, test_out) + # Add target properties + env.SetTargetProperty( + prog_name, + # The copy of the program we care about is the one in the tests dir + EXE='$TESTS_DIR/$PROGRAM_NAME', + RUN_TARGET='run_' + prog_name, + RUN_CMDLINE='$COMPONENT_TEST_CMDLINE', + RUN_DIR='$TESTS_DIR', + TARGET_PATH='$TESTS_DIR/$PROGRAM_NAME', + ) + def ComponentTestProgram(self, prog_name, *args, **kwargs): """Pseudo-builder for test program to handle platform-dependent type. @@ -382,6 +399,9 @@ def ComponentTestProgram(self, prog_name, *args, **kwargs): # Store list of components for this program env._StoreComponents(prog_name) + # Let component_targets know this target is available in the current mode + env.SetTargetProperty(prog_name, TARGET_PATH=out_nodes[0]) + # Set up deferred call to replicate resources and run test env.Defer(ComponentTestProgramDeferred) @@ -446,6 +466,9 @@ def ComponentProgram(self, prog_name, *args, **kwargs): # Store list of components for this program env._StoreComponents(prog_name) + # Let component_targets know this target is available in the current mode + env.SetTargetProperty(prog_name) + # Set up deferred call to replicate resources env.Defer(ComponentProgramDeferred) @@ -485,6 +508,9 @@ def ComponentTestOutput(self, test_name, nodes): for group in groups: SCons.Script.Alias(group, a) + # Let component_targets know this target is available in the current mode + self.SetTargetProperty(test_name, TARGET_PATH=nodes[0]) + # Return the output node return a @@ -496,7 +522,10 @@ def generate(env): """SCons entry point for this tool.""" env.Replace( - COMPONENT_LIBRARY_DIR='$TARGET_ROOT/lib', + LIB_DIR='$TARGET_ROOT/lib', + # TODO(rspangler): Remove legacy COMPONENT_LIBRARY_DIR, once all users + # have transitioned to LIB_DIR + COMPONENT_LIBRARY_DIR='$LIB_DIR', STAGING_DIR='$TARGET_ROOT/staging', TESTS_DIR='$TARGET_ROOT/tests', TEST_OUTPUT_DIR='$TARGET_ROOT/test_output', @@ -508,7 +537,7 @@ def generate(env): # Default test size is large COMPONENT_TEST_SIZE='large', # Default timeouts for component tests - COMPONENT_TEST_TIMEOUT={'large':900, 'medium':450, 'small':180}, + COMPONENT_TEST_TIMEOUT={'large': 900, 'medium': 450, 'small': 180}, # Tests are enabled by default COMPONENT_TEST_ENABLED=True, # Static linking is a sensible default @@ -517,8 +546,8 @@ def generate(env): COMPONENT_LIBRARY_PUBLISH=False, ) env.Append( - LIBPATH=['$COMPONENT_LIBRARY_DIR'], - RPATH=['$COMPONENT_LIBRARY_DIR'], + LIBPATH=['$LIB_DIR'], + RPATH=['$LIB_DIR'], # Default alias groups for component builders COMPONENT_PACKAGE_GROUPS=['all_packages'], @@ -557,8 +586,11 @@ def generate(env): SCons.Script.Help(' --retest ' 'Rerun specified tests, ignoring cached results.\n') + # Defer per-environment initialization, but do before building SConscripts + env.Defer(_InitializeComponentBuilders) + env.Defer('BuildEnvironmentSConscripts', after=_InitializeComponentBuilders) + # Add our pseudo-builder methods - env.AddMethod(_InitializeComponentBuilders) env.AddMethod(_StoreComponents) env.AddMethod(ComponentPackage) env.AddMethod(ComponentObject) diff --git a/site_scons/site_tools/component_setup.py b/site_scons/site_tools/component_setup.py index 947a2be..e3c4574 100644 --- a/site_scons/site_tools/component_setup.py +++ b/site_scons/site_tools/component_setup.py @@ -63,6 +63,18 @@ def InstallUsingLink(target, source, env): # Need to force the target and source to be lists of nodes return SCons.Node.FS.LinkFunc([env.Entry(target)], [env.Entry(source)], env) + +def PreEvaluateVariables(env): + """Deferred function to pre-evaluate SCons varables for each build mode. + + Args: + env: Environment for the current build mode. + """ + # Convert directory variables to strings + for var in env.SubstList2('PRE_EVALUATE_DIRS'): + env[var] = str(env.Dir('$' + var)) + + #------------------------------------------------------------------------------ @@ -108,10 +120,6 @@ def generate(env): new_lookup_list.append(func) env.lookup_list = new_lookup_list - # Add other default tools from our toolkit - for t in component_setup_tools: - env.Tool(t) - # Cover part of the environment env.Replace( # Add a reference to our python executable, so subprocesses can find and @@ -129,17 +137,30 @@ def generate(env): # Use install function above, which uses links in preference to copying. INSTALL = InstallUsingLink, + ) + # Specify defaults for variables where we don't need to force replacement + env.SetDefault( # Environments are in the 'all' group by default BUILD_GROUPS=['all'], + # Directories DESTINATION_ROOT='$MAIN_DIR/scons-out$HOST_PLATFORM_SUFFIX', TARGET_ROOT='$DESTINATION_ROOT/$BUILD_TYPE', OBJ_ROOT='$TARGET_ROOT/obj', ARTIFACTS_DIR='$TARGET_ROOT/artifacts', - CPPDEFINES=[], ) + # Add default list of variables we should pre-evaluate for each build mode + env.Append(PRE_EVALUATE_DIRS = [ + 'ARTIFACTS_DIR', + 'DESTINATION_ROOT', + 'OBJ_ROOT', + 'SOURCE_ROOT', + 'TARGET_ROOT', + 'TOOL_ROOT', + ]) + # If a host platform was specified, need to put the SCons output in its own # destination directory. Different host platforms compile the same files # different ways, so need their own .sconsign files. @@ -147,26 +168,6 @@ def generate(env): if force_host_platform: env['HOST_PLATFORM_SUFFIX'] = '-' + force_host_platform - # The following environment replacements use env.Dir() to force immediate - # evaluation/substitution of SCons variables. They can't be part of the - # preceding env.Replace() since they they may rely indirectly on variables - # defined there, and the env.Dir() calls would be evaluated before the - # env.Replace(). - - # Set default SOURCE_ROOT if there is none, assuming we're in a local - # site_scons directory for the project. - source_root_relative = os.path.normpath( - os.path.join(os.path.dirname(__file__), '../..')) - source_root = env.get('SOURCE_ROOT', source_root_relative) - env['SOURCE_ROOT'] = env.Dir(source_root) - - # Make tool root separate from source root so it can be overridden when we - # have a common location for tools outside of the current clientspec. Need - # to check if it's defined already, so it can be set prior to this tool - # being included. - tool_root = env.get('TOOL_ROOT', '$SOURCE_ROOT') - env['TOOL_ROOT'] = env.Dir(tool_root) - # Put the .sconsign.dblite file in our destination root directory, so that we # don't pollute the source tree. Use the '_' + sys.platform suffix to prevent # the .sconsign.dblite from being shared between host platforms, even in the @@ -193,3 +194,35 @@ def generate(env): # Note that this currently forces projects which want to override the # default to do so after including the component_setup tool. env.Default('$DESTINATION_ROOT') + + # Add other default tools from our toolkit + # TODO(rspangler): Currently this needs to be before SOURCE_ROOT in case a + # tool needs to redefine it. Need a better way to handle order-dependency + # in tool setup. + for t in component_setup_tools: + env.Tool(t) + + # The following environment replacements use env.Dir() to force immediate + # evaluation/substitution of SCons variables. They can't be part of the + # preceding env.Replace() since they they may rely indirectly on variables + # defined there, and the env.Dir() calls would be evaluated before the + # env.Replace(). + + # Set default SOURCE_ROOT if there is none, assuming we're in a local + # site_scons directory for the project. + source_root_relative = os.path.normpath( + os.path.join(os.path.dirname(__file__), '../..')) + source_root = env.get('SOURCE_ROOT', source_root_relative) + env['SOURCE_ROOT'] = env.Dir(source_root).abspath + + # Make tool root separate from source root so it can be overridden when we + # have a common location for tools outside of the current clientspec. Need + # to check if it's defined already, so it can be set prior to this tool + # being included. + tool_root = env.get('TOOL_ROOT', '$SOURCE_ROOT') + env['TOOL_ROOT'] = env.Dir(tool_root).abspath + + # Defer pre-evaluating some environment variables, but do before building + # SConscripts. + env.Defer(PreEvaluateVariables) + env.Defer('BuildEnvironmentSConscripts', after=PreEvaluateVariables) diff --git a/site_scons/site_tools/component_targets.py b/site_scons/site_tools/component_targets.py new file mode 100755 index 0000000..493e0e2 --- /dev/null +++ b/site_scons/site_tools/component_targets.py @@ -0,0 +1,264 @@ +#!/usr/bin/python2.4 +# Copyright 2008, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Software construction toolkit target management for SCons.""" + + +import __builtin__ +import SCons.Script + + +# Dict of target groups (TargetGroup indexed by group name) +__target_groups = {} + +# Dict of targets (Target indexed by target name) +__targets = {} + +# Dict of target modes (TargetMode indexed by mode name) +__target_modes = {} + +#------------------------------------------------------------------------------ + + +class TargetGroup(object): + """Target group, as used by AddTargetGroup() and GetTargetGroups().""" + + def __init__(self, name, description): + """Initializes the target group. + + Args: + name: Name of the target group. + description: Description of group. + """ + self.name = name + self.description = description + + def GetTargetNames(self): + """Returns a list of target name strings for the group.""" + items = map(str, SCons.Script.Alias(self.name)[0].sources) + # Remove duplicates from multiple environments + return list(set(items)) + +#------------------------------------------------------------------------------ + + +class TargetMode(object): + """Target mode, as used by GetTargetModes().""" + + def __init__(self, name, description): + """Initializes the target mode. + + Args: + name: Name of the target mode. + description: Description of mode. + """ + self.name = name + self.description = description + + def GetTargetNames(self): + """Returns a list of target name strings for the group.""" + items = map(str, SCons.Script.Alias(self.name)[0].sources) + # Remove duplicates from multiple environments + return list(set(items)) + +#------------------------------------------------------------------------------ + + +class Target(object): + """Target object.""" + + def __init__(self, name): + """Initializes the target. + + Args: + name: Name of the target. + """ + self.name = name + self.properties = {} # Global properties + self.mode_properties = {} # Dict of modes to mode-specific properties + +#------------------------------------------------------------------------------ + + +def AddTargetGroup(name, description): + """Adds a target group, used for printing help. + + Args: + name: Name of target group. This should be the name of an alias which + points to other aliases for the specific targets. + description: Description of the target group. + """ + + # Warn if the target group already exists with a different description + if (name in __target_groups + and __target_groups[name].description != description): + print ('Warning: Changing description of target group "%s" from "%s" to ' + '"%s"' % (name, __target_groups[name].description, description)) + __target_groups[name].description = description + else: + __target_groups[name] = TargetGroup(name, description) + + +def GetTargetGroups(): + """Gets the dict of target groups. + + Returns: + The dict of target groups, indexed by group name. + + This dict is not fully populated until after BuildEnvironments() has been + called. + """ + return __target_groups + + +def GetTargetModes(): + """Gets the dict of target modes. + + Returns: + The dict of target modes, indexed by mode name. + + This dict is not fully populated until after BuildEnvironments() has been + called. + """ + return __target_modes + + +def GetTargets(): + """Gets the dict of targets. + + Returns: + The dict of targets, indexed by target name. + + This dict is not fully populated until after BuildEnvironments() has been + called. + """ + return __targets + + +def SetTargetProperty(self, target_name, all_modes=False, **kwargs): + """Sets one or more properties for a target. + + Args: + self: Environment context. + target_name: Name of the target. + all_modes: If True, property applies to all modes. If false, it applies + only to the current mode (determined by self['BUILD_TYPE']). + kwargs: Keyword args are used to set properties. Properties will be + converted to strings via env.subst(). + + For example: + foo_test = env.Program(...)[0] + env.SetTargetProperty('foo_test', global=True, DESCRIPTION='Foo test') + env.SetTargetProperty('foo_test', EXE=foo_test) + """ + # Get the target + if target_name not in __targets: + __targets[target_name] = Target(target_name) + target = __targets[target_name] + + if all_modes: + add_to_dict = target.properties + else: + mode = self.get('BUILD_TYPE') + if mode not in target.mode_properties: + target.mode_properties[mode] = {} + add_to_dict = target.mode_properties[mode] + + # Add values + for k, v in kwargs.items(): + add_to_dict[k] = self.subst(str(v)) + + +def AddTargetHelp(): + """Adds help for the targets, groups, and modes.""" + help_text = '' + + for group in GetTargetGroups().values(): + items = group.GetTargetNames() + items.sort() + if items: + help_text += '\nThe following %s:' % group.description + colwidth = max(map(len, items)) + 2 + cols = 77 / colwidth + if cols < 1: + cols = 1 # If target names are really long, one per line + rows = (len(items) + cols - 1) / cols + for row in range(0, rows): + help_text += '\n ' + for i in range(row, len(items), rows): + help_text += '%-*s' % (colwidth, items[i]) + help_text += '\n %s (do all of the above)\n' % group.name + + SCons.Script.Help(help_text) + + +def SetTargetDescription(self, target_name, description): + """Convenience function to set a target's global DESCRIPTION property. + + Args: + self: Environment context. + target_name: Name of the target. + description: Description of the target. + """ + self.SetTargetProperty(target_name, all_modes=True, DESCRIPTION=description) + + +def AddTargetMode(env): + """Adds the environment as a target mode. + + Args: + env: Environment context. + + Called via env.Defer() for each build mode. + """ + # Save the build mode and description + mode = env.get('BUILD_TYPE') + __target_modes[mode] = TargetMode(mode, env.get('BUILD_TYPE_DESCRIPTION')) + + +#------------------------------------------------------------------------------ + + +def generate(env): + # NOTE: SCons requires the use of this name, which fails gpylint. + """SCons entry point for this tool.""" + env = env # Silence gpylint + + __builtin__.AddTargetGroup = AddTargetGroup + __builtin__.AddTargetHelp = AddTargetHelp + __builtin__.GetTargetGroups = GetTargetGroups + __builtin__.GetTargetModes = GetTargetModes + __builtin__.GetTargets = GetTargets + + env.AddMethod(SetTargetDescription) + env.AddMethod(SetTargetProperty) + + # Defer per-mode setup + env.Defer(AddTargetMode) diff --git a/site_scons/site_tools/component_targets_msvs.py b/site_scons/site_tools/component_targets_msvs.py new file mode 100755 index 0000000..57be5b3 --- /dev/null +++ b/site_scons/site_tools/component_targets_msvs.py @@ -0,0 +1,461 @@ +#!/usr/bin/python2.4 +# Copyright 2008, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Visual Studio solution output of component targets for SCons.""" + +import md5 +import sys +import xml.dom +import xml.dom.minidom +import SCons + + +#------------------------------------------------------------------------------ + + +def MakeGuid(name, seed='component_targets_msvs'): + """Returns a GUID for the specified target name. + + Args: + name: Target name. + seed: Seed for MD5 hash. + Returns: + A GUID-line string calculated from the name and seed. + + This generates something which looks like a GUID, but depends only on the + name and seed. This means the same name/seed will always generate the same + GUID, so that projects and solutions which refer to each other can explicitly + determine the GUID to refer to explicitly. It also means that the GUID will + not change when the project for a target is rebuilt. + """ + # Calculate a MD5 signature for the seed and name. + d = md5.new(str(seed) + str(name)).hexdigest().upper() + # Convert most of the signature to GUID form (discard the rest) + guid = ('{' + d[:8] + '-' + d[8:12] + '-' + d[12:16] + '-' + d[16:20] + + '-' + d[20:32] + '}') + return guid + +#------------------------------------------------------------------------------ + + +def GetGuidFromVSProject(project_path): + """Reads the GUID from a Visual Studio project file. + + Args: + project_path: Path to the Visual Studio project file. + + Returns: + The GUID string from the file. + """ + doc = xml.dom.minidom.parse(project_path) + try: + n_root = doc.documentElement + if n_root.nodeName != 'VisualStudioProject': + raise SCons.Errors.UserError('%s is not a Visual Studio project.' % + project_path) + return str(n_root.attributes['ProjectGUID'].nodeValue) + finally: + # Clean up doc + doc.unlink() + +#------------------------------------------------------------------------------ + + +def ComponentVSProjectBuilder(target, source, env): + """Visual Studio project builder. + + Args: + target: Destination file. + source: List of sources to be added to the target. + env: Environment context. + + Returns: + Zero if successful. + """ + source = source # Silence gpylint + + target_name = env['TARGET_NAME'] + project_file = target[0].path + project_to_main = env.RelativePath(target[0].dir, env.Dir('$MAIN_DIR'), + sep='/') + hammer_bat = '$(ProjectDir)/%s/hammer.bat' % project_to_main + + # Project header + xml_impl = xml.dom.getDOMImplementation() + doc = xml_impl.createDocument(None, 'VisualStudioProject', None) + + n_root = doc.documentElement + n_root.setAttribute('ProjectType', 'Visual C++') + n_root.setAttribute('Version', '8.00') + n_root.setAttribute('Name', target_name) + n_root.setAttribute('ProjectGUID', MakeGuid(target_name)) + n_root.setAttribute('RootNamespace', target_name) + n_root.setAttribute('Keyword', 'MakeFileProj') + + n_platform = doc.createElement('Platforms') + n_root.appendChild(n_platform) + n = doc.createElement('Platform') + n.setAttribute('Name', 'Win32') + n_platform.appendChild(n) + + n_root.appendChild(doc.createElement('ToolFiles')) + + # One configuration per build mode supported by this target + n_configs = doc.createElement('Configurations') + n_root.appendChild(n_configs) + + target_path = env['TARGET_PATH'] + for mode, path in target_path.items(): + n_config = doc.createElement('Configuration') + n_config.setAttribute('Name', '%s|Win32' % mode) + n_config.setAttribute('OutputDirectory', + '$(ProjectDir)/%s/%s/out' % (mode, target_name)) + n_config.setAttribute('IntermediateDirectory', + '$(ProjectDir)/%s/%s/tmp' % (mode, target_name)) + n_config.setAttribute('ConfigurationType', '0') + n_configs.appendChild(n_config) + + n_tool = doc.createElement('Tool') + n_tool.setAttribute('Name', 'VCNMakeTool') + n_tool.setAttribute('IncludeSearchPath', '') + n_tool.setAttribute('ForcedIncludes', '') + n_tool.setAttribute('AssemblySearchPath', '') + n_tool.setAttribute('ForcedUsingAssemblies', '') + n_tool.setAttribute('CompileAsManaged', '') + n_tool.setAttribute('PreprocessorDefinitions', '') + if path: + n_tool.setAttribute( + 'Output', env.RelativePath(target[0].dir, env.Entry(path), sep='/')) + build_cmd = '%s --mode=%s %s' % (hammer_bat, mode, target_name) + clean_cmd = '%s --mode=%s -c %s' % (hammer_bat, mode, target_name) + n_tool.setAttribute('BuildCommandLine', build_cmd) + n_tool.setAttribute('CleanCommandLine', clean_cmd) + n_tool.setAttribute('ReBuildCommandLine', clean_cmd + ' && ' + build_cmd) + n_config.appendChild(n_tool) + + n_files = doc.createElement('Files') + n_root.appendChild(n_files) + # TODO(rspangler): Fill in files - at least, the .scons file invoking the + # target. + + n_root.appendChild(doc.createElement('Globals')) + + f = open(project_file, 'wt') + doc.writexml(f, encoding='Windows-1252', addindent=' ', newl='\n') + f.close() + + return 0 + + +def ComponentVSProject(self, target_name, **kwargs): + """Visual Studio project pseudo-builder for the specified target. + + Args: + self: Environment context. + target_name: Name of the target. + kwargs: Optional keyword arguments override environment variables in the + derived environment used to create the project. + + Returns: + A list of output nodes. + """ + # Builder only works on Windows + if sys.platform not in ('win32', 'cygwin'): + return [] + + # Clone environment and add keyword args + env = self.Clone() + for k, v in kwargs.items(): + env[k] = v + + # Save the target name + env['TARGET_NAME'] = target_name + + # Extract the target properties and save in the environment for use by the + # real builder. + t = GetTargets().get(target_name) + env['TARGET_PATH'] = {} + if t: + for mode, mode_properties in t.mode_properties.items(): + # Since the target path is what Visual Studio will run, use the EXE + # property in preference to TARGET_PATH. + target_path = mode_properties.get('EXE', + mode_properties.get('TARGET_PATH')) + env.Append(TARGET_PATH={mode: target_path}) + else: + # No modes declared for this target. Could be a custom alias created by + # a SConscript, rather than a component builder. Assume it can be built in + # all modes, but produces no output. + for mode in GetTargetModes(): + env.Append(TARGET_PATH={mode: None}) + + # Call the real builder + return env.ComponentVSProjectBuilder( + '$COMPONENT_VS_PROJECT_DIR/${TARGET_NAME}', []) + +#------------------------------------------------------------------------------ + + +def ComponentVSSolutionBuilder(target, source, env): + """Visual Studio solution builder. + + Args: + target: Destination file. + source: List of sources to be added to the target. + env: Environment context. + + Returns: + Zero if successful. + """ + source = source # Silence gpylint + + solution_file = target[0].path + projects = env['SOLUTION_PROJECTS'] + folders = env['SOLUTION_FOLDERS'] + + f = open(solution_file, 'wt') + + f.write('Microsoft Visual Studio Solution File, Format Version 9.00\n') + f.write('# Visual Studio 2005\n') + + # Projects generated by ComponentVSSolution() + for p in projects: + project_file = env.File( + '$COMPONENT_VS_PROJECT_DIR/%s$COMPONENT_VS_PROJECT_SUFFIX' % p) + f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( + '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}', # External makefile GUID + p, # Project name + env.RelativePath(target[0].dir, project_file), # Path to project file + MakeGuid(p), # Project GUID + )) + + # Projects generated elsewhere + for p in source: + f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( + # TODO(rspangler): What if this project isn't type external makefile? + # How to tell what type it is? + '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}', # External makefile GUID + p, # Project name + env.RelativePath(target[0].dir, p), # Path to project file + GetGuidFromVSProject(p.abspath), # Project GUID + )) + + # Folders from build groups + for folder in folders: + f.write('Project("%s") = "%s", "%s", "%s"\nEndProject\n' % ( + '{2150E333-8FDC-42A3-9474-1A3956D46DE8}', # Solution folder GUID + folder, # Folder name + folder, # Folder name (again) + # Use a different seed so the folder won't get the same GUID as a + # project. + MakeGuid(folder, seed='folder'), # Project GUID + )) + + f.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') + for mode in GetTargetModes(): + f.write('\t\t%s|Win32 = %s|Win32\n' % (mode, mode)) + f.write('\tEndGlobalSection\n') + + f.write('\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n') + for p in projects: + for mode in GetTargetModes(): + f.write('\t\t%s.%s|Win32.ActiveCfg = %s|Win32\n' % ( + MakeGuid(p), # Project GUID + mode, # Solution build configuration + mode, # Project build config for that solution config + )) + t = GetTargets().get(p) + if t and mode in t.mode_properties: + # Target can be built in this mode + f.write('\t\t%s.%s|Win32.Build.0 = %s|Win32\n' % ( + MakeGuid(p), # Project GUID + mode, # Solution build configuration + mode, # Project build config for that solution config + )) + f.write('\tEndGlobalSection\n') + + f.write('\tGlobalSection(SolutionProperties) = preSolution\n') + f.write('\t\tHideSolutionNode = FALSE\n') + f.write('\tEndGlobalSection\n') + + if folders: + f.write('\tGlobalSection(NestedProjects) = preSolution\n') + for p, folder in projects.items(): + f.write('\t\t%s = %s\n' % (MakeGuid(p), MakeGuid(folder, seed='folder'))) + f.write('\tEndGlobalSection\n') + + f.write('EndGlobal\n') + f.close() + + return 0 + + +def ComponentVSSolution(self, solution_name, target_names, projects=None, + **kwargs): + """Visual Studio solution pseudo-builder. + + Args: + self: Environment context. + solution_name: Name of the target. + target_names: Names of targets or target groups to include in the solution. + This will automatically build projects for them. + projects: List of aditional projects not generated by this solution to + include in the solution. + kwargs: Optional keyword arguments override environment variables in the + derived environment used to create the solution. + + Returns: + The list of output nodes. + """ + # TODO(rspangler): Should have option to build source project also. Perhaps + # select using a --source_project option, since it needs to use gather_inputs + # to scan the DAG and will blow up the null build time. + # TODO(rspangler): Should also have autobuild_projects option. If false, + # don't build them. + # TODO(rspangler): Should also be able to specify a target group directly + # (i.e. 'run_all_tests') + + # Builder only works on Windows + if sys.platform not in ('win32', 'cygwin'): + return [] + + # Clone environment and add keyword args + env = self.Clone() + for k, v in kwargs.items(): + env[k] = v + + # Save the target name + env['SOLUTION_NAME'] = solution_name + + # Get list of targets to make projects for. At this point we haven't + # determined whether they're groups or targets. + target_names = env.SubstList2(target_names) + env['SOLUTION_TARGETS'] = target_names + + project_names = {} + folders = [] + # Expand target_names into project names, and create project-to-folder + # mappings + if target_names: + # Expand target_names into project names + for target in target_names: + if target in GetTargetGroups(): + # Add target to folders + folders.append(target) + # Add all project_names in the group + for t in GetTargetGroups()[target].GetTargetNames(): + project_names[t] = target + elif target in GetTargets(): + # Just a target name + project_names[target] = None + else: + print 'Warning: ignoring unknown target "%s"' % target + else: + # No target names specified, so use all projects + for t in GetTargets(): + project_names[t] = None + + env['SOLUTION_FOLDERS'] = folders + env['SOLUTION_PROJECTS'] = project_names + + # Call the real builder + out_nodes = env.ComponentVSSolutionBuilder( + '$COMPONENT_VS_SOLUTION_DIR/${SOLUTION_NAME}', projects or []) + + # Call the real builder for the projects we generate + for p in project_names: + out_nodes += env.ComponentVSProject(p) + + # Add the solution target + # TODO(rspangler): Should really defer the rest of the work above, since + # otherwise we can't build a solution which has a target to rebuild itself. + env.Alias('all_solutions', env.Alias(solution_name, out_nodes)) + + # TODO(rspangler): To rebuild the solution properly, need to override its + # project configuration so it only has '--mode=all' (or some other way of + # setting the subset of modes which it should use to rebuild itself). + # Rebuilding with the property below will strip it down to just the current + # build mode, which isn't what we want. + # Let component_targets know this target is available in the current mode + #self.SetTargetProperty(solution_name, TARGET_PATH=out_nodes[0]) + + return out_nodes + +#------------------------------------------------------------------------------ + + +def generate(env): + # NOTE: SCons requires the use of this name, which fails gpylint. + """SCons entry point for this tool.""" + + # Add pseudo-builders to set up the project and solution builders. These + # need to be available on all platforms so that SConscripts which reference + # them will work. + env.AddMethod(ComponentVSProject) + env.AddMethod(ComponentVSSolution) + + # If not on Windows, don't do anything else + if sys.platform not in ('win32', 'cygwin'): + return + + # Include tools we need + env.Tool('gather_inputs') + + env.SetDefault( + COMPONENT_VS_SOLUTION_DIR='$DESTINATION_ROOT/solution', + COMPONENT_VS_PROJECT_DIR='$COMPONENT_VS_SOLUTION_DIR/projects', + COMPONENT_VS_SOLUTION_SUFFIX='.sln', + COMPONENT_VS_PROJECT_SUFFIX='.vcproj', + ) + + AddTargetGroup('all_solutions', 'solutions can be built') + + # Add builders + vcprojaction = SCons.Script.Action(ComponentVSProjectBuilder, varlist=[ + 'TARGET_NAME', + 'TARGET_PATH', + ]) + vcprojbuilder = SCons.Script.Builder( + action=vcprojaction, + suffix='$COMPONENT_VS_PROJECT_SUFFIX') + + slnaction = SCons.Script.Action(ComponentVSSolutionBuilder, varlist=[ + 'SOLUTION_TARGETS', + 'SOLUTION_FOLDERS', + 'SOLUTION_PROJECTS', + ]) + slnbuilder = SCons.Script.Builder( + action=slnaction, + suffix='$COMPONENT_VS_SOLUTION_SUFFIX') + + env.Append(BUILDERS={ + 'ComponentVSProjectBuilder': vcprojbuilder, + 'ComponentVSSolutionBuilder': slnbuilder, + }) diff --git a/site_scons/site_tools/component_targets_xml.py b/site_scons/site_tools/component_targets_xml.py new file mode 100755 index 0000000..9162d97 --- /dev/null +++ b/site_scons/site_tools/component_targets_xml.py @@ -0,0 +1,126 @@ +#!/usr/bin/python2.4 +# Copyright 2008, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""XML output of component targets for SCons.""" + + +import xml.dom +import SCons.Script + + +def TargetXMLHelp(target, source, env): + """Generates target information in XML format. + + Args: + target: Destination file. + source: List of sources. Should be empty, otherwise this will actually + require the sources to be built first. + env: Environment context. + """ + env = env + source = source # Silence gpylint + + target_path = target[0].abspath + + xml_impl = xml.dom.getDOMImplementation() + doc = xml_impl.createDocument(None, 'help', None) + + mode_list = doc.createElement('mode_list') + doc.documentElement.appendChild(mode_list) + for mode in GetTargetModes().values(): + n = doc.createElement('build_mode') + n.setAttribute('name', mode.name) + n.setAttribute('description', mode.description) + mode_list.appendChild(n) + + group_list = doc.createElement('target_groups') + doc.documentElement.appendChild(group_list) + for group in GetTargetGroups().values(): + items = group.GetTargetNames() + if not items: + continue + + ngroup = doc.createElement('target_group') + ngroup.setAttribute('name', group.name) + group_list.appendChild(ngroup) + + for i in items: + ntarget = doc.createElement('build_target') + ntarget.setAttribute('name', i) + ngroup.appendChild(ntarget) + + # Get properties for target, if any + target = GetTargets().get(i) + if target: + # All modes + for k, v in target.properties.items(): + n = doc.createElement('target_property') + n.setAttribute('name', k) + n.setAttribute('value', v) + ntarget.appendChild(n) + + # Mode-specific + for mode, mode_properties in target.mode_properties.items(): + nmode = doc.createElement('target_mode') + nmode.setAttribute('name', mode) + ntarget.appendChild(nmode) + + for k, v in mode_properties.items(): + n = doc.createElement('target_property') + n.setAttribute('name', k) + n.setAttribute('value', v) + nmode.appendChild(n) + + f = open(target_path, 'wt') + doc.writexml(f, encoding='UTF-8', addindent=' ', newl='\n') + f.close() + +#------------------------------------------------------------------------------ + + +def generate(env): + # NOTE: SCons requires the use of this name, which fails gpylint. + """SCons entry point for this tool.""" + env = env # Silence gpylint + + SCons.Script.Help('''\ + targets_xml Write information on the build mode's targets to + targets.xml. Most useful with --mode=all. +''') + + # Add build target to generate help + p = env.Command('$DESTINATION_ROOT/targets.xml', [], + env.Action(TargetXMLHelp)) + + # Always build xml info if requested. + # TODO(rspangler): Is there a better way to determine the xml info is up to + # date? + env.AlwaysBuild(p) + env.Alias('targets_xml', p) diff --git a/site_scons/site_tools/concat_source.py b/site_scons/site_tools/concat_source.py index c67463b..0598f97 100644 --- a/site_scons/site_tools/concat_source.py +++ b/site_scons/site_tools/concat_source.py @@ -52,14 +52,7 @@ def ConcatSourceBuilder(target, source, env): output_lines = [ '// This file is auto-generated by the ConcatSource builder.'] - for source_file in source: - # Skip source files which are not CPP files. These will be passed through - # to the output list by the pseudo-builder. - if source_file.suffix not in env['CONCAT_SOURCE_SUFFIXES']: - continue - - source_path = str(source_file) - + for source_path in map(str, source): if env.get('CC') == 'cl': # Add message pragma for nicer progress indication when building with # MSVC. @@ -93,8 +86,9 @@ def ConcatSourcePseudoBuilder(self, target, source): # from other files (which we pass through). cppsource = [] outputs = [] - for source_file in SCons.Script.Flatten(source): - if self.File(source_file).suffix in self['CONCAT_SOURCE_SUFFIXES']: + suffixes = self.Flatten(self.subst_list('$CONCAT_SOURCE_SUFFIXES')) + for source_file in self.arg2nodes(source): + if source_file.suffix in suffixes: cppsource.append(source_file) else: outputs.append(source_file) diff --git a/site_scons/site_tools/defer.py b/site_scons/site_tools/defer.py index ffd30da..8b6de89 100644 --- a/site_scons/site_tools/defer.py +++ b/site_scons/site_tools/defer.py @@ -36,33 +36,115 @@ import sys import types import SCons.Errors -__defer_groups = {} +class DeferGroup: + """Named list of functions to be deferred.""" + # If we derive DeferGroup from object, instances of it return type + # <class 'defer.DeferGroup'>, which prevents SCons.Util.semi_deepcopy() + # from calling its __semi_deepcopy__ function. + # TODO(sgk): Make semi_deepcopy() capable of handling classes derived from + # object. + + def __init__(self): + """Initialize deferred function object.""" + self.func_env_cwd = [] + self.after = set() + + def __semi_deepcopy__(self): + """Makes a semi-deep-copy of this object. + + Returns: + A semi-deep-copy of this object. + + This means it copies the sets and lists contained by this object, but + doesn't make copies of the function pointers and environments pointed to by + those lists. + + Needed so env.Clone() makes a copy of the defer list, so that functions + and after-relationships subsequently added to the clone are not added to + the parent. + """ + c = DeferGroup() + c.func_env_cwd = self.func_env_cwd[:] + c.after = self.after.copy() + return c + + +def SetDeferRoot(self): + """Sets the current environment as the root environment for defer. + + Args: + self: Current environment context. + + Functions deferred by environments cloned from the root environment (that is, + function deferred by children of the root environment) will be executed when + ExecuteDefer() is called from the root environment. + + Functions deferred by environments from which the root environment was cloned + (that is, functions deferred by parents of the root environment) will be + passed the root environment instead of the original parent environment. + (Otherwise, they would have no way to determine the root environment.) + """ + # Set the current environment as the root for holding defer groups + self['_DEFER_ROOT_ENV'] = self + + # Deferred functions this environment got from its parents will be run in the + # new root context. + for group in GetDeferGroups(self).values(): + new_list = [(func, self, cwd) for (func, env, cwd) in group.func_env_cwd] + group.func_env_cwd = new_list -def _InitializeDefer(self): - """Re-initializes deferred function handling. + +def GetDeferRoot(self): + """Returns the root environment for defer. Args: - self: Parent environment + self: Current environment context. + + Returns: + The root environment for defer. If one of this environment's parents + called SetDeferRoot(), returns that environment. Otherwise returns the + current environment. """ - # Clear the list of deferred groups - __defer_groups.clear() + return self.get('_DEFER_ROOT_ENV', self) + +def GetDeferGroups(env): + """Returns the dict of defer groups from the root defer environment. + + Args: + env: Environment context. -def _ExecuteDefer(self): + Returns: + The dict of defer groups from the root defer environment. + """ + return env.GetDeferRoot()['_DEFER_GROUPS'] + + +def ExecuteDefer(self): """Executes deferred functions. Args: - self: Parent environment + self: Current environment context. """ # Save directory, so SConscript functions can occur in the right subdirs oldcwd = os.getcwd() + # If defer root is set and isn't this environment, we're being called from a + # sub-environment. That's not where we should be called. + if self.GetDeferRoot() != self: + print ('Warning: Ignoring call to ExecuteDefer() from child of the ' + 'environment passed to SetDeferRoot().') + return + + # Get list of defer groups from ourselves. + defer_groups = GetDeferGroups(self) + # Loop through deferred functions - while __defer_groups: + while defer_groups: did_work = False - for name, group in __defer_groups.items(): - if group.after.intersection(__defer_groups.keys()): + for name, group in defer_groups.items(): + if group.after.intersection(defer_groups.keys()): continue # Still have dependencies if group.func_env_cwd: # Run all the functions in our named group @@ -70,11 +152,11 @@ def _ExecuteDefer(self): os.chdir(cwd) func(env) did_work = True - del __defer_groups[name] + del defer_groups[name] break if not did_work: - errmsg = 'Error in _ExecuteDefer: dependency cycle detected.\n' - for name, group in __defer_groups.items(): + errmsg = 'Error in ExecuteDefer: dependency cycle detected.\n' + for name, group in defer_groups.items(): errmsg += ' %s after: %s\n' % (name, group.after) raise SCons.Errors.UserError(errmsg) @@ -82,14 +164,31 @@ def _ExecuteDefer(self): os.chdir(oldcwd) -class DeferFunc(object): - """Named list of functions to be deferred.""" +def PrintDefer(self, print_functions=True): + """Prints the current defer dependency graph. - def __init__(self): - """Initialize deferred function object.""" - object.__init__(self) - self.func_env_cwd = [] - self.after = set() + Args: + self: Environment in which PrintDefer() was called. + print_functions: Print individual functions in defer groups. + """ + # Get the defer dict + # Get list of defer groups from ourselves. + defer_groups = GetDeferGroups(self) + dgkeys = defer_groups.keys() + dgkeys.sort() + for k in dgkeys: + print ' +- %s' % k + group = defer_groups[k] + after = list(group.after) + if after: + print ' | after' + after.sort() + for a in after: + print ' | +- %s' % a + if print_functions and group.func_env_cwd: + print ' functions' + for func, env, cwd in group.func_env_cwd: + print ' | +- %s %s' % (func.__name__, cwd) def Defer(self, *args, **kwargs): @@ -102,6 +201,9 @@ def Defer(self, *args, **kwargs): The deferred function will be passed the environment used to call Defer(), and will be executed in the same working directory as the calling SConscript. + (Exception: if this environment is cloned and the clone calls SetDeferRoot() + and then ExecuteDefer(), the function will be passed the root environment, + instead of the environment used to call Defer().) All deferred functions run after all SConscripts. Additional dependencies may be specified with the after= keyword. @@ -142,10 +244,14 @@ def Defer(self, *args, **kwargs): if func and not name: name = func.__name__ + # TODO(rspangler): Why not allow multiple functions? Should be ok + # Get list of names and/or functions this function should defer until after after = [] for a in self.Flatten(kwargs.get('after')): if isinstance(a, str): + # TODO(rspangler): Should check if '$' in a, and if so, subst() it and + # recurse into it. after.append(a) elif isinstance(a, types.FunctionType): after.append(a.__name__) @@ -154,9 +260,10 @@ def Defer(self, *args, **kwargs): raise ValueError('Defer after=%r is not a function or name' % a) # Find the deferred function - if name not in __defer_groups: - __defer_groups[name] = DeferFunc() - group = __defer_groups[name] + defer_groups = GetDeferGroups(self) + if name not in defer_groups: + defer_groups[name] = DeferGroup() + group = defer_groups[name] # If we were given a function, also save environment and current directory if func: @@ -169,7 +276,10 @@ def Defer(self, *args, **kwargs): def generate(env): # NOTE: SCons requires the use of this name, which fails gpylint. """SCons entry point for this tool.""" + env.Append(_DEFER_GROUPS={}) - env.AddMethod(_InitializeDefer) - env.AddMethod(_ExecuteDefer) env.AddMethod(Defer) + env.AddMethod(ExecuteDefer) + env.AddMethod(GetDeferRoot) + env.AddMethod(PrintDefer) + env.AddMethod(SetDeferRoot) diff --git a/site_scons/site_tools/environment_tools.py b/site_scons/site_tools/environment_tools.py index 969a923..0e4f938 100644 --- a/site_scons/site_tools/environment_tools.py +++ b/site_scons/site_tools/environment_tools.py @@ -35,6 +35,7 @@ will automatically be included by the component_setup tool. """ +import os import SCons @@ -61,14 +62,15 @@ def FilterOut(self, **kw): continue for vremove in val: - if vremove in envval: + # Use while not if, so we can handle duplicates. + while vremove in envval: envval.remove(vremove) self[key] = envval # TODO(sgk): SCons.Environment.Append() has much more logic to deal # with various types of values. We should handle all those cases in here - # too. + # too. (If variable is a dict, etc.) #------------------------------------------------------------------------------ @@ -83,23 +85,15 @@ def Overlap(self, values1, values2): Returns: The list of values in common after substitution, or an empty list if - the values do no overlap. + the values do not overlap. - Converts the values to a set of plain strings via self.subst() before + Converts the values to a set of plain strings via self.SubstList2() before comparison, so SCons $ variables are evaluated. """ - - set1 = set() - for v in self.Flatten(values1): - set1.add(self.subst(v)) - - set2 = set() - for v in self.Flatten(values2): - set2.add(self.subst(v)) - + set1 = set(self.SubstList2(values1)) + set2 = set(self.SubstList2(values2)) return list(set1.intersection(set2)) - #------------------------------------------------------------------------------ @@ -137,7 +131,7 @@ def ApplySConscript(self, sconscript_file): If you need to export multiple variables to the called SConscript, or return variables from it, use the existing SConscript() function. """ - return SCons.Script.SConscript(sconscript_file, exports={'env':self}) + return SCons.Script.SConscript(sconscript_file, exports={'env': self}) #------------------------------------------------------------------------------ @@ -194,7 +188,85 @@ def BuildSConscript(self, sconscript_file): else: script_file = sconscript_file - self.SConscript(script_file, exports={'env':self.Clone()}) + self.SConscript(script_file, exports={'env': self.Clone()}) + +#------------------------------------------------------------------------------ + + +def SubstList2(self, *args): + """Replacement subst_list designed for flags/parameters, not command lines. + + Args: + self: Environment context. + args: One or more strings or lists of strings. + + Returns: + A flattened, substituted list of strings. + + SCons's built-in subst_list evaluates (substitutes) variables in its + arguments, and returns a list of lists (one per positional argument). Since + it is designed for use in command line expansion, the list items are + SCons.Subst.CmdStringHolder instances. These instances can't be passed into + env.File() (or subsequent calls to env.subst(), either). The returned + nested lists also need to be flattened via env.Flatten() before the caller + can iterate over the contents. + + SubstList2() does a subst_list, flattens the result, then maps the flattened + list to strings. + + It is better to do: + for x in env.SubstList2('$MYPARAMS'): + than to do: + for x in env.get('MYPARAMS', []): + and definitely better than: + for x in env['MYPARAMS']: + which will throw an exception if MYPARAMS isn't defined. + """ + return map(str, self.Flatten(self.subst_list(args))) + + +#------------------------------------------------------------------------------ + + +def RelativePath(self, source, target, sep=os.sep, source_is_file=False): + """Calculates the relative path from source to target. + + Args: + self: Environment context. + source: Source path or node. + target: Target path or node. + sep: Path separator to use in returned relative path. + source_is_file: If true, calculates the relative path from the directory + containing the source, rather than the source itself. Note that if + source is a node, you can pass in source.dir instead, which is shorter. + + Returns: + The relative path from source to target. + """ + # Split source and target into list of directories + source = self.Entry(str(source)) + if source_is_file: + source = source.dir + source = source.abspath.split(os.sep) + target = self.Entry(str(target)).abspath.split(os.sep) + + # Handle source and target identical + if source == target: + if source_is_file: + return source[-1] # Bare filename + else: + return '.' # Directory pointing to itself + + # TODO(rspangler): Handle UNC paths and drive letters (fine if they're the + # same, but if they're different, there IS no relative path) + + # Remove common elements + while source and target and source[0] == target[0]: + source.pop(0) + target.pop(0) + # Join the remaining elements + return sep.join(['..'] * len(source) + target) + #------------------------------------------------------------------------------ @@ -208,3 +280,5 @@ def generate(env): env.AddMethod(BuildSConscript) env.AddMethod(FilterOut) env.AddMethod(Overlap) + env.AddMethod(RelativePath) + env.AddMethod(SubstList2) diff --git a/site_scons/site_tools/publish.py b/site_scons/site_tools/publish.py index ebd6acf..bf6a737 100644 --- a/site_scons/site_tools/publish.py +++ b/site_scons/site_tools/publish.py @@ -31,7 +31,10 @@ """Publish tool for SCons.""" -__published = {} # List of published resources +# List of published resources. This is a dict indexed by group name. Each +# item in this dict is a dict indexed by resource type. Items in that dict +# are lists of files for that resource. +__published = {} #------------------------------------------------------------------------------ @@ -54,13 +57,13 @@ class PublishItem(object): #------------------------------------------------------------------------------ -def _InitializePublish(self): +def _InitializePublish(env): """Re-initializes published resources. Args: - self: Parent environment + env: Parent environment """ - self=self # Silence gpylint + env=env # Silence gpylint # Clear the dict of published resources __published.clear() @@ -87,10 +90,10 @@ def ReplicatePublished(self, target, group_name, resource_type): target_path = self.Dir(target).abspath dest_nodes = [] - for group in self.Flatten(group_name): - for resource in self.Flatten(resource_type): + for group in self.SubstList2(group_name): + for resource in self.SubstList2(resource_type): # Get items for publish group and resource type - items = __published.get(self.subst(group), {}).get(resource, []) + items = __published.get(group, {}).get(resource, []) for i in items: if i.subdir: dest_nodes += self.Replicate(target_path + '/' + i.subdir, i.source) @@ -113,10 +116,10 @@ def GetPublished(self, group_name, resource_type): no matching resources. """ source_list = [] - for group in self.Flatten(group_name): + for group in self.SubstList2(group_name): # Get items for publish group and resource type - for resource in self.Flatten(resource_type): - items = __published.get(self.subst(group), {}).get(resource, []) + for resource in self.SubstList2(resource_type): + items = __published.get(group, {}).get(resource, []) for i in items: source_list.append(i.source) @@ -141,13 +144,17 @@ def Publish(self, group_name, resource_type, source, subdir=None): subdir = '' # Make string so we can append to it # Evaluate SCons variables in group name + # TODO(rspangler): Should Publish() be able to take a list of group names + # and publish the resource to all of them? group_name = self.subst(group_name) # Get list of sources items = [] for source_entry in self.Flatten(source): - if type(source_entry) == str: + if isinstance(source_entry, str): # Search for matches for each source entry + # TODO(rspangler): If there are no wildcard chars in the source entry, + # should generate an error if there were no matches? source_nodes = self.Glob(source_entry) else: # Source entry is already a file or directory node; no need to glob it @@ -179,7 +186,10 @@ def generate(env): # NOTE: SCons requires the use of this name, which fails gpylint. """SCons entry point for this tool.""" - env.AddMethod(_InitializePublish) + # Defer initializing publish, but do before building SConscripts + env.Defer(_InitializePublish) + env.Defer('BuildEnvironmentSConscripts', after=_InitializePublish) + env.AddMethod(GetPublished) env.AddMethod(Publish) env.AddMethod(ReplicatePublished) diff --git a/site_scons/site_tools/replicate.py b/site_scons/site_tools/replicate.py index 6547167b..b11ce14 100644 --- a/site_scons/site_tools/replicate.py +++ b/site_scons/site_tools/replicate.py @@ -105,9 +105,12 @@ def Replicate(env, target, source, **kw): target_name = re.sub(r[0], r[1], target_name) target = env.File(target_name) if (target.has_builder() + and hasattr(target.get_builder(), 'name') and target.get_builder().name == 'InstallBuilder' and target.sources == [s]): # Already installed that file, so pass through the destination node + # TODO(rspangler): Is there a better way to determine if this is a + # duplicate install? dest_nodes += [target] else: dest_nodes += env.InstallAs(target_name, s) diff --git a/site_scons/site_tools/target_platform_mac.py b/site_scons/site_tools/target_platform_mac.py index 3034f36..4c35334 100644 --- a/site_scons/site_tools/target_platform_mac.py +++ b/site_scons/site_tools/target_platform_mac.py @@ -113,6 +113,9 @@ def BundlePseudoBuilder(env, target, **kwargs): return env.Dir(target) +#------------------------------------------------------------------------------ + + def generate(env): # NOTE: SCons requires the use of this name, which fails gpylint. """SCons entry point for this tool.""" @@ -140,7 +143,6 @@ def generate(env): env.Append( HOST_PLATFORMS=['MAC'], CPPDEFINES=['OS_MACOSX=OS_MACOSX'], - BITS=['mac', 'posix'], # Settings for debug CCFLAGS_DEBUG=['-g'], diff --git a/site_scons/site_tools/visual_studio_solution.py b/site_scons/site_tools/visual_studio_solution.py index eae83fd..4c18d23 100644 --- a/site_scons/site_tools/visual_studio_solution.py +++ b/site_scons/site_tools/visual_studio_solution.py @@ -71,7 +71,7 @@ def Solution(env, solution_name, build_targets = [e.subst('$TARGET_ROOT') for e in environments] # pick out sources, headers, and resources sources, headers, resources, others = env.GatherInputs( - env.Dir('$DESTINATION_ROOT'), + [SCons.Script.Dir('$DESTINATION_ROOT')], ['.+\\.(c|cc|m|mm|cpp)$', # source files '.+\\.(h|hh|hpp)$', # header files '.+\\.(rc)$', # resource files @@ -79,18 +79,19 @@ def Solution(env, solution_name, exclude_pattern=exclude_pattern, ) # Build main Visual Studio Project file - project_list = env.MSVSProject( - target=solution_name + env['MSVSPROJECTSUFFIX'], - srcs=sources + headers + others + resources, - incs=[], - misc=[], - resources=[], - auto_build_solution=0, - MSVSCLEANCOM='hammer.bat -c MODE=all', - MSVSBUILDCOM='hammer.bat MODE=all', - MSVSREBUILD='hammer.bat -c MODE=all; hammer.bat MODE=all', - buildtarget=build_targets, - variant=variants) + project_list = env.MSVSProject(target=solution_name + + env['MSVSPROJECTSUFFIX'], + srcs=sources + headers + others + resources, + incs=[], + misc=[], + resources=[], + auto_build_solution=0, + MSVSCLEANCOM='hammer.bat -c MODE=all', + MSVSBUILDCOM='hammer.bat MODE=all', + MSVSREBUILD='hammer.bat -c MODE=all;' + 'hammer.bat MODE=all', + buildtarget=build_targets, + variant=variants) # Collect other projects for e in extra_build_targets: # Explicitly create a node for target, so SCons will expand env variables. |