3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """Adds user-friendly customizable variables to an SCons build."""
26 from __future__
import annotations
30 from contextlib
import suppress
31 from dataclasses
import dataclass
32 from functools
import cmp_to_key
33 from typing
import Any
, Callable
, Sequence
39 # Note: imports are for the benefit of SCons.Main (and tests); since they
40 # are not used here, the "as Foo" form is for checkers.
41 from .BoolVariable
import BoolVariable
42 from .EnumVariable
import EnumVariable
43 from .ListVariable
import ListVariable
44 from .PackageVariable
import PackageVariable
45 from .PathVariable
import PathVariable
58 @dataclass(order
=True)
60 """A Build Variable."""
61 __slots__
= ('key', 'aliases', 'help', 'default', 'validator', 'converter', 'do_subst')
66 validator
: Callable |
None
67 converter
: Callable |
None
72 """A container for Build Variables.
74 Includes a method to populate the variables with values into a
75 construction envirionment, and methods to render the help text.
77 Note that the pubic API for creating a ``Variables`` object is
78 :func:`SCons.Script.Variables`, a kind of factory function, which
79 defaults to supplying the contents of :attr:`~SCons.Script.ARGUMENTS`
80 as the *args* parameter if it was not otherwise given. That is the
81 behavior documented in the manpage for ``Variables`` - and different
82 from the default if you instantiate this directly.
85 files: string or list of strings naming variable config scripts
87 args: dictionary to override values set from *files*. (default ``None``)
88 is_global: if true, return a global singleton :class:`Variables` object
89 instead of a fresh instance. Currently inoperable (default ``False``)
91 .. versionchanged:: 4.8.0
92 The default for *is_global* changed to ``False`` (the previous
93 default ``True`` had no effect due to an implementation error).
96 *is_global* is deprecated.
98 .. versionadded:: NEXT_RELEASE
99 The :attr:`defaulted` attribute now lists those variables which
100 were filled in from default values.
105 files
: str | Sequence
[str |
None] = None,
106 args
: dict |
None = None,
107 is_global
: bool = False,
109 self
.options
: list[Variable
] = []
110 self
.args
= args
if args
is not None else {}
111 if not SCons
.Util
.is_Sequence(files
):
112 files
= [files
] if files
else []
113 self
.files
: Sequence
[str] = files
114 self
.unknown
: dict[str, str] = {}
115 self
.defaulted
: list[str] = []
117 def __str__(self
) -> str:
118 """Provide a way to "print" a :class:`Variables` object."""
119 opts
= ',\n'.join((f
" {option!s}" for option
in self
.options
))
121 f
"Variables(\n options=[\n{opts}\n ],\n"
122 f
" args={self.args},\n"
123 f
" files={self.files},\n"
124 f
" unknown={self.unknown},\n"
125 f
" defaulted={self.defaulted},\n)"
128 # lint: W0622: Redefining built-in 'help'
131 key
: str | Sequence
[str],
134 validator
: Callable |
None = None,
135 converter
: Callable |
None = None,
138 """Create a :class:`Variable` and add it to the list.
140 This is the internal implementation for :meth:`Add` and
141 :meth:`AddVariables`. Not part of the public API.
143 .. versionadded:: 4.8.0
144 *subst* keyword argument is now recognized.
146 # aliases needs to be a list for later concatenation operations
147 if SCons
.Util
.is_Sequence(key
):
148 name
, aliases
= key
[0], list(key
[1:])
150 name
, aliases
= key
, []
151 if not name
.isidentifier():
152 raise SCons
.Errors
.UserError(f
"Illegal Variables key {name!r}")
153 do_subst
= kwargs
.pop("subst", True)
154 option
= Variable(name
, aliases
, help, default
, validator
, converter
, do_subst
)
155 self
.options
.append(option
)
157 # options might be added after the 'unknown' dict has been set up:
158 # look for and remove the key and all its aliases from that dict
159 for alias
in option
.aliases
+ [option
.key
,]:
160 if alias
in self
.unknown
:
161 del self
.unknown
[alias
]
163 def keys(self
) -> list:
164 """Return the variable names."""
165 for option
in self
.options
:
169 self
, key
: str | Sequence
, *args
, **kwargs
,
171 """Add a Build Variable.
174 key: the name of the variable, or a 5-tuple (or other sequence).
175 If *key* is a tuple, and there are no additional arguments
176 except the *help*, *default*, *validator* and *converter*
177 keyword arguments, *key* is unpacked into the variable name
178 plus the *help*, *default*, *validator* and *converter*
179 arguments; if there are additional arguments, the first
180 elements of *key* is taken as the variable name, and the
181 remainder as aliases.
182 args: optional positional arguments, corresponding to the
183 *help*, *default*, *validator* and *converter* keyword args.
184 kwargs: arbitrary keyword arguments used by the variable itself.
187 help: help text for the variable (default: empty string)
188 default: default value for variable (default: ``None``)
189 validator: function called to validate the value (default: ``None``)
190 converter: function to be called to convert the variable's
191 value before putting it in the environment. (default: ``None``)
192 subst: perform substitution on the value before the converter
193 and validator functions (if any) are called (default: ``True``)
195 .. versionadded:: 4.8.0
196 The *subst* keyword argument is now specially recognized.
198 if SCons
.Util
.is_Sequence(key
):
199 # If no other positional args (and no fundamental kwargs),
200 # unpack key, and pass the kwargs on:
201 known_kw
= {'help', 'default', 'validator', 'converter'}
202 if not args
and not known_kw
.intersection(kwargs
.keys()):
203 return self
._do
_add
(*key
, **kwargs
)
205 return self
._do
_add
(key
, *args
, **kwargs
)
207 def AddVariables(self
, *optlist
) -> None:
208 """Add Build Variables.
210 Each *optlist* element is a sequence of arguments to be passed on
211 to the underlying method for adding variables.
218 ('CC', 'The C compiler'),
219 ('VALIDATE', 'An option for testing validation', 'notset', validator, None),
225 def Update(self
, env
, args
: dict |
None = None) -> None:
226 """Update an environment with the Build Variables.
228 This is where the work of adding variables to the environment
229 happens, The input sources saved at init time are scanned for
230 variables to add, though if *args* is passed, then it is used
231 instead of the saved one. If any variable description set up
232 a callback for a validator and/or converter, those are called.
233 Variables from the input sources which do not match a variable
234 description in this object are ignored for purposes of adding
235 to *env*, but are saved in the :attr:`unknown` dict attribute.
236 Variables which are set in *env* from the default in a variable
237 description and not from the input sources are saved in the
238 :attr:`defaulted` list attribute.
241 env: the environment to update.
242 args: a dictionary of keys and values to update in *env*.
243 If omitted, uses the saved :attr:`args`
245 # first pull in the defaults, except any which are None.
246 values
= {opt
.key
: opt
.default
for opt
in self
.options
if opt
.default
is not None}
247 self
.defaulted
= list(values
)
249 # next set the values specified in any saved-variables script(s)
250 for filename
in self
.files
:
251 # TODO: issue #816 use Node to access saved-variables file?
252 if os
.path
.exists(filename
):
253 # issue #4645: don't exec directly into values,
254 # so we can iterate through for unknown variables.
256 dirname
= os
.path
.split(os
.path
.abspath(filename
))[0]
258 sys
.path
.insert(0, dirname
)
260 temp_values
['__name__'] = filename
261 with
open(filename
) as f
:
263 exec(contents
, {}, temp_values
)
267 del temp_values
['__name__']
269 for arg
, value
in temp_values
.items():
271 for option
in self
.options
:
272 if arg
in option
.aliases
+ [option
.key
,]:
273 values
[option
.key
] = value
274 with
suppress(ValueError):
275 self
.defaulted
.remove(option
.key
)
278 self
.unknown
[arg
] = value
280 # set the values specified on the command line
284 for arg
, value
in args
.items():
286 for option
in self
.options
:
287 if arg
in option
.aliases
+ [option
.key
,]:
288 values
[option
.key
] = value
289 with
suppress(ValueError):
290 self
.defaulted
.remove(option
.key
)
293 self
.unknown
[arg
] = value
295 # put the variables in the environment
296 # (don't copy over variables that are not declared as options)
298 # Nitpicking: in OO terms, this method increases coupling as its
299 # main work is to update a different object (env), rather than
300 # the object it's bound to (although it does update self, too).
301 # It's tricky to decouple because the algorithm counts on directly
302 # setting a var in *env* first so it can call env.subst() on it
305 for option
in self
.options
:
307 env
[option
.key
] = values
[option
.key
]
312 for option
in self
.options
:
313 if option
.converter
and option
.key
in values
:
315 value
= env
.subst('${%s}' % option
.key
)
317 value
= env
[option
.key
]
320 env
[option
.key
] = option
.converter(value
)
322 env
[option
.key
] = option
.converter(value
, env
)
323 except ValueError as exc
:
324 # We usually want the converter not to fail and leave
325 # that to the validator, but in case, handle it.
326 msg
= f
'Error converting option: {option.key!r}\n{exc}'
327 raise SCons
.Errors
.UserError(msg
) from exc
330 for option
in self
.options
:
331 if option
.validator
and option
.key
in values
:
333 val
= env
[option
.key
]
334 if not SCons
.Util
.is_String(val
):
335 # issue #4585: a _ListVariable should not be further
336 # substituted, breaks on values with spaces.
339 value
= env
.subst('${%s}' % option
.key
)
341 value
= env
[option
.key
]
342 option
.validator(option
.key
, value
, env
)
344 def UnknownVariables(self
) -> dict:
345 """Return dict of unknown variables.
347 Identifies variables that were not recognized in this object.
351 def Save(self
, filename
, env
) -> None:
352 """Save the variables to a script.
354 Saves all the variables which have non-default settings
355 to the given file as Python expressions. This script can
356 then be used to load the variables for a subsequent run.
357 This can be used to create a build variable "cache" or
358 capture different configurations for selection.
361 filename: Name of the file to save into
362 env: the environment to get the option values from
364 # Create the file and write out the header
366 # TODO: issue #816 use Node to access saved-variables file?
367 with
open(filename
, 'w') as fh
:
368 # Make an assignment in the file for each option
369 # within the environment that was assigned a value
370 # other than the default. We don't want to save the
371 # ones set to default: in case the SConscript settings
372 # change you would then pick up old defaults.
373 for option
in self
.options
:
375 value
= env
[option
.key
]
377 prepare
= value
.prepare_to_store
378 except AttributeError:
381 except KeyboardInterrupt:
384 # Convert stuff that has a repr() that
385 # cannot be evaluated into a string
386 value
= SCons
.Util
.to_String(value
)
390 defaultVal
= env
.subst(SCons
.Util
.to_String(option
.default
))
393 defaultVal
= option
.converter(defaultVal
)
395 defaultVal
= option
.converter(defaultVal
, env
)
397 if str(env
.subst(f
'${option.key}')) != str(defaultVal
):
398 fh
.write(f
'{option.key} = {value!r}\n')
401 except OSError as exc
:
402 msg
= f
'Error writing options to file: {filename}\n{exc}'
403 raise SCons
.Errors
.UserError(msg
) from exc
405 def GenerateHelpText(self
, env
, sort
: bool | Callable
= False) -> str:
406 """Generate the help text for the Variables object.
409 env: an environment that is used to get the current values
411 sort: Either a comparison function used for sorting
412 (must take two arguments and return ``-1``, ``0`` or ``1``)
413 or a boolean to indicate if it should be sorted.
415 # TODO this interface was designed when Python's sorted() took an
416 # optional comparison function (pre-3.0). Since it no longer does,
417 # we use functools.cmp_to_key() since can't really change the
418 # documented meaning of the "sort" argument. Maybe someday?
420 options
= sorted(self
.options
, key
=cmp_to_key(lambda x
, y
: sort(x
.key
, y
.key
)))
422 options
= sorted(self
.options
)
424 options
= self
.options
426 def format_opt(opt
, self
=self
, env
=env
) -> str:
428 actual
= env
.subst(f
'${opt.key}')
431 return self
.FormatVariableHelpText(
432 env
, opt
.key
, opt
.help, opt
.default
, actual
, opt
.aliases
434 return ''.join(_f
for _f
in map(format_opt
, options
) if _f
)
436 fmt
= '\n%s: %s\n default: %s\n actual: %s\n'
437 aliasfmt
= '\n%s: %s\n default: %s\n actual: %s\n aliases: %s\n'
439 # lint: W0622: Redefining built-in 'help'
440 def FormatVariableHelpText(
447 aliases
: list[str |
None] = None,
449 """Format the help text for a single variable.
451 The caller is responsible for obtaining all the values,
452 although now the :class:`Variable` class is more publicly exposed,
453 this method could easily do most of that work - however
454 that would change the existing published API.
458 # Don't display the key name itself as an alias.
459 aliases
= [a
for a
in aliases
if a
!= key
]
461 return self
.aliasfmt
% (key
, help, default
, actual
, aliases
)
462 return self
.fmt
% (key
, help, default
, actual
)
466 # indent-tabs-mode:nil
468 # vim: set expandtab tabstop=4 shiftwidth=4: