Convert args.projectoptions into a dict

This simplifies a lot of code, and centralize "key=value" parsing in a
single place.

Unknown command line options becomes an hard error instead of
merely printing warning message. It has been warning it would become an
hard error for a while now. This has exceptions though, any
unknown option starting with "<lang>_" or "b_" are ignored because they
depend on which languages gets added and which compiler gets selected.
Also any option for unknown subproject are ignored because they depend
on which subproject actually gets built.

Also write more command line parsing tests. "19 bad command line
options" is removed because bad cmd line option became hard error and
it's covered with new tests in "30 command line".
pull/3705/head
Xavier Claessens 7 years ago committed by Nirbheek Chauhan
parent b38452636c
commit 7c4736d27f
  1. 41
      mesonbuild/coredata.py
  2. 2
      mesonbuild/environment.py
  3. 142
      mesonbuild/interpreter.py
  4. 45
      mesonbuild/optinterpreter.py
  5. 1
      run_tests.py
  6. 60
      run_unittests.py
  7. 17
      test cases/unit/19 bad command line options/meson.build
  8. 16
      test cases/unit/19 bad command line options/meson_options.txt
  9. 15
      test cases/unit/19 bad command line options/subprojects/one/meson.build
  10. 15
      test cases/unit/19 bad command line options/subprojects/one/meson_options.txt
  11. 2
      test cases/unit/27 forcefallback/meson.build
  12. 8
      test cases/unit/30 command line/meson.build
  13. 1
      test cases/unit/30 command line/meson_options.txt
  14. 3
      test cases/unit/30 command line/subprojects/subp/meson.build
  15. 1
      test cases/unit/30 command line/subprojects/subp/meson_options.txt

