Skip to content

Commit

Permalink
compilers: Convert b_sanitize to a freeform array option
Browse files Browse the repository at this point in the history
There are a lot of sanitizers these days, and trying to keep up with
them would be entirely too much work. Instead we'll do a compile check
to see if the option is valid. An array makes more sense than a string,
since users could just write `address,undefined` or they could write
`undefined,address`. With the array we get to handle that ourselves and
users writing code know that it's not safe to simply do a check like
```meson
if get_option('b_sanitize') == 'address,undefined'
  ...
endif
```

instead they need to do something like
```meson
opt = get_option('b_sanitize')
if opt.contains('address') and opt.contains('undefined')
   ...
endif
```

To ensure backwards compatibility, a special check has been added to the
get_option() call for `b_sanitize`, which converts it to a string if the
compatibility is < 0.63, with a warning that the behavior will change in
the future.

Fixes mesonbuild#8283
Fixes mesonbuild#7761
Fixes mesonbuild#5154
Fixes mesonbuild#1582
  • Loading branch information
dcbaker committed Jun 6, 2022
1 parent b8c3de8 commit 0e22712
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 120 deletions.
171 changes: 87 additions & 84 deletions docs/markdown/Builtin-options.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions docs/markdown/snippets/b_sanitizer_changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Changes to the b_sanitize option

In meson <= 0.62 the b_sanitize option is a combo, which is an enumerated set of
values. One picks one of the values as provided by that enumeration. In 0.63
this was changed to a free array of options, and a compiler check for the
validity of those options.

This solves a number of longstanding issues such as:
- sanitizers may be supported by a compiler, but not on a specific platform (OpenBSD)
- new sanitizers are not recognized by Meson
- using sanitizers in different combinations

In order to not break backwards compatibility, meson will make continue to
return `get_option('b_sanitize')` as a string if the requested meson version is
`< 0.63`. In addition, it alphabetically sorts the values so that
`undefined,address` will still be presented as `address,undefined`. When the
minimum version is changed to >= 0.63, then it will return an array of strings.
2 changes: 1 addition & 1 deletion mesonbuild/backend/vs2010backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def generate(self):
try:
self.sanitize = self.environment.coredata.get_option(OptionKey('b_sanitize'))
except MesonException:
self.sanitize = 'none'
self.sanitize = []
sln_filename = os.path.join(self.environment.get_build_dir(), self.build.project_name + '.sln')
projlist = self.generate_projects()
self.gen_testproj('RUN_TESTS', os.path.join(self.environment.get_build_dir(), 'RUN_TESTS.vcxproj'))
Expand Down
11 changes: 5 additions & 6 deletions mesonbuild/compilers/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,7 @@ class CompileCheckMode(enum.Enum):
OptionKey('b_lto_mode'): coredata.UserComboOption('Select between different LTO modes.',
['default', 'thin'],
'default'),
OptionKey('b_sanitize'): coredata.UserComboOption('Code sanitizer to use',
['none', 'address', 'thread', 'undefined', 'memory', 'leak', 'address,undefined'],
'none'),
OptionKey('b_sanitize'): coredata.UserArrayOption('Code sanitizer to use', []),
OptionKey('b_lundef'): coredata.UserBooleanOption('Use -Wl,--no-undefined when linking', True),
OptionKey('b_asneeded'): coredata.UserBooleanOption('Use -Wl,--as-needed when linking', True),
OptionKey('b_pgo'): coredata.UserComboOption('Use profile guided optimization',
Expand Down Expand Up @@ -391,7 +389,8 @@ def get_base_link_args(options: 'KeyedOptionDictType', linker: 'Compiler',
except KeyError:
pass
try:
sani_opt = options[OptionKey('b_sanitize')].value
sani_opt: T.List[str] = options[OptionKey('b_sanitize')].value
assert isinstance(sani_opt, list), 'for mypy'
sani_args = linker.sanitizer_link_args(sani_opt)
# We consider that if there are no sanitizer arguments returned, then the language doesn't support them
if sani_args:
Expand Down Expand Up @@ -994,10 +993,10 @@ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.
def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
return self.linker.get_lto_args()

def sanitizer_compile_args(self, value: str) -> T.List[str]:
def sanitizer_compile_args(self, value: T.List[str]) -> T.List[str]:
return []

def sanitizer_link_args(self, value: str) -> T.List[str]:
def sanitizer_link_args(self, value: T.List[str]) -> T.List[str]:
return self.linker.sanitizer_args(value)

def get_asneeded_args(self) -> T.List[str]:
Expand Down
4 changes: 2 additions & 2 deletions mesonbuild/compilers/cuda.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,10 +671,10 @@ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
# return self._to_host_flags(self.host_compiler.get_optimization_args(optimization_level))
return cuda_optimization_args[optimization_level]

def sanitizer_compile_args(self, value: str) -> T.List[str]:
def sanitizer_compile_args(self, value: T.List[str]) -> T.List[str]:
return self._to_host_flags(self.host_compiler.sanitizer_compile_args(value))

def sanitizer_link_args(self, value: str) -> T.List[str]:
def sanitizer_link_args(self, value: T.List[str]) -> T.List[str]:
return self._to_host_flags(self.host_compiler.sanitizer_link_args(value))

def get_debug_args(self, is_debug: bool) -> T.List[str]:
Expand Down
10 changes: 5 additions & 5 deletions mesonbuild/compilers/mixins/gnu.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,11 @@ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.
# for their specific arguments
return ['-flto']

def sanitizer_compile_args(self, value: str) -> T.List[str]:
if value == 'none':
return []
args = ['-fsanitize=' + value]
if 'address' in value: # for -fsanitize=address,undefined
def sanitizer_compile_args(self, value: T.List[str]) -> T.List[str]:
if not value:
return value
args = ['-fsanitize=' + ','.join(value)]
if 'address' in value:
args.append('-fno-omit-frame-pointer')
return args

Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/compilers/mixins/islinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class BasicLinkerIsCompilerMixin(Compiler):
functionality itself.
"""

def sanitizer_link_args(self, value: str) -> T.List[str]:
def sanitizer_link_args(self, value: T.List[str]) -> T.List[str]:
return []

def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
Expand Down
10 changes: 4 additions & 6 deletions mesonbuild/compilers/mixins/visualstudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,10 @@ def get_compile_only_args(self) -> T.List[str]:
def get_no_optimization_args(self) -> T.List[str]:
return ['/Od', '/Oi-']

def sanitizer_compile_args(self, value: str) -> T.List[str]:
if value == 'none':
return []
if value != 'address':
raise mesonlib.MesonException('VS only supports address sanitizer at the moment.')
return ['/fsanitize=address']
def sanitizer_compile_args(self, value: T.List[str]) -> T.List[str]:
if not value:
return value
return [f'/fsanitize={",".join(value)}']

def get_output_args(self, target: str) -> T.List[str]:
if target.endswith('.exe'):
Expand Down
24 changes: 22 additions & 2 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ def get_option_internal(self, optname: str) -> coredata.UserOption:

@typed_pos_args('get_option', str)
@noKwargs
def func_get_option(self, nodes: mparser.BaseNode, args: T.Tuple[str],
def func_get_option(self, node: mparser.BaseNode, args: T.Tuple[str],
kwargs: 'TYPE_kwargs') -> T.Union[coredata.UserOption, 'TYPE_var']:
optname = args[0]
if ':' in optname:
Expand All @@ -1071,6 +1071,26 @@ def func_get_option(self, nodes: mparser.BaseNode, args: T.Tuple[str],
if isinstance(opt, coredata.UserFeatureOption):
opt.name = optname
return opt
elif optname == 'b_sanitize':
assert isinstance(opt, coredata.UserArrayOption), 'for mypy'
# Handle the change of sanitize from a combo to an array
# if the version compat is < 0.63 then we return a `,` joined
# string, sorted alphabetically (so that address,undefined remains
# unchanged), otherwise we return an array

# TODO: this should be 0.63, but there's no way to test this until 0.63 if it is
if mesonlib.version_compare(mesonlib.project_meson_versions[self.subproject], '< 0.62.99'):
# Use deprecation to not trip up --fatal-warnings
mlog.deprecation(textwrap.dedent('''\
In meson 0.63 the type of the builtin "b_sanitize" option changed from a combo to an array of strings. For
compatiblity, projects that set their minimum version < 0.63 will continue to get a string from the command
`get_option('b_sanitize')`, even when using meson > 0.63. When the minimum version changes this will change to an
array. Meson will attempt to provide the same values as before, (such as sorting to try to match `address,undefined`),
however, new values available in meson >= 0.63 may trip this up, and any reliance on this should be removed,
as the array version will be provided in user provided order.
'''), location=node)
return ','.join(sorted(opt.value))
return opt.value
elif isinstance(opt, coredata.UserOption):
return opt.value
return opt
Expand Down Expand Up @@ -2816,7 +2836,7 @@ def check_clang_asan_lundef(self) -> None:
if OptionKey('b_sanitize') not in self.coredata.options:
return
if (self.coredata.options[OptionKey('b_lundef')].value and
self.coredata.options[OptionKey('b_sanitize')].value != 'none'):
self.coredata.options[OptionKey('b_sanitize')].value):
mlog.warning('''Trying to use {} sanitizer on Clang with b_lundef.
This will probably not work.
Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey('b_sanitize')].value),
Expand Down
18 changes: 9 additions & 9 deletions mesonbuild/linkers/linkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def get_pie_args(self) -> T.List[str]:
def get_lto_args(self) -> T.List[str]:
return []

def sanitizer_args(self, value: str) -> T.List[str]:
def sanitizer_args(self, value: T.List[str]) -> T.List[str]:
return []

def get_buildtype_args(self, buildtype: str) -> T.List[str]:
Expand Down Expand Up @@ -594,10 +594,10 @@ def get_allow_undefined_args(self) -> T.List[str]:
def get_lto_args(self) -> T.List[str]:
return ['-flto']

def sanitizer_args(self, value: str) -> T.List[str]:
if value == 'none':
return []
return ['-fsanitize=' + value]
def sanitizer_args(self, value: T.List[str]) -> T.List[str]:
if not value:
return value
return [f'-fsanitize={",".join(value)}']

def get_coverage_args(self) -> T.List[str]:
return ['--coverage']
Expand Down Expand Up @@ -758,10 +758,10 @@ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
def get_coverage_args(self) -> T.List[str]:
return ['--coverage']

def sanitizer_args(self, value: str) -> T.List[str]:
if value == 'none':
return []
return ['-fsanitize=' + value]
def sanitizer_args(self, value: T.List[str]) -> T.List[str]:
if not value:
return value
return [f'-fsanitize={",".join(value)}']

def no_undefined_args(self) -> T.List[str]:
return self._apply_prefix('-undefined,error')
Expand Down
4 changes: 2 additions & 2 deletions mesonbuild/modules/gnome.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,9 +918,9 @@ def _get_langs_compilers_flags(state: 'ModuleState', langs_compilers: T.List[T.T
if state.project_args.get(lang):
cflags += state.project_args[lang]
if mesonlib.OptionKey('b_sanitize') in compiler.base_options:
sanitize = state.environment.coredata.options[mesonlib.OptionKey('b_sanitize')].value
sanitize: T.List[str] = state.environment.coredata.options[mesonlib.OptionKey('b_sanitize')].value
assert isinstance(sanitize, list), 'for mypy'
cflags += compiler.sanitizer_compile_args(sanitize)
sanitize = sanitize.split(',')
# These must be first in ldflags
if 'address' in sanitize:
internal_ldflags += ['-lasan']
Expand Down
4 changes: 4 additions & 0 deletions test cases/linuxlike/16 sanitizers/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
project('sanitizer', 'c')

subproject('new_behavior')
subproject('old_behavior')
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
project(
'sanitizer-new-behavior',
'c',
# XXX: this should be 0.62, but there's no way to test it until we actuall bump to 0.62…
meson_version : '>= 0.61.99',
)

sanitize = get_option('b_sanitize')

assert(sanitize == ['undefined', 'address'], ','.join(sanitize))
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
project(
'sanitizer-old-behavior',
'c',
meson_version : '>= 0.55',
)

sanitize = get_option('b_sanitize')

assert(sanitize == 'address,undefined', sanitize)
7 changes: 7 additions & 0 deletions test cases/linuxlike/16 sanitizers/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"matrix": {
"options": {
"b_sanitize": [{ "val": ["undefined", "address"] }]
}
}
}
4 changes: 2 additions & 2 deletions unittests/allplatformstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2291,7 +2291,7 @@ def test_command_line(self):
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.options[OptionKey('bindir')].value, 'bar')
self.assertEqual(obj.options[OptionKey('buildtype')].value, 'release')
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'thread')
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, ['thread'])
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dbar'])
self.setconf(['--bindir=bar', '--bindir=foo',
'-Dbuildtype=release', '-Dbuildtype=plain',
Expand All @@ -2300,7 +2300,7 @@ def test_command_line(self):
obj = mesonbuild.coredata.load(self.builddir)
self.assertEqual(obj.options[OptionKey('bindir')].value, 'foo')
self.assertEqual(obj.options[OptionKey('buildtype')].value, 'plain')
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'address')
self.assertEqual(obj.options[OptionKey('b_sanitize')].value, ['address'])
self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dfoo'])
self.wipe()
except KeyError:
Expand Down

0 comments on commit 0e22712

Please sign in to comment.