diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index cb3552d7f0c1..11417c2968da 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -2927,6 +2927,10 @@ def _generate_single_compile_target_args(self, target: build.BuildTarget, compil # Finally add the private dir for the target to the include path. This # must override everything else and must be the final path added. commands += compiler.get_include_args(self.get_target_private_dir(target), False) + + # Add Meson per-compiler defaults (like /utf-8 for MSVC) + compiler.add_default_build_args(commands) + return commands # Returns a dictionary, mapping from each compiler src type (e.g. 'c', 'cpp', etc.) to a list of compiler arg strings diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index 08a19c659e44..c7c78ec7df46 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -66,18 +66,23 @@ def autodetect_vs_version(build: T.Optional[build.Build], interpreter: T.Optiona 'Please specify the exact backend to use.'.format(vs_version, vs_install_dir)) -def split_o_flags_args(args: T.List[str]) -> T.List[str]: +def split_o_flags_args(args: T.List[str], remove: bool) -> T.List[str]: """ Splits any /O args and returns them. Does not take care of flags overriding - previous ones. Skips non-O flag arguments. + previous ones. Skips non-O flag arguments. If remove is true, found arguments + will be removed from the args list ['/Ox', '/Ob1'] returns ['/Ox', '/Ob1'] ['/Oxj', '/MP'] returns ['/Ox', '/Oj'] """ o_flags = [] - for arg in args: + indexes = [] + for index, arg in enumerate(args): if not arg.startswith('/O'): continue + + indexes.append(index) + flags = list(arg[2:]) # Assume that this one can't be clumped with the others since it takes # an argument itself @@ -85,6 +90,11 @@ def split_o_flags_args(args: T.List[str]) -> T.List[str]: o_flags.append(arg) else: o_flags += ['/O' + f for f in flags] + + if remove: + for index in reversed(indexes): + args.pop(index) + return o_flags def generate_guid_from_path(path, path_type) -> str: @@ -1013,6 +1023,10 @@ def get_args_defines_and_inc_dirs(self, target, compiler, generated_files_includ # to override all the defaults, but not the per-target compile args. for lang in file_args.keys(): file_args[lang] += target.get_option(OptionKey(f'{lang}_args', machine=target.for_machine)) + # Meson default build arguments, which must added as last so that they're added only + # if not already set in other ways + for lang in file_args.keys(): + target.compilers[lang].add_default_build_args(file_args[lang]) for args in file_args.values(): # This is where Visual Studio will insert target_args, target_defines, # etc, which are added later from external deps (see below). @@ -1118,7 +1132,8 @@ def get_args_defines_and_inc_dirs(self, target, compiler, generated_files_includ @staticmethod def get_build_args(compiler, optimization_level: str, debug: bool, sanitize: str) -> T.List[str]: - build_args = compiler.get_optimization_args(optimization_level) + build_args = compiler.get_always_args() + build_args += compiler.get_optimization_args(optimization_level) build_args += compiler.get_debug_args(debug) build_args += compiler.sanitizer_compile_args(sanitize) @@ -1273,7 +1288,7 @@ def add_non_makefile_vcxproj_elements( target, platform: str, subsystem, - build_args, + build_args_, target_args, target_defines, target_inc_dirs, @@ -1282,6 +1297,24 @@ def add_non_makefile_vcxproj_elements( compiler = self._get_cl_compiler(target) buildtype_link_args = compiler.get_optimization_link_args(self.optimization) + build_args = build_args_ + target_args + + def check_build_arg(name, remove = True): + index = None + try: + index = build_args.index(name) + except ValueError: + pass + if index and remove: + build_args.pop(index) + return index is not None + + def check_build_arg_prefix(prefix): + for a in build_args: + if a.startswith(prefix): + return True + return False + # Prefix to use to access the build root from the vcxproj dir down = self.target_to_build_root(target) @@ -1301,6 +1334,7 @@ def add_non_makefile_vcxproj_elements( clconf = ET.SubElement(compiles, 'ClCompile') if True in ((dep.name == 'openmp') for dep in target.get_external_deps()): ET.SubElement(clconf, 'OpenMPSupport').text = 'true' + # CRT type; debug or release vscrt_type = target.get_option(OptionKey('b_vscrt')) vscrt_val = compiler.get_crt_val(vscrt_type, self.buildtype) @@ -1318,25 +1352,31 @@ def add_non_makefile_vcxproj_elements( else: ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' + # Sanitizers - if '/fsanitize=address' in build_args: + if check_build_arg('/fsanitize=address', remove=True): ET.SubElement(type_config, 'EnableASAN').text = 'true' + # Debug format - if '/ZI' in build_args: + if check_build_arg('/ZI', remove=True): ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue' - elif '/Zi' in build_args: + elif check_build_arg('/Zi', remove=True): ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase' - elif '/Z7' in build_args: + elif check_build_arg('/Z7', remove=True): ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle' else: ET.SubElement(clconf, 'DebugInformationFormat').text = 'None' + assert not check_build_arg_prefix('/Z[:alnum:]') # TODO + # Runtime checks - if '/RTC1' in build_args: + if check_build_arg('/RTC1', remove=True): ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks' - elif '/RTCu' in build_args: + elif check_build_arg('/RTCu', remove=True): ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck' - elif '/RTCs' in build_args: + elif check_build_arg('/RTCs', remove=True): ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck' + assert not check_build_arg_prefix('/RTC') + # Exception handling has to be set in the xml in addition to the "AdditionalOptions" because otherwise # cl will give warning D9025: overriding '/Ehs' with cpp_eh value if 'cpp' in target.compilers: @@ -1349,22 +1389,26 @@ def add_non_makefile_vcxproj_elements( ET.SubElement(clconf, 'ExceptionHandling').text = 'false' else: # 'sc' or 'default' ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync' + assert not check_build_arg_prefix('/EH') + + if len(target_inc_dirs) > 0: + ET.SubElement(clconf, 'AdditionalIncludeDirectories').text = ';'.join(target_inc_dirs) + + if len(target_defines) > 0: + target_defines.append('%(PreprocessorDefinitions)') + ET.SubElement(clconf, 'PreprocessorDefinitions').text = ';'.join(target_defines) - if len(target_args) > 0: - target_args.append('%(AdditionalOptions)') - ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(target_args) - ET.SubElement(clconf, 'AdditionalIncludeDirectories').text = ';'.join(target_inc_dirs) - target_defines.append('%(PreprocessorDefinitions)') - ET.SubElement(clconf, 'PreprocessorDefinitions').text = ';'.join(target_defines) ET.SubElement(clconf, 'FunctionLevelLinking').text = 'true' + # Warning level warning_level = T.cast('str', target.get_option(OptionKey('warning_level'))) warning_level = 'EnableAllWarnings' if warning_level == 'everything' else 'Level' + str(1 + int(warning_level)) ET.SubElement(clconf, 'WarningLevel').text = warning_level if target.get_option(OptionKey('werror')): ET.SubElement(clconf, 'TreatWarningAsError').text = 'true' + # Optimization flags - o_flags = split_o_flags_args(build_args) + o_flags = split_o_flags_args(build_args, remove=True) if '/Ox' in o_flags: ET.SubElement(clconf, 'Optimization').text = 'Full' elif '/O2' in o_flags: @@ -1379,22 +1423,52 @@ def add_non_makefile_vcxproj_elements( ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'OnlyExplicitInline' elif '/Ob2' in o_flags: ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'AnySuitable' + assert not check_build_arg_prefix('/O') + # Size-preserving flags if '/Os' in o_flags or '/O1' in o_flags: ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Size' # Note: setting FavorSizeOrSpeed with clang-cl conflicts with /Od and can make debugging difficult, so don't. elif '/Od' not in o_flags: ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Speed' - # Note: SuppressStartupBanner is /NOLOGO and is 'true' by default + self.generate_lang_standard_info(file_args, clconf) + # SuppressStartupBanner is a boolean prop and defaults to true + if not check_build_arg('/nologo', remove=True): + ET.SubElement(clconf, 'SuppressStartupBanner').text = 'false' + + # Anything else goes in AdditionalOptions + if len(build_args) > 0: + build_args.append('%(AdditionalOptions)') + ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(build_args) + resourcecompile = ET.SubElement(compiles, 'ResourceCompile') ET.SubElement(resourcecompile, 'PreprocessorDefinitions') # Linker options link = ET.SubElement(compiles, 'Link') - extra_link_args = compiler.compiler_args() + extra_link_args = compiler.get_linker_always_args() + extra_link_args += compiler.compiler_args() extra_link_args += compiler.get_optimization_link_args(self.optimization) + + # Note that cl.exe arguments are case sensitive, but link.exe arguments are not. + def check_link_arg(name, remove = True): + index = None + for i, value in enumerate(extra_link_args): + if value.upper() == name.upper(): + index = i + break + if index and remove: + extra_link_args.pop(index) + return index is not None + + def check_link_arg_prefix(prefix): + for a in extra_link_args: + if a.upper().startswith(prefix.upper()): + return True + return False + # Generate Debug info if self.debug: self.generate_debug_information(link) @@ -1489,9 +1563,6 @@ def add_non_makefile_vcxproj_elements( for lib in self.get_custom_target_provided_libraries(target): additional_links.append(self.relpath(lib, self.get_target_dir(target))) - if len(extra_link_args) > 0: - extra_link_args.append('%(AdditionalOptions)') - ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args) if len(additional_libpaths) > 0: additional_libpaths.insert(0, '%(AdditionalLibraryDirectories)') ET.SubElement(link, 'AdditionalLibraryDirectories').text = ';'.join(additional_libpaths) @@ -1531,12 +1602,18 @@ def add_non_makefile_vcxproj_elements( targetmachine.text = 'MachineARM64EC' else: raise MesonException('Unsupported Visual Studio target machine: ' + targetplatform) - # /nologo - ET.SubElement(link, 'SuppressStartupBanner').text = 'true' - # /release - if not target.get_option(OptionKey('debug')): + + if not check_link_arg('/NOLOGO', remove=True): + ET.SubElement(link, 'SuppressStartupBanner').text = 'false' + + if check_link_arg('/RELEASE', remove=True): ET.SubElement(link, 'SetChecksum').text = 'true' + # Remaining arguments in AdditionalOptions + if len(extra_link_args) > 0: + extra_link_args.append('%(AdditionalOptions)') + ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args) + # Visual studio doesn't simply allow the src files of a project to be added with the 'Condition=...' attribute, # to allow us to point to the different debug/debugoptimized/release sets of generated src files for each of # the solution's configurations. Similarly, 'ItemGroup' also doesn't support 'Condition'. So, without knowing @@ -2089,6 +2166,7 @@ def add_regen_dependency(self, root: ET.Element) -> None: self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid) def generate_lang_standard_info(self, file_args: T.Dict[str, CompilerArgs], clconf: ET.Element) -> None: + # virtual method implemented in vs20XXbackend subclass pass # Returns if a target generates a manifest or not. diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py index 31fd272b3f0b..704463cdf6f0 100644 --- a/mesonbuild/backend/xcodebackend.py +++ b/mesonbuild/backend/xcodebackend.py @@ -832,6 +832,7 @@ def generate_pbx_build_rule(self, objects_dict: PbxDict) -> None: input='"$SCRIPT_INPUT_FILE"', depfile='"$(dirname "$SCRIPT_OUTPUT_FILE_0")/$(basename "$SCRIPT_OUTPUT_FILE_0" .o).d"', extras=['$OTHER_INPUT_FILE_FLAGS']) + compiler.add_default_build_args(commands) buildrule.add_item('script', self.to_shell_script(commands)) objects_dict.add_item(idval, buildrule, 'PBXBuildRule') diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 603a3eb484de..4a6800205284 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -782,6 +782,15 @@ def compiler_args(self, args: T.Optional[T.Iterable[str]] = None) -> CompilerArg """Return an appropriate CompilerArgs instance for this class.""" return CompilerArgs(self, args) + def add_default_build_args(self, args: CompilerArgs) -> None: + """Append Meson default compiler arguments. + + Because Meson default arguments can be overriden by the user, this method + must be called after building the complete arguments list. This method + checks all the previously added arguments. + """ + pass + @contextlib.contextmanager def compile(self, code: 'mesonlib.FileOrString', extra_args: T.Union[None, CompilerArgs, T.List[str]] = None, diff --git a/mesonbuild/compilers/mixins/visualstudio.py b/mesonbuild/compilers/mixins/visualstudio.py index b4677f4172ba..893fab9d4857 100644 --- a/mesonbuild/compilers/mixins/visualstudio.py +++ b/mesonbuild/compilers/mixins/visualstudio.py @@ -96,11 +96,7 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): # /showIncludes is needed for build dependency tracking in Ninja # See: https://ninja-build.org/manual.html#_deps - # Assume UTF-8 sources by default, but self.unix_args_to_native() removes it - # if `/source-charset` is set too. - # It is also dropped if Visual Studio 2013 or earlier is used, since it would - # not be supported in that case. - always_args = ['/nologo', '/showIncludes', '/utf-8'] + always_args = ['/nologo', '/showIncludes'] warn_args: T.Dict[str, T.List[str]] = { '0': [], '1': ['/W2'], @@ -131,11 +127,47 @@ def __init__(self, target: str): assert self.linker is not None self.linker.machine = self.machine - # Override CCompiler.get_always_args def get_always_args(self) -> T.List[str]: # TODO: use ImmutableListProtocol[str] here instead return self.always_args.copy() + def add_default_build_args(self, args: CompilerArgs) -> None: + # /utf-8 is supported starting with VS 2015 + # FIXME: what about clang-cl? + if mesonlib.version_compare(self.version, '<19.00'): + return + + native = args.to_native() + + def have_arg(native, name): + name = name.lower() + for a in args: + start = a[:len(name)+1].lower() + if (start[0] == '/' or start[0] == '-') and start[1:] == name: + return True + return False + + if have_arg(native, 'utf-8'): + return + + have_execution_charset = have_arg(native, 'execution-charset:') + have_source_charset = have_arg(native, 'source-charset:') + have_validate_charset = have_arg(native, 'validate-charset') + + options: T.List[str] = [] + if (not have_execution_charset and + not have_source_charset and + not have_validate_charset): + options = ['/utf-8'] + else: + if not have_execution_charset: + options += ['/execution-charset:utf-8'] + if not have_source_charset: + options += ['/source-charset:utf-8'] + if not have_validate_charset: + options += ['/validate-charset'] + args += options + def get_pch_suffix(self) -> str: return 'pch' @@ -252,15 +284,6 @@ def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]: # -pthread in link flags is only used on Linux elif i == '-pthread': continue - # cl.exe does not allow specifying both, so remove /utf-8 that we - # added automatically in the case the user overrides it manually. - elif (i.startswith('/source-charset:') - or i.startswith('/execution-charset:') - or i == '/validate-charset-'): - try: - result.remove('/utf-8') - except ValueError: - pass result.append(i) return result @@ -396,12 +419,6 @@ class MSVCCompiler(VisualStudioLikeCompiler): def __init__(self, target: str): super().__init__(target) - # Visual Studio 2013 and earlier don't support the /utf-8 argument. - # We want to remove it. We also want to make an explicit copy so we - # don't mutate class constant state - if mesonlib.version_compare(self.version, '<19.00') and '/utf-8' in self.always_args: - self.always_args = [r for r in self.always_args if r != '/utf-8'] - def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]: args = super().get_compile_debugfile_args(rel_obj, pch) # When generating a PDB file with PCH, all compile commands write @@ -414,11 +431,6 @@ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[ args = ['/FS'] + args return args - # Override CCompiler.get_always_args - # We want to drop '/utf-8' for Visual Studio 2013 and earlier - def get_always_args(self) -> T.List[str]: - return self.always_args - def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: if self.version.split('.')[0] == '16' and instruction_set == 'avx': # VS documentation says that this exists and should work, but diff --git a/unittests/windowstests.py b/unittests/windowstests.py index 8448ab1649cc..73e34a3ee881 100644 --- a/unittests/windowstests.py +++ b/unittests/windowstests.py @@ -437,11 +437,6 @@ def test_modules(self): self.build() def test_non_utf8_fails(self): - # FIXME: VS backend does not use flags from compiler.get_always_args() - # and thus it's missing /utf-8 argument. Was that intentional? This needs - # to be revisited. - if self.backend is not Backend.ninja: - raise SkipTest(f'This test only pass with ninja backend (not {self.backend.name}).') testdir = os.path.join(self.platform_test_dir, '18 msvc charset') env = get_fake_env(testdir, self.builddir, self.prefix) cc = detect_c_compiler(env, MachineChoice.HOST)