@ -301,18 +301,11 @@ class CoreData:
args = [key] + builtin_options[key][1:-1] + [value]
self.builtins[key] = builtin_options[key][0](*args)
def init_backend_options(self, backend_name, options):
def init_backend_options(self, backend_name):
if backend_name == 'ninja':
self.backend_options['backend_max_links'] = UserIntegerOption('backend_max_links',
'Maximum number of linker processes to run or 0 for no limit',
0, None, 0)
for o in options:
key, value = o.split('=', 1)
if not key.startswith('backend_'):
continue
if key not in self.backend_options:
raise MesonException('Unknown backend option %s' % key)
self.backend_options[key].set_value(value)
def get_builtin_option(self, optname):
if optname in self.builtins:
@ -497,27 +490,6 @@ def register_builtin_arguments(parser):
parser.add_argument('-D', action='append', dest='projectoptions', default=[], metavar="option",
help='Set the value of an option, can be used several times to set multiple options.')
def filter_builtin_options(args):
"""Filter out any builtin arguments passed as -- instead of -D.
Error if an argument is passed with -- and -D
"""
for name in builtin_options:
cmdline_name = get_builtin_option_cmdline_name(name)
# Chekc if user passed -Doption=value or --option=value
has_dashdash = hasattr(args, name)
has_dashd = any([a.startswith('{}='.format(name)) for a in args.projectoptions])
# Passing both is ambigous, abort
if has_dashdash and has_dashd:
raise MesonException(
'Got argument {0} as both -D{0} and {1}. Pick one.'.format(name, cmdline_name))
# Pretend --option never existed
if has_dashdash:
args.projectoptions.append('{}={}'.format(name, getattr(args, name)))
delattr(args, name)
def create_options_dict(options):
result = {}
for o in options:
@ -529,9 +501,18 @@ def create_options_dict(options):
return result
def parse_cmd_line_options(args):
filter_builtin_options(args)
args.cmd_line_options = create_options_dict(args.projectoptions)
# Merge builtin options set with --option into the dict.
for name in builtin_options:
value = getattr(args, name, None)
if value is not None:
if name in args.cmd_line_options:
cmdline_name = get_builtin_option_cmdline_name(name)
raise MesonException(
'Got argument {0} as both -D{0} and {1}. Pick one.'.format(name, cmdline_name))
args.cmd_line_options[name] = value
delattr(args, name)
builtin_options = {
'buildtype': [UserComboOption, 'Build type to use.', ['plain', 'debug', 'debugoptimized', 'release', 'minsize'], 'debug'],

@ -288,7 +288,7 @@ class Environment:
self.cross_info = CrossBuildInfo(self.coredata.cross_file)
else:
self.cross_info = None
self.cmd_line_options = options
self.cmd_line_options = options.cmd_line_options
# List of potential compilers.
if mesonlib.is_windows():

@ -1755,7 +1755,7 @@ permitted_kwargs = {'add_global_arguments': {'language'},
class Interpreter(InterpreterBase):
def __init__(self, build, backend=None, subproject='', subdir='', subproject_dir='subprojects',
modules = None, default_project_options=[]):
modules = None, default_project_options=None):
super().__init__(build.environment.get_source_dir(), subdir)
self.an_unpicklable_object = mesonlib.an_unpicklable_object
self.build = build
@ -1781,7 +1781,11 @@ class Interpreter(InterpreterBase):
self.global_args_frozen = False # implies self.project_args_frozen
self.subprojects = {}
self.subproject_stack = []
self.default_project_options = default_project_options[:] # Passed from the outside, only used in subprojects.
# Passed from the outside, only used in subprojects.
if default_project_options:
self.default_project_options = default_project_options.copy()
else:
self.default_project_options = {}
self.build_func_dict()
# build_def_files needs to be defined before parse_project is called
self.build_def_files = [os.path.join(self.subdir, environment.build_filename)]
@ -2106,6 +2110,8 @@ external dependencies (including libraries) must go to "dependencies".''')
return self.do_subproject(dirname, kwargs)
def do_subproject(self, dirname, kwargs):
default_options = mesonlib.stringlistify(kwargs.get('default_options', []))
default_options = coredata.create_options_dict(default_options)
if dirname == '':
raise InterpreterException('Subproject dir name must not be empty.')
if dirname[0] == '.':
@ -2142,7 +2148,7 @@ external dependencies (including libraries) must go to "dependencies".''')
with mlog.nested():
mlog.log('\nExecuting subproject ', mlog.bold(dirname), '.\n', sep='')
subi = Interpreter(self.build, self.backend, dirname, subdir, self.subproject_dir,
self.modules, mesonlib.stringlistify(kwargs.get('default_options', [])))
self.modules, default_options)
subi.subprojects = self.subprojects
subi.subproject_stack = self.subproject_stack + [dirname]
@ -2206,53 +2212,34 @@ to directly access options of other subprojects.''')
raise InterpreterException('configuration_data takes no arguments')
return ConfigurationDataHolder()
def parse_default_options(self, default_options):
default_options = listify(default_options)
for option in default_options:
if not isinstance(option, str):
mlog.debug(option)
raise InterpreterException('Default options must be strings')
if '=' not in option:
raise InterpreterException('All default options must be of type key=value.')
key, value = option.split('=', 1)
if coredata.is_builtin_option(key):
if self.subproject != '':
continue # Only the master project is allowed to set global options.
newoptions = [option] + self.environment.cmd_line_options.projectoptions
self.environment.cmd_line_options.projectoptions = newoptions
else:
# Option values set with subproject() default_options override those
# set in project() default_options.
pref = key + '='
for i in self.default_project_options:
if i.startswith(pref):
option = i
break
# If we are in a subproject, add the subproject prefix to option
# name.
if self.subproject != '':
option = self.subproject + ':' + option
newoptions = [option] + self.environment.cmd_line_options.projectoptions
self.environment.cmd_line_options.projectoptions = newoptions
# Add options that are only in default_options.
for defopt in self.default_project_options:
key, value = defopt.split('=')
pref = key + '='
for i in default_options:
if i.startswith(pref):
break
else:
defopt = self.subproject + ':' + defopt
newoptions = [defopt] + self.environment.cmd_line_options.projectoptions
self.environment.cmd_line_options.projectoptions = newoptions
def set_builtin_options(self):
# Create a dict containing only builtin options, then use
# coredata.set_options() because it already has code to set the prefix
# option first to sanitize all other options.
options = coredata.create_options_dict(self.environment.cmd_line_options.projectoptions)
options = {k: v for k, v in options.items() if coredata.is_builtin_option(k)}
self.coredata.set_options(options)
def set_options(self, default_options):
# Set default options as if they were passed to the command line.
# Subprojects can only define default for user options.
for k, v in default_options.items():
if self.subproject:
if optinterpreter.is_invalid_name(k):
continue
k = self.subproject + ':' + k
self.environment.cmd_line_options.setdefault(k, v)
# Create a subset of cmd_line_options, keeping only options for this
# subproject. Also take builtin options if it's the main project.
# Language and backend specific options will be set later when adding
# languages and setting the backend (builtin options must be set first
# to know which backend we'll use).
options = {}
for k, v in self.environment.cmd_line_options.items():
if self.subproject:
if not k.startswith(self.subproject + ':'):
continue
elif k not in coredata.get_builtin_options():
if ':' in k:
continue
if optinterpreter.is_invalid_name(k):
continue
options[k] = v
self.coredata.set_options(options, self.subproject)
def set_backend(self):
# The backend is already set when parsing subprojects
@ -2282,7 +2269,10 @@ to directly access options of other subprojects.''')
else:
raise InterpreterException('Unknown backend "%s".' % backend)
self.coredata.init_backend_options(backend, self.environment.cmd_line_options.projectoptions)
self.coredata.init_backend_options(backend)
options = {k: v for k, v in self.environment.cmd_line_options.items() if k.startswith('backend_')}
self.coredata.set_options(options)
@stringArgs
@permittedKwargs(permitted_kwargs['project'])
@ -2293,20 +2283,20 @@ to directly access options of other subprojects.''')
proj_langs = args[1:]
if ':' in proj_name:
raise InvalidArguments("Project name {!r} must not contain ':'".format(proj_name))
default_options = kwargs.get('default_options', [])
if self.environment.first_invocation and (len(default_options) > 0 or
len(self.default_project_options) > 0):
self.parse_default_options(default_options)
if not self.is_subproject():
self.build.project_name = proj_name
self.set_builtin_options()
if os.path.exists(self.option_file):
oi = optinterpreter.OptionInterpreter(self.subproject,
self.build.environment.cmd_line_options.projectoptions,
)
oi = optinterpreter.OptionInterpreter(self.subproject)
oi.process(self.option_file)
self.coredata.merge_user_options(oi.options)
default_options = mesonlib.stringlistify(kwargs.get('default_options', []))
default_options = coredata.create_options_dict(default_options)
default_options.update(self.default_project_options)
self.set_options(default_options)
self.set_backend()
if not self.is_subproject():
self.build.project_name = proj_name
self.active_projectname = proj_name
self.project_version = kwargs.get('version', 'undefined')
if self.build.project_version is None:
@ -2450,17 +2440,14 @@ to directly access options of other subprojects.''')
cross_comp.sanity_check(self.environment.get_scratch_dir(), self.environment)
self.coredata.cross_compilers[lang] = cross_comp
new_options.update(cross_comp.get_options())
optprefix = lang + '_'
for i in new_options:
if not i.startswith(optprefix):
raise InterpreterException('Internal error, %s has incorrect prefix.' % i)
cmd_prefix = i + '='
for cmd_arg in self.environment.cmd_line_options.projectoptions:
if cmd_arg.startswith(cmd_prefix):
value = cmd_arg.split('=', 1)[1]
new_options[i].set_value(value)
new_options.update(self.coredata.compiler_options)
self.coredata.compiler_options = new_options
for k, o in new_options.items():
if not k.startswith(optprefix):
raise InterpreterException('Internal error, %s has incorrect prefix.' % k)
if k in self.environment.cmd_line_options:
o.set_value(self.environment.cmd_line_options[k])
self.coredata.compiler_options.setdefault(k, o)
# Unlike compiler and linker flags, preprocessor flags are not in
# compiler_options because they are not visible to user.
@ -2510,19 +2497,14 @@ to directly access options of other subprojects.''')
def add_base_options(self, compiler):
enabled_opts = []
proj_opt = self.environment.cmd_line_options.projectoptions
for optname in compiler.base_options:
if optname in self.coredata.base_options:
continue
oobj = compilers.base_options[optname]
for po in proj_opt:
if po.startswith(optname + '='):
opt, value = po.split('=', 1)
oobj.set_value(value)
if oobj.value:
enabled_opts.append(opt)
break
self.coredata.base_options[optname] = oobj
if optname in self.environment.cmd_line_options:
oobj.set_value(self.environment.cmd_line_options[optname])
enabled_opts.append(optname)
self.coredata. base_options[optname] = oobj
self.emit_base_options_warnings(enabled_opts)
def program_from_cross_file(self, prognames, silent=False):

@ -15,7 +15,6 @@
import os, re
import functools
from . import mlog
from . import mparser
from . import coredata
from . import mesonlib
@ -125,48 +124,9 @@ option_types = {'string': StringParser,
}
class OptionInterpreter:
def __init__(self, subproject, command_line_options):
def __init__(self, subproject):
self.options = {}
self.subproject = subproject
self.sbprefix = subproject + ':'
self.cmd_line_options = {}
for o in command_line_options:
if self.subproject != '': # Strip the beginning.
# Ignore options that aren't for this subproject
if not o.startswith(self.sbprefix):
continue
try:
(key, value) = o.split('=', 1)
except ValueError:
raise OptionException('Option {!r} must have a value separated by equals sign.'.format(o))
# Ignore subproject options if not fetching subproject options
if self.subproject == '' and ':' in key:
continue
self.cmd_line_options[key] = value
def get_bad_options(self):
subproj_len = len(self.subproject)
if subproj_len > 0:
subproj_len += 1
retval = []
# The options need to be sorted (e.g. here) to get consistent
# error messages (on all platforms) which is required by some test
# cases that check (also) the order of these options.
for option in sorted(self.cmd_line_options):
if option in list(self.options) + forbidden_option_names:
continue
if any(option[subproj_len:].startswith(p) for p in forbidden_prefixes):
continue
retval += [option]
return retval
def check_for_bad_options(self):
bad = self.get_bad_options()
if bad:
sub = 'In subproject {}: '.format(self.subproject) if self.subproject else ''
mlog.warning(
'{}Unknown command line options: "{}"\n'
'This will become a hard error in a future Meson release.'.format(sub, ', '.join(bad)))
def process(self, option_file):
try:
@ -187,7 +147,6 @@ class OptionInterpreter:
e.colno = cur.colno
e.file = os.path.join('meson_options.txt')
raise e
self.check_for_bad_options()
def reduce_single(self, arg):
if isinstance(arg, str):
@ -243,6 +202,4 @@ class OptionInterpreter:
opt = option_types[opt_type](opt_name, kwargs.pop('description', ''), kwargs)
if opt.description == '':
opt.description = opt_name
if opt_name in self.cmd_line_options:
opt.set_value(self.cmd_line_options[opt_name])
self.options[opt_name] = opt

@ -153,6 +153,7 @@ def get_fake_options(prefix):
opts.cross_file = None
opts.wrap_mode = None
opts.prefix = prefix
opts.cmd_line_options = {}
return opts
def should_run_linux_cross_tests():

@ -2072,6 +2072,8 @@ recommended as it can lead to undefined behaviour on some platforms''')
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.builtins['default_library'].value, 'static')
self.assertEqual(obj.builtins['warning_level'].value, '1')
self.assertEqual(obj.user_options['set_sub_opt'].value, True)
self.assertEqual(obj.user_options['subp:subp_opt'].value, 'default3')
self.wipe()
# warning_level is special, it's --warnlevel instead of --warning-level
@ -2114,6 +2116,45 @@ recommended as it can lead to undefined behaviour on some platforms''')
self.assertEqual(obj.builtins['default_library'].value, 'shared')
self.wipe()
# Should fail on unknown options
with self.assertRaises(subprocess.CalledProcessError) as cm:
self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo'])
self.assertNotEqual(0, cm.exception.returncode)
self.assertIn('Unknown options: "bad, foo, wrong_link_args"', cm.exception.output)
self.wipe()
# Should fail on malformed option
with self.assertRaises(subprocess.CalledProcessError) as cm:
self.init(testdir, extra_args=['-Dfoo'])
self.assertNotEqual(0, cm.exception.returncode)
self.assertIn('Option \'foo\' must have a value separated by equals sign.', cm.exception.output)
self.init(testdir)
with self.assertRaises(subprocess.CalledProcessError) as cm:
self.setconf('-Dfoo')
self.assertNotEqual(0, cm.exception.returncode)
self.assertIn('Option \'foo\' must have a value separated by equals sign.', cm.exception.output)
self.wipe()
# It is not an error to set wrong option for unknown subprojects or
# language because we don't have control on which one will be selected.
self.init(testdir, extra_args=['-Dc_wrong=1', '-Dwrong:bad=1', '-Db_wrong=1'])
self.wipe()
# Test we can set subproject option
self.init(testdir, extra_args=['-Dsubp:subp_opt=foo'])
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.user_options['subp:subp_opt'].value, 'foo')
self.wipe()
# c_args value should be parsed with shlex
self.init(testdir, extra_args=['-Dc_args=foo bar "one two"'])
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.compiler_options['c_args'].value, ['foo', 'bar', 'one two'])
self.setconf('-Dc_args="foo bar" one two')
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.compiler_options['c_args'].value, ['foo bar', 'one', 'two'])
self.wipe()
def test_compiler_options_documented(self):
'''
Test that C and C++ compiler options and base options are documented in
@ -2271,25 +2312,6 @@ class FailureTests(BasePlatformTests):
'''
self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal")
def test_bad_option(self):
tdir = os.path.join(self.unit_test_dir, '19 bad command line options')
os.environ['MESON_FORCE_BACKTRACE'] = '1'
self.init(tdir, extra_args=['-Dopt=bar', '-Dc_args=-Wall'], inprocess=True)
self.wipe()
out = self.init(tdir, extra_args=['-Dfoo=bar', '-Dbad=true'], inprocess=True)
self.assertRegex(
out, r'Unknown command line options: "bad, foo"')
def test_bad_option_subproject(self):
tdir = os.path.join(self.unit_test_dir, '19 bad command line options')
os.environ['MESON_FORCE_BACKTRACE'] = '1'
self.init(tdir, extra_args=['-Done:one=bar'], inprocess=True)
self.wipe()
out = self.init(tdir, extra_args=['-Done:two=bar'], inprocess=True)
self.assertRegex(
out,
r'In subproject one: Unknown command line options: "one:two"')
def test_objc_cpp_detection(self):
'''
Test that when we can't detect objc or objcpp, we fail gracefully.

@ -1,17 +0,0 @@
# Copyright © 2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
project('Bad command line options')
one = subproject('one')

@ -1,16 +0,0 @@
# Copyright © 2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
option('opt', type : 'string', description : 'An argument')
option('good', type : 'boolean', value : true, description : 'another argument')

@ -1,15 +0,0 @@
# Copyright © 2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
project('one subproject', default_options : [ 'b_colorout=never' ])

@ -1,15 +0,0 @@
# Copyright © 2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
option('one', type : 'string', description : 'an option')

@ -1,5 +1,5 @@
project('mainproj', 'c',
default_options : ['wrap_mode=forcefallback'])
default_options : [])
zlib_dep = dependency('zlib', fallback: ['notzlib', 'zlib_dep'])
notfound_dep = dependency('cannotabletofind', fallback: ['definitelynotfound', 'some_var'], required : false)

@ -1,3 +1,9 @@
project('command line test', 'c',
default_options : ['default_library=static']
default_options : ['default_library=static', 'set_sub_opt=true']
)
if get_option('set_sub_opt')
subproject('subp', default_options : ['subp_opt=default3'])
else
subproject('subp')
endif

@ -0,0 +1 @@
option('set_sub_opt', type : 'boolean', value : false)

@ -0,0 +1,3 @@
project('subp',
default_options : ['subp_opt=default2']
)

@ -0,0 +1 @@
option('subp_opt', type : 'string', value : 'default1')
Loading…
Cancel
Save