Add customizable variable helper. (Anthony Roach)
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Thu, 26 Sep 2002 00:54:35 +0000 (00:54 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Thu, 26 Sep 2002 00:54:35 +0000 (00:54 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@469 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/CHANGES.txt
src/engine/MANIFEST.in
src/engine/SCons/Environment.py
src/engine/SCons/Options.py [new file with mode: 0644]
src/engine/SCons/OptionsTests.py [new file with mode: 0644]
src/engine/SCons/Script/SConscript.py
test/Options.py [new file with mode: 0644]

index 13c31d7470a8c30ce3edd176fb35397915dbe421..7bb09bb6ca3262875ae8c27916c046fe3cb409cc 100644 (file)
@@ -1940,6 +1940,83 @@ method:
 env2 = env.Copy(CC="cl.exe")
 .EE
 
+.SS Costruction Variable Options
+
+Often when building software, various options need to be specified at build
+time that are not known when the SConstruct/SConscript files are
+written. For example, libraries needed for the build may be in non-standard
+locations, or site-specific compiler options may need to be passed to the
+compiler. 
+.B scons
+provides a mechanism for overridding construction variables from the
+command line or a text based configuration file through an Options
+object. To create an Options object, call the Options() function:
+
+.TP
+.RI Options([ file ])
+This creates an Options object that will read construction variables from
+the filename based in the 
+.I file
+argument. If no filename is given, then no file will be read. Example:
+
+.ES
+opts = Options('custom.py')
+.EE
+
+Options objects have the following methods:
+
+.TP
+.RI Add( key ", [" help ", " default ", " validater ", " converter ])
+This adds a customizable construction variable to the Options object. 
+.I key
+is the name of the variable. 
+.I help 
+is the help text for the variable.
+.I default 
+is the default value of the variable.
+.I validater
+is called to validate the value of the variable, and should take two
+arguments: key and value.
+.I converter
+is called to convert the value before putting it in the environment, and
+should take a single argument: value. Example:
+
+.ES
+opts.Add('CC', 'The C compiler')
+.EE
+
+.TP
+.RI Update( env )
+This updates a construction environment
+.I env
+with the customized construction variables. Normally this method is not
+called directly, but is called indirectly by passing the Options object to
+the Environment() function:
+
+.ES
+env = Environment(options=opts)
+.EE
+
+.TP
+.RI GenerateHelpText( env )
+This generates help text documenting the customizable construction
+variables suitable to passing in to the Help() function. 
+.I env
+is the construction environment that will be used to get the actual values
+of customizable variables. Example:
+
+.ES
+Help(opts.GenerateHelpText(env))
+.EE
+
+The text based configuration file is executed as a Python script, and the
+global variables are queried for customizable construction
+variables. Example:
+
+.ES
+CC = 'my_cc'
+.EE
+
 .SS Other Functions
 
 .B scons
@@ -3048,6 +3125,41 @@ prefix and suffix for the current platform
 (for example, 'liba.a' on POSIX systems,
 'a.lib' on Windows).
 
+.SS Customizing contruction variables from the command line.
+
+The following would allow the C compiler to be specified on the command
+line or in the file custom.py. 
+
+.ES
+opts = Options('custom.py')
+opts.Add('CC', 'The C compiler.')
+env = Environment(options=opts)
+Help(opts.GenerateHelpText(env))
+.EE
+
+The user could specify the C compiler on the command line:
+
+.ES
+scons "CC=my_cc"
+.EE
+
+or in the custom.py file:
+
+.ES
+CC = 'my_cc'
+.EE
+
+or get documentation on the options:
+
+.ES
+> scons -h
+
+CC: The C compiler.
+    default: None
+    actual: cc
+
+.EE
+
 .SH ENVIRONMENT
 
 .IP SCONS_LIB_DIR
index ba3ccb16617bd7558162335c2caf67855d84de61..86737fc34b83e93d553eb1fe70646ec7754aa863 100644 (file)
@@ -69,6 +69,9 @@ RELEASE 0.09 -
 
   - Fix use of -j with multiple targets.
 
+  - Add an Options() object for friendlier accomodation of command-
+    line arguments.
+
   From sam th:
 
   - Dynamically check for the existence of utilities with which to
index 4441f5a0dcea88ec3fdd485d1e57ffb0948cda3e..bca093f8630506d153d592eb82056f3bd1a22e2e 100644 (file)
@@ -9,6 +9,7 @@ SCons/exitfuncs.py
 SCons/Node/__init__.py
 SCons/Node/Alias.py
 SCons/Node/FS.py
+SCons/Options.py
 SCons/Platform/__init__.py
 SCons/Platform/cygwin.py
 SCons/Platform/os2.py
index d8a87a42fedc1a66048498a40d6ae1ce05e71313..a512d93a1a49aa9a6623c4653f193daacadb45a5 100644 (file)
@@ -122,21 +122,41 @@ class Environment:
     def __init__(self,
                  platform=SCons.Platform.Platform(),
                  tools=None,
+                 options=None,
                  **kw):
         self.fs = SCons.Node.FS.default_fs
         self._dict = our_deepcopy(SCons.Defaults.ConstructionEnvironment)
+
         if SCons.Util.is_String(platform):
             platform = SCons.Platform.Platform(platform)
         platform(self)
+
+        # Apply the passed-in variables before calling the tools,
+        # because they may use some of them:
+        apply(self.Replace, (), kw)
+        
+        # Update the environment with the customizable options
+        # before calling the tools, since they may use some of the options: 
+        if options:
+            options.Update(self)
+
         if tools is None:
             tools = ['default']
-        apply(self.Replace, (), kw)
         for tool in tools:
             if SCons.Util.is_String(tool):
                 tool = SCons.Tool.Tool(tool)
             tool(self, platform)
+
+        # Reapply the passed in variables after calling the tools,
+        # since they should overide anything set by the tools:
         apply(self.Replace, (), kw)
 
+        # Update the environment with the customizable options
+        # after calling the tools, since they should override anything
+        # set by the tools:
+        if options:
+            options.Update(self)
+
         #
         # self.autogen_vars is a tuple of tuples.  Each inner tuple
         # has four elements, each strings referring to an environment
diff --git a/src/engine/SCons/Options.py b/src/engine/SCons/Options.py
new file mode 100644 (file)
index 0000000..becee14
--- /dev/null
@@ -0,0 +1,128 @@
+"""engine.SCons.Options
+
+This file defines the Options class that is used to add user-friendly customizable
+variables to a scons build.
+"""
+
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import SCons.Errors
+import os.path
+
+
+class Options:
+    """
+    Holds all the options, updates the environment with the variables,
+    and renders the help text.
+    """
+    def __init__(self, file=None):
+        """
+        file - [optional] the name of the customizable file.
+        """
+
+        self.options = []
+        self.file = file
+
+    def Add(self, key, help="", default=None, validater=None, converter=None):
+        """
+        Add an option.
+
+        key - the name of the variable
+        help - optional help text for the options
+        default - optional default value
+        validater - optional function that is called to validate the option's value
+        converter - optional function that is called to convert the option's value before
+            putting it in the environment.
+        """
+
+        class Option:
+            pass
+
+        option = Option()
+        option.key = key
+        option.help = help
+        option.default = default
+        option.validater = validater
+        option.converter = converter
+
+        self.options.append(option)
+
+    def Update(self, env, args):
+        """
+        Update an environment with the option variables.
+
+        env - the environment to update.
+        args - the dictionary to get the command line arguments from.
+        """
+
+        values = {}
+
+        # first set the defaults:
+        for option in self.options:
+            if not option.default is None:
+                values[option.key] = option.default
+
+        # next set the value specified in the options file
+        if self.file and os.path.exists(self.file):
+            execfile(self.file, values)
+
+        # finally set the values specified on the command line
+        values.update(args)
+
+        # put the variables in the environment:
+        for key in values.keys():
+            env[key] = values[key]
+
+        # Call the convert functions:
+        for option in self.options:
+            if option.converter:
+                value = env.subst('${%s}'%option.key)
+                try:
+                    env[option.key] = option.converter(value)
+                except ValueError, x:
+                    raise SCons.Errors.UserError, 'Error converting option: %s\n%s'%(options.key, x)
+
+
+        # Finally validate the values:
+        for option in self.options:
+            if option.validater:
+                option.validater(option.key, env.subst('${%s}'%option.key))
+
+
+    def GenerateHelpText(self, env):
+        """
+        Generate the help text for the options.
+
+        env - an environment that is used to get the current values of the options.
+        """
+
+        help_text = ""
+
+        for option in self.options:
+            help_text = help_text + '\n%s: %s\n    default: %s\n    actual: %s\n'%(option.key, option.help, option.default, env.subst('${%s}'%option.key))
+
+        return help_text
+
diff --git a/src/engine/SCons/OptionsTests.py b/src/engine/SCons/OptionsTests.py
new file mode 100644 (file)
index 0000000..ec9e42f
--- /dev/null
@@ -0,0 +1,157 @@
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import unittest
+import TestSCons
+import SCons.Options
+import sys
+import string
+
+class Environment:
+    def __init__(self):
+        self.dict = {}
+    def subst(self, x):
+        return self.dict[x[2:-1]]
+    def __setitem__(self, key, value):
+        self.dict[key] = value
+    def __getitem__(self, key):
+        return self.dict[key]
+
+
+def check(key,value):
+    assert value == 6 * 9,key
+
+class OptionsTestCase(unittest.TestCase):
+    def test_Add(self):
+        opts = SCons.Options.Options()
+
+        opts.Add('VAR')
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "42",
+                 check,
+                 lambda x: int(x) + 12)
+
+        o = opts.options[0]
+        assert o.key == 'VAR'
+        assert o.help == ''
+        assert o.default == None
+        assert o.validater == None
+        assert o.converter == None
+
+        o = opts.options[1]
+        assert o.key == 'ANSWER'
+        assert o.help == 'THE answer to THE question'
+        assert o.default == "42"
+        o.validater(o.key, o.converter(o.default))
+
+    def test_Update(self):
+
+        test = TestSCons.TestSCons()
+        file = test.workpath('custom.py')
+        opts = SCons.Options.Options(file)
+        
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "42",
+                 check,
+                 lambda x: int(x) + 12)
+
+        env = Environment()
+        opts.Update(env, {})
+        assert env['ANSWER'] == 54
+
+        test = TestSCons.TestSCons()
+        file = test.workpath('custom.py')
+        test.write('custom.py', 'ANSWER=54')
+        opts = SCons.Options.Options(file)
+        
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "42",
+                 check,
+                 lambda x: int(x) + 12)
+
+        env = Environment()
+        try:
+            opts.Update(env, {})
+        except AssertionError:
+            pass
+
+        test = TestSCons.TestSCons()
+        file = test.workpath('custom.py')
+        test.write('custom.py', 'ANSWER=42')
+        opts = SCons.Options.Options(file)
+        
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "54",
+                 check,
+                 lambda x: int(x) + 12)
+
+        env = Environment()
+        opts.Update(env, {})
+        assert env['ANSWER'] == 54
+
+        test = TestSCons.TestSCons()
+        file = test.workpath('custom.py')
+        test.write('custom.py', 'ANSWER=54')
+        opts = SCons.Options.Options(file)
+        
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "54",
+                 check,
+                 lambda x: int(x) + 12)
+
+        env = Environment()
+        opts.Update(env, {'ANSWER':'42'})
+        assert env['ANSWER'] == 54
+
+    def test_GenerateHelpText(self):
+        opts = SCons.Options.Options()
+
+        opts.Add('ANSWER',
+                 'THE answer to THE question',
+                 "42",
+                 check,
+                 lambda x: int(x) + 12)
+
+        env = Environment()
+        opts.Update(env, {})
+
+        expect = """
+ANSWER: THE answer to THE question
+    default: 42
+    actual: 54
+"""
+
+        text = opts.GenerateHelpText(env)
+        assert text == expect, text
+        
+if __name__ == "__main__":
+    suite = unittest.makeSuite(OptionsTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful():
+       sys.exit(1)
index a1412975f95e0595663d316eacdfbb1c1e83634d..a8ce5d78c1c998c4a10b2988160484cb38ca93df 100644 (file)
@@ -40,6 +40,7 @@ import SCons.Platform
 import SCons.Tool
 import SCons.Util
 import SCons.Sig
+import SCons.Options
 
 import os
 import os.path
@@ -264,6 +265,10 @@ def SetBuildSignatureType(type):
     else:
         raise SCons.Errors.UserError, "Unknown build signature type '%s'"%type
 
+class Options(SCons.Options.Options):
+    def Update(self, env):
+        return SCons.Options.Options.Update(self, env, arguments)
+
 def BuildDefaultGlobals():
     """
     Create a dictionary containing all the default globals for 
@@ -306,4 +311,5 @@ def BuildDefaultGlobals():
     globals['Split']             = SCons.Util.Split
     globals['Tool']              = SCons.Tool.Tool
     globals['WhereIs']           = SCons.Util.WhereIs
+    globals['Options']           = Options
     return globals
diff --git a/test/Options.py b/test/Options.py
new file mode 100644 (file)
index 0000000..2b2d5a2
--- /dev/null
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import TestSCons
+import string
+
+test = TestSCons.TestSCons()
+
+test.write('SConstruct', """
+env = Environment()
+print env['CC']
+print env['CCFLAGS']
+Default(env.Alias('dummy'))
+""")
+test.run()
+cc, ccflags = string.split(test.stdout(), '\n')[:2]
+
+test.write('SConstruct', """
+opts = Options('custom.py')
+opts.Add('RELEASE_BUILD',
+         'Set to 1 to build a release build',
+         0,
+         None,
+         int)
+
+opts.Add('DEBUG_BUILD',
+         'Set to 1 to build a debug build',
+         1,
+         None,
+         int)
+
+opts.Add('CC',
+         'The C compiler')
+
+def test_tool(env, platform):
+    if env['RELEASE_BUILD']:
+        env['CCFLAGS'] = env['CCFLAGS'] + ' -O'
+    if env['DEBUG_BUILD']:
+        env['CCFLAGS'] = env['CCFLAGS'] + ' -g'
+    
+
+env = Environment(options=opts, tools=['default', test_tool])
+
+Help('Variables settable in custom.py or on the command line:\\n' + opts.GenerateHelpText(env))
+
+print env['RELEASE_BUILD']
+print env['DEBUG_BUILD']
+print env['CC']
+print env['CCFLAGS']
+
+Default(env.Alias('dummy'))
+        
+""")
+
+def check(expect):
+    result = string.split(test.stdout(), '\n')
+    assert result[0:len(expect)] == expect, (result[0:len(expect)], expect)
+
+test.run()
+check(['0', '1', cc, ccflags + ' -g'])
+
+test.run(arguments='"RELEASE_BUILD=1"')
+check(['1', '1', cc, ccflags + ' -O -g'])
+
+test.run(arguments='"RELEASE_BUILD=1" "DEBUG_BUILD=0"')
+check(['1', '0', cc, ccflags + ' -O'])
+
+test.run(arguments='"CC=not_a_c_compiler"')
+check(['0', '1', 'not_a_c_compiler', ccflags + ' -g'])
+
+test.write('custom.py', """
+DEBUG_BUILD=0
+RELEASE_BUILD=1
+""")
+
+test.run()
+check(['1', '0', cc, ccflags + ' -O'])
+
+test.run(arguments='"DEBUG_BUILD=1"')
+check(['1', '1', cc, ccflags + ' -O -g'])
+   
+test.run(arguments='-h')
+assert test.stdout() == """Variables settable in custom.py or on the command line:
+
+RELEASE_BUILD: Set to 1 to build a release build
+    default: 0
+    actual: 1
+
+DEBUG_BUILD: Set to 1 to build a debug build
+    default: 1
+    actual: 0
+
+CC: The C compiler
+    default: None
+    actual: %s
+
+Use scons -H for help about command-line options.
+"""%cc
+
+test.pass_test()