From 1523e6f372549807f31962bfbb9d429ead2db9d2 Mon Sep 17 00:00:00 2001 From: Steven Knight Date: Thu, 26 Sep 2002 00:54:35 +0000 Subject: Add customizable variable helper. (Anthony Roach) --- doc/man/scons.1 | 112 ++++++++++++++++++++++++ src/CHANGES.txt | 3 + src/engine/MANIFEST.in | 1 + src/engine/SCons/Environment.py | 22 ++++- src/engine/SCons/Options.py | 128 +++++++++++++++++++++++++++ src/engine/SCons/OptionsTests.py | 157 ++++++++++++++++++++++++++++++++++ src/engine/SCons/Script/SConscript.py | 6 ++ test/Options.py | 123 ++++++++++++++++++++++++++ 8 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 src/engine/SCons/Options.py create mode 100644 src/engine/SCons/OptionsTests.py create mode 100644 test/Options.py diff --git a/doc/man/scons.1 b/doc/man/scons.1 index 13c31d7..7bb09bb 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -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 diff --git a/src/CHANGES.txt b/src/CHANGES.txt index ba3ccb1..86737fc 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -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 diff --git a/src/engine/MANIFEST.in b/src/engine/MANIFEST.in index 4441f5a..bca093f 100644 --- a/src/engine/MANIFEST.in +++ b/src/engine/MANIFEST.in @@ -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 diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index d8a87a4..a512d93 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -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 index 0000000..becee14 --- /dev/null +++ b/src/engine/SCons/Options.py @@ -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 index 0000000..ec9e42f --- /dev/null +++ b/src/engine/SCons/OptionsTests.py @@ -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) diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py index a141297..a8ce5d7 100644 --- a/src/engine/SCons/Script/SConscript.py +++ b/src/engine/SCons/Script/SConscript.py @@ -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 index 0000000..2b2d5a2 --- /dev/null +++ b/test/Options.py @@ -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() -- cgit v0.12