diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index c714dc707fed..fed893e104c7 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -76,6 +76,7 @@ machine](#specifying-options-per-machine) section for details. | -------------------------------------- | ------------- | ----------- | -------------- | ----------------- | | auto_features {enabled, disabled, auto} | auto | Override value of all 'auto' features | no | no | | backend {ninja, vs,
vs2010, vs2012, vs2013, vs2015, vs2017, vs2019, vs2022, xcode, none} | ninja | Backend to use | no | no | +| genvslite {vs2022} | vs2022 | Setup multi-builtype ninja build directories and Visual Studio solution | no | no | | buildtype {plain, debug,
debugoptimized, release, minsize, custom} | debug | Build type to use | no | no | | debug | true | Enable debug symbols and other information | no | no | | default_library {shared, static, both} | shared | Default library type | no | yes | @@ -106,6 +107,26 @@ configure with no backend at all, which is an error if you have targets to build, but for projects that need configuration + testing + installation allows for a lighter automated build pipeline. +#### Details for `genvslite` + +Setup multiple buildtype-suffixed, ninja-backend build directories (e.g. +[builddir]_[debug/release/etc.]) and generate [builddir]_vs containing a Visual +Studio solution with multiple configurations that invoke a meson compile of the +setup build directories, as appropriate for the current configuration (builtype). + +This has the effect of a simple setup macro of multiple 'meson setup ...' +invocations with a set of different buildtype values. E.g. +`meson setup ... --genvslite vs2022 somebuilddir` does the following - +``` +meson setup ... --backend ninja --buildtype debug somebuilddir_debug +meson setup ... --backend ninja --buildtype debugoptimized somebuilddir_debugoptimized +meson setup ... --backend ninja --buildtype release somebuilddir_release +``` +and additionally creates another 'somebuilddir_vs' directory that contains +a generated multi-configuration visual studio solution and project(s) that are +set to build/compile with the somebuilddir_[...] that's appropriate for the +solution's selected buildtype configuration. + #### Details for `buildtype` For setting optimization levels and diff --git a/docs/markdown/snippets/gen_vslite.md b/docs/markdown/snippets/gen_vslite.md new file mode 100644 index 000000000000..e647b0429dd1 --- /dev/null +++ b/docs/markdown/snippets/gen_vslite.md @@ -0,0 +1,11 @@ +## Added a new '--genvslite' option for use with 'meson setup ...' + +To facilitate a more usual visual studio work-flow of supporting and switching between +multiple build configurations (buildtypes) within the same solution, among other +[reasons](https://github.com/mesonbuild/meson/pull/11049), use of this new option +has the effect of setting up multiple ninja back-end-configured build directories, +named with their respective buildtype suffix. E.g. 'somebuilddir_debug', +'somebuilddir_release', etc. as well as a '_vs'-suffixed directory that contains the +generated multi-buildtype solution. Building/cleaning/rebuilding in the solution +now launches the meson build (compile) of the corresponding buildtype-suffixed build +directory, instead of using Visual Studio's native engine. \ No newline at end of file diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index dec1e68cc53e..9e247b6fc479 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -256,6 +256,13 @@ def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, i return nonebackend.NoneBackend(build, interpreter) return None + +def get_genvslite_backend(genvsname: str, build: T.Optional[build.Build] = None, interpreter: T.Optional['Interpreter'] = None) -> T.Optional['Backend']: + if genvsname == 'vs2022': + from . import vs2022backend + return vs2022backend.Vs2022Backend(build, interpreter, gen_lite = True) + return None + # This class contains the basic functionality that is needed by all backends. # Feel free to move stuff in and out of it as you see fit. class Backend: @@ -280,7 +287,17 @@ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional['Inte self.src_to_build = mesonlib.relpath(self.environment.get_build_dir(), self.environment.get_source_dir()) - def generate(self) -> None: + # If requested via 'capture = True', returns captured compile args per + # target (e.g. captured_args[target]) that can be used later, for example, + # to populate things like intellisense fields in generated visual studio + # projects (as is the case when using '--genvslite'). + # + # 'captured_compile_args_per_buildtype_and_target' is only provided when + # we expect this backend setup/generation to make use of previously captured + # compile args (as is the case when using '--genvslite'). + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: raise RuntimeError(f'generate is not implemented in {type(self).__name__}') def get_target_filename(self, t: T.Union[build.Target, build.CustomTargetIndex], *, warn_multi_output: bool = True) -> str: diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index ba614fd564f7..21398476504d 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -38,7 +38,7 @@ from ..compilers import Compiler from ..linkers import ArLikeLinker, RSPFileSyntax from ..mesonlib import ( - File, LibType, MachineChoice, MesonException, OrderedSet, PerMachine, + File, LibType, MachineChoice, MesonBugException, MesonException, OrderedSet, PerMachine, ProgressBar, quote_arg ) from ..mesonlib import get_compiler_for_source, has_path_sep, OptionKey @@ -575,7 +575,13 @@ def detect_prefix(out): raise MesonException(f'Could not determine vs dep dependency prefix string. output: {stderr} {stdout}') - def generate(self): + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: + if captured_compile_args_per_buildtype_and_target: + # We don't yet have a use case where we'd expect to make use of this, + # so no harm in catching and reporting something unexpected. + raise MesonBugException('We do not expect the ninja backend to be given a valid \'captured_compile_args_per_buildtype_and_target\'') ninja = environment.detect_ninja_command_and_version(log=True) if self.environment.coredata.get_option(OptionKey('vsenv')): builddir = Path(self.environment.get_build_dir()) @@ -614,6 +620,14 @@ def generate(self): self.build_elements = [] self.generate_phony() self.add_build_comment(NinjaComment('Build rules for targets')) + + # Optionally capture compile args per target, for later use (i.e. VisStudio project's NMake intellisense include dirs, defines, and compile options). + if capture: + captured_compile_args_per_target = {} + for target in self.build.get_targets().values(): + if isinstance(target, build.BuildTarget): + captured_compile_args_per_target[target.get_id()] = self.generate_common_compile_args_per_src_type(target) + for t in ProgressBar(self.build.get_targets().values(), desc='Generating targets'): self.generate_target(t) self.add_build_comment(NinjaComment('Test rules')) @@ -652,6 +666,9 @@ def generate(self): self.generate_compdb() self.generate_rust_project_json() + if capture: + return captured_compile_args_per_target + def generate_rust_project_json(self) -> None: """Generate a rust-analyzer compatible rust-project.json file.""" if not self.rust_crates: @@ -2922,6 +2939,34 @@ def _generate_single_compile_target_args(self, target: build.BuildTarget, compil commands += compiler.get_include_args(self.get_target_private_dir(target), False) return commands + # Returns a dictionary, mapping from each compiler src type (e.g. 'c', 'cpp', etc.) to a list of compiler arg strings + # used for that respective src type. + # Currently used for the purpose of populating VisualStudio intellisense fields but possibly useful in other scenarios. + def generate_common_compile_args_per_src_type(self, target: build.BuildTarget) -> dict[str, list[str]]: + src_type_to_args = {} + + use_pch = self.environment.coredata.options.get(OptionKey('b_pch')) + + for src_type_str in target.compilers.keys(): + compiler = target.compilers[src_type_str] + commands = self._generate_single_compile_base_args(target, compiler) + + # Include PCH header as first thing as it must be the first one or it will be + # ignored by gcc https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100462 + if use_pch and 'mw' not in compiler.id: + commands += self.get_pch_include_args(compiler, target) + + commands += self._generate_single_compile_target_args(target, compiler, is_generated=False) + + # Metrowerks compilers require PCH include args to come after intraprocedural analysis args + if use_pch and 'mw' in compiler.id: + commands += self.get_pch_include_args(compiler, target) + + commands = commands.compiler.compiler_args(commands) + + src_type_to_args[src_type_str] = commands.to_native() + return src_type_to_args + def generate_single_compile(self, target: build.BuildTarget, src, is_generated=False, header_deps=None, order_deps: T.Optional[T.List['mesonlib.FileOrString']] = None, diff --git a/mesonbuild/backend/nonebackend.py b/mesonbuild/backend/nonebackend.py index cf7c3dde93da..79ee7b2e1c31 100644 --- a/mesonbuild/backend/nonebackend.py +++ b/mesonbuild/backend/nonebackend.py @@ -14,6 +14,8 @@ from __future__ import annotations +import typing as T + from .backends import Backend from .. import mlog from ..mesonlib import MesonBugException @@ -23,7 +25,15 @@ class NoneBackend(Backend): name = 'none' - def generate(self): + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect the none backend to generate with \'capture = True\'') + if captured_compile_args_per_buildtype_and_target: + raise MesonBugException('We do not expect the none backend to be given a valid \'captured_compile_args_per_buildtype_and_target\'') + if self.build.get_targets(): raise MesonBugException('None backend cannot generate target rules, but should have failed earlier.') mlog.log('Generating simple install-only backend') diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index d6b2f44c4102..501525a8f3de 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -22,15 +22,18 @@ import typing as T from pathlib import Path, PurePath import re +from collections import Counter from . import backends from .. import build from .. import mlog from .. import compilers +from .. import mesonlib from ..mesonlib import ( - File, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice + File, MesonBugException, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice ) from ..environment import Environment, build_filename +from .. import coredata if T.TYPE_CHECKING: from ..arglist import CompilerArgs @@ -94,18 +97,57 @@ def split_o_flags_args(args: T.List[str]) -> T.List[str]: o_flags += ['/O' + f for f in flags] return o_flags - def generate_guid_from_path(path, path_type) -> str: return str(uuid.uuid5(uuid.NAMESPACE_URL, 'meson-vs-' + path_type + ':' + str(path))).upper() def detect_microsoft_gdk(platform: str) -> bool: return re.match(r'Gaming\.(Desktop|Xbox.XboxOne|Xbox.Scarlett)\.x64', platform, re.IGNORECASE) +def filtered_src_langs_generator(sources: T.List[str]): + for src in sources: + ext = src.split('.')[-1] + if compilers.compilers.is_source_suffix(ext): + yield compilers.compilers.SUFFIX_TO_LANG[ext] + +# Returns the source language (i.e. a key from 'lang_suffixes') of the most frequent source language in the given +# list of sources. +# We choose the most frequent language as 'primary' because it means the most sources in a target/project can +# simply refer to the project's shared intellisense define and include fields, rather than have to fill out their +# own duplicate full set of defines/includes/opts intellisense fields. All of which helps keep the vcxproj file +# size down. +def get_primary_source_lang(target_sources: T.List[File], custom_sources: T.List[str]) -> T.Optional[str]: + lang_counts = Counter([compilers.compilers.SUFFIX_TO_LANG[src.suffix] for src in target_sources if compilers.compilers.is_source_suffix(src.suffix)]) + lang_counts += Counter(filtered_src_langs_generator(custom_sources)) + most_common_lang_list = lang_counts.most_common(1) + # It may be possible that we have a target with no actual src files of interest (e.g. a generator target), + # leaving us with an empty list, which we should handle - + return most_common_lang_list[0][0] if most_common_lang_list else None + +# Returns a dictionary (by [src type][build type]) that contains a tuple of - +# (pre-processor defines, include paths, additional compiler options) +# fields to use to fill in the respective intellisense fields of sources that can't simply +# reference and re-use the shared 'primary' language intellisense fields of the vcxproj. +def get_non_primary_lang_intellisense_fields(captured_compile_args_per_buildtype_and_target: dict, + target_id: str, + primary_src_lang: str) -> T.Dict[str, T.Dict[str, T.Tuple[str, str, str]]]: + defs_paths_opts_per_lang_and_buildtype = {} + for buildtype in coredata.get_genvs_default_buildtype_list(): + captured_build_args = captured_compile_args_per_buildtype_and_target[buildtype][target_id] # Results in a 'Src types to compile args' dict + non_primary_build_args_per_src_lang = [(lang, build_args) for lang, build_args in captured_build_args.items() if lang != primary_src_lang] # Only need to individually populate intellisense fields for sources of non-primary types. + for src_lang, args_list in non_primary_build_args_per_src_lang: + if src_lang not in defs_paths_opts_per_lang_and_buildtype: + defs_paths_opts_per_lang_and_buildtype[src_lang] = {} + defs = Vs2010Backend.extract_nmake_preprocessor_defs(args_list) + paths = Vs2010Backend.extract_nmake_include_paths(args_list) + opts = Vs2010Backend.extract_intellisense_additional_compiler_options(args_list) + defs_paths_opts_per_lang_and_buildtype[src_lang][buildtype] = (defs, paths, opts) + return defs_paths_opts_per_lang_and_buildtype + class Vs2010Backend(backends.Backend): name = 'vs2010' - def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): + def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter], gen_lite: bool = False): super().__init__(build, interpreter) self.project_file_version = '10.0.30319.1' self.sln_file_version = '11.00' @@ -115,6 +157,7 @@ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Inter self.windows_target_platform_version = None self.subdirs = {} self.handled_target_deps = {} + self.gen_lite = gen_lite # Synonymous with generating the simpler makefile-style multi-config projects that invoke 'meson compile' builds, avoiding native MSBuild complications def get_target_private_dir(self, target): return os.path.join(self.get_target_dir(target), target.get_id()) @@ -189,7 +232,12 @@ def generate_custom_generator_commands(self, target, parent_node): self.generate_genlist_for_target(genlist, target, parent_node, generator_output_files, custom_target_include_dirs, custom_target_output_files) return generator_output_files, custom_target_output_files, custom_target_include_dirs - def generate(self) -> None: + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect any vs backend to generate with \'capture = True\'') target_machine = self.interpreter.builtin['target_machine'].cpu_family_method(None, None) if target_machine in {'64', 'x86_64'}: # amd64 or x86_64 @@ -238,10 +286,10 @@ def generate(self) -> None: except MesonException: self.sanitize = 'none' 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')) - self.gen_installproj('RUN_INSTALL', os.path.join(self.environment.get_build_dir(), 'RUN_INSTALL.vcxproj')) - self.gen_regenproj('REGEN', os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj')) + projlist = self.generate_projects(captured_compile_args_per_buildtype_and_target) + self.gen_testproj() + self.gen_installproj() + self.gen_regenproj() self.generate_solution(sln_filename, projlist) self.generate_regen_info() Vs2010Backend.touch_regen_timestamp(self.environment.get_build_dir()) @@ -382,8 +430,7 @@ def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> Non ofile.write('# Visual Studio %s\n' % self.sln_version_comment) prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n' for prj in projlist: - coredata = self.environment.coredata - if coredata.get_option(OptionKey('layout')) == 'mirror': + if self.environment.coredata.get_option(OptionKey('layout')) == 'mirror': self.generate_solution_dirs(ofile, prj[1].parents) target = self.build.targets[prj[0]] lang = 'default' @@ -409,8 +456,14 @@ def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> Non self.environment.coredata.test_guid) ofile.write(test_line) ofile.write('EndProject\n') + if self.gen_lite: # REGEN is replaced by the lighter-weight RECONFIGURE utility, for now. See comment in 'gen_regenproj' + regen_proj_name = 'RECONFIGURE' + regen_proj_fname = 'RECONFIGURE.vcxproj' + else: + regen_proj_name = 'REGEN' + regen_proj_fname = 'REGEN.vcxproj' regen_line = prj_templ % (self.environment.coredata.lang_guids['default'], - 'REGEN', 'REGEN.vcxproj', + regen_proj_name, regen_proj_fname, self.environment.coredata.regen_guid) ofile.write(regen_line) ofile.write('EndProject\n') @@ -422,18 +475,23 @@ def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> Non ofile.write('Global\n') ofile.write('\tGlobalSection(SolutionConfigurationPlatforms) = ' 'preSolution\n') - ofile.write('\t\t%s|%s = %s|%s\n' % - (self.buildtype, self.platform, self.buildtype, - self.platform)) + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() if self.gen_lite else [self.buildtype] + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t%s|%s = %s|%s\n' % + (buildtype, self.platform, buildtype, + self.platform)) ofile.write('\tEndGlobalSection\n') ofile.write('\tGlobalSection(ProjectConfigurationPlatforms) = ' 'postSolution\n') - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.regen_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) - ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % - (self.environment.coredata.regen_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) + # REGEN project (multi-)configurations + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (self.environment.coredata.regen_guid, buildtype, + self.platform, buildtype, self.platform)) + if not self.gen_lite: # With a 'genvslite'-generated solution, the regen (i.e. reconfigure) utility is only intended to run when the user explicitly builds this proj. + ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % + (self.environment.coredata.regen_guid, buildtype, + self.platform, buildtype, self.platform)) # Create the solution configuration for p in projlist: if p[3] is MachineChoice.BUILD: @@ -441,21 +499,31 @@ def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> Non else: config_platform = self.platform # Add to the list of projects in this solution + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (p[2], buildtype, self.platform, + buildtype, config_platform)) + # If we're building the solution with Visual Studio's build system, enable building of buildable + # projects. However, if we're building with meson (via --genvslite), then, since each project's + # 'build' action just ends up doing the same 'meson compile ...' we don't want the 'solution build' + # repeatedly going off and doing the same 'meson compile ...' multiple times over, so we just + # leave it up to the user to select or build just one project. + # FIXME: Would be slightly nicer if we could enable building of just one top level target/project, + # but not sure how to identify that. + if not self.gen_lite and \ + p[0] in default_projlist and \ + not isinstance(self.build.targets[p[0]], build.RunTarget): + ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % + (p[2], buildtype, self.platform, + buildtype, config_platform)) + # RUN_TESTS and RUN_INSTALL project (multi-)configurations + for buildtype in multi_config_buildtype_list: ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (p[2], self.buildtype, self.platform, - self.buildtype, config_platform)) - if p[0] in default_projlist and \ - not isinstance(self.build.targets[p[0]], build.RunTarget): - # Add to the list of projects to be built - ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % - (p[2], self.buildtype, self.platform, - self.buildtype, config_platform)) - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.test_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.install_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) + (self.environment.coredata.test_guid, buildtype, + self.platform, buildtype, self.platform)) + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (self.environment.coredata.install_guid, buildtype, + self.platform, buildtype, self.platform)) ofile.write('\tEndGlobalSection\n') ofile.write('\tGlobalSection(SolutionProperties) = preSolution\n') ofile.write('\t\tHideSolutionNode = FALSE\n') @@ -473,7 +541,7 @@ def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> Non ofile.write('EndGlobal\n') replace_if_different(sln_filename, sln_filename_tmp) - def generate_projects(self) -> T.List[Project]: + def generate_projects(self, captured_compile_args_per_buildtype_and_target: dict = None) -> T.List[Project]: startup_project = self.environment.coredata.options[OptionKey('backend_startup_project')].value projlist: T.List[Project] = [] startup_idx = 0 @@ -490,8 +558,9 @@ def generate_projects(self) -> T.List[Project]: relname = target_dir / fname projfile_path = outdir / fname proj_uuid = self.environment.coredata.target_guids[name] - self.gen_vcxproj(target, str(projfile_path), proj_uuid) - projlist.append((name, relname, proj_uuid, target.for_machine)) + generated = self.gen_vcxproj(target, str(projfile_path), proj_uuid, captured_compile_args_per_buildtype_and_target) + if generated: + projlist.append((name, relname, proj_uuid, target.for_machine)) # Put the startup project first in the project list if startup_idx: @@ -570,12 +639,13 @@ def create_basic_project(self, target_name, *, confitems = ET.SubElement(root, 'ItemGroup', {'Label': 'ProjectConfigurations'}) if not target_platform: target_platform = self.platform - prjconf = ET.SubElement(confitems, 'ProjectConfiguration', - {'Include': self.buildtype + '|' + target_platform}) - p = ET.SubElement(prjconf, 'Configuration') - p.text = self.buildtype - pl = ET.SubElement(prjconf, 'Platform') - pl.text = target_platform + + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() if self.gen_lite else [self.buildtype] + for buildtype in multi_config_buildtype_list: + prjconf = ET.SubElement(confitems, 'ProjectConfiguration', + {'Include': buildtype + '|' + target_platform}) + ET.SubElement(prjconf, 'Configuration').text = buildtype + ET.SubElement(prjconf, 'Platform').text = target_platform # Globals globalgroup = ET.SubElement(root, 'PropertyGroup', Label='Globals') @@ -583,46 +653,52 @@ def create_basic_project(self, target_name, *, guidelem.text = '{%s}' % guid kw = ET.SubElement(globalgroup, 'Keyword') kw.text = self.platform + 'Proj' - # XXX Wasn't here before for anything but gen_vcxproj , but seems fine? - ns = ET.SubElement(globalgroup, 'RootNamespace') - ns.text = target_name - - p = ET.SubElement(globalgroup, 'Platform') - p.text = target_platform - pname = ET.SubElement(globalgroup, 'ProjectName') - pname.text = target_name - if self.windows_target_platform_version: - ET.SubElement(globalgroup, 'WindowsTargetPlatformVersion').text = self.windows_target_platform_version - ET.SubElement(globalgroup, 'UseMultiToolTask').text = 'true' ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.Default.props') - # Start configuration + # Configuration type_config = ET.SubElement(root, 'PropertyGroup', Label='Configuration') ET.SubElement(type_config, 'ConfigurationType').text = conftype - ET.SubElement(type_config, 'CharacterSet').text = 'MultiByte' - # Fixme: wasn't here before for gen_vcxproj() - ET.SubElement(type_config, 'UseOfMfc').text = 'false' if self.platform_toolset: ET.SubElement(type_config, 'PlatformToolset').text = self.platform_toolset - # End configuration section (but it can be added to further via type_config) + # This must come AFTER the '' element; importing before the 'PlatformToolset' elt + # gets set leads to msbuild failures reporting - + # "The build tools for v142 (Platform Toolset = 'v142') cannot be found. ... please install v142 build tools." + # This is extremely unhelpful and misleading since the v14x build tools ARE installed. ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.props') - # Project information - direlem = ET.SubElement(root, 'PropertyGroup') - fver = ET.SubElement(direlem, '_ProjectFileVersion') - fver.text = self.project_file_version - outdir = ET.SubElement(direlem, 'OutDir') - outdir.text = '.\\' - intdir = ET.SubElement(direlem, 'IntDir') - intdir.text = temp_dir + '\\' - - tname = ET.SubElement(direlem, 'TargetName') - tname.text = target_name - - if target_ext: - ET.SubElement(direlem, 'TargetExt').text = target_ext + if not self.gen_lite: # Plenty of elements aren't necessary for 'makefile'-style project that just redirects to meson builds + # XXX Wasn't here before for anything but gen_vcxproj , but seems fine? + ns = ET.SubElement(globalgroup, 'RootNamespace') + ns.text = target_name + + p = ET.SubElement(globalgroup, 'Platform') + p.text = target_platform + pname = ET.SubElement(globalgroup, 'ProjectName') + pname.text = target_name + if self.windows_target_platform_version: + ET.SubElement(globalgroup, 'WindowsTargetPlatformVersion').text = self.windows_target_platform_version + ET.SubElement(globalgroup, 'UseMultiToolTask').text = 'true' + + ET.SubElement(type_config, 'CharacterSet').text = 'MultiByte' + # Fixme: wasn't here before for gen_vcxproj() + ET.SubElement(type_config, 'UseOfMfc').text = 'false' + + # Project information + direlem = ET.SubElement(root, 'PropertyGroup') + fver = ET.SubElement(direlem, '_ProjectFileVersion') + fver.text = self.project_file_version + outdir = ET.SubElement(direlem, 'OutDir') + outdir.text = '.\\' + intdir = ET.SubElement(direlem, 'IntDir') + intdir.text = temp_dir + '\\' + + tname = ET.SubElement(direlem, 'TargetName') + tname.text = target_name + + if target_ext: + ET.SubElement(direlem, 'TargetExt').text = target_ext return (root, type_config) @@ -780,6 +856,36 @@ def add_additional_options(self, lang, parent_node, file_args): args.append(self.escape_additional_option(arg)) ET.SubElement(parent_node, "AdditionalOptions").text = ' '.join(args) + # Set up each project's source file ('CLCompile') element with appropriate preprocessor, include dir, and compile option values for correct intellisense. + def add_project_nmake_defs_incs_and_opts(self, parent_node, src: str, defs_paths_opts_per_lang_and_buildtype: dict, platform: str): + # For compactness, sources whose type matches the primary src type (i.e. most frequent in the set of source types used in the target/project, + # according to the 'captured_build_args' map), can simply reference the preprocessor definitions, include dirs, and compile option NMake fields of + # the project itself. + # However, if a src is of a non-primary type, it could have totally different defs/dirs/options so we're going to have to fill in the full, verbose + # set of values for these fields, which needs to be fully expanded per build type / configuration. + # + # FIXME: Suppose a project contains .cpp and .c src files with different compile defs/dirs/options, while also having .h files, some of which + # are included by .cpp sources and others included by .c sources: How do we know whether the .h source should be using the .cpp or .c src + # defs/dirs/options? Might it also be possible for a .h header to be shared between .cpp and .c sources? If so, I don't see how we can + # correctly configure these intellisense fields. + # For now, all sources/headers that fail to find their extension's language in the '...nmake_defs_paths_opts...' map will just adopt the project + # defs/dirs/opts that are set for the nominal 'primary' src type. + ext = src.split('.')[-1] + lang = compilers.compilers.SUFFIX_TO_LANG.get(ext, None) + if lang in defs_paths_opts_per_lang_and_buildtype.keys(): + # This is a non-primary src type for which can't simply reference the project's nmake fields; + # we must laboriously fill in the fields for all buildtypes. + for buildtype in coredata.get_genvs_default_buildtype_list(): + (defs, paths, opts) = defs_paths_opts_per_lang_and_buildtype[lang][buildtype] + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{platform}\'' + ET.SubElement(parent_node, 'PreprocessorDefinitions', Condition=condition).text = defs + ET.SubElement(parent_node, 'AdditionalIncludeDirectories', Condition=condition).text = paths + ET.SubElement(parent_node, 'AdditionalOptions', Condition=condition).text = opts + else: # Can't find bespoke nmake defs/dirs/opts fields for this extention, so just reference the project's fields + ET.SubElement(parent_node, 'PreprocessorDefinitions').text = '$(NMakePreprocessorDefinitions)' + ET.SubElement(parent_node, 'AdditionalIncludeDirectories').text = '$(NMakeIncludeSearchPath)' + ET.SubElement(parent_node, 'AdditionalOptions').text = '$(AdditionalOptions)' + def add_preprocessor_defines(self, lang, parent_node, file_defines): defines = [] for define in file_defines[lang]: @@ -918,147 +1024,8 @@ def _prettyprint_vcxproj_xml(self, tree: ET.ElementTree, ofname: str) -> None: of.write(doc.toprettyxml()) replace_if_different(ofname, ofname_tmp) - def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None: - mlog.debug(f'Generating vcxproj {target.name}.') - subsystem = 'Windows' - self.handled_target_deps[target.get_id()] = [] - if isinstance(target, build.Executable): - conftype = 'Application' - if target.gui_app is not None: - if not target.gui_app: - subsystem = 'Console' - else: - # If someone knows how to set the version properly, - # please send a patch. - subsystem = target.win_subsystem.split(',')[0] - elif isinstance(target, build.StaticLibrary): - conftype = 'StaticLibrary' - elif isinstance(target, build.SharedLibrary): - conftype = 'DynamicLibrary' - elif isinstance(target, build.CustomTarget): - return self.gen_custom_target_vcxproj(target, ofname, guid) - elif isinstance(target, build.RunTarget): - return self.gen_run_target_vcxproj(target, ofname, guid) - elif isinstance(target, build.CompileTarget): - return self.gen_compile_target_vcxproj(target, ofname, guid) - else: - raise MesonException(f'Unknown target type for {target.get_basename()}') - assert isinstance(target, (build.Executable, build.SharedLibrary, build.StaticLibrary, build.SharedModule)), 'for mypy' - # Prefix to use to access the build root from the vcxproj dir - down = self.target_to_build_root(target) - # Prefix to use to access the source tree's root from the vcxproj dir - proj_to_src_root = os.path.join(down, self.build_to_src) - # Prefix to use to access the source tree's subdir from the vcxproj dir - proj_to_src_dir = os.path.join(proj_to_src_root, self.get_target_dir(target)) - (sources, headers, objects, languages) = self.split_sources(target.sources) - if target.is_unity: - sources = self.generate_unity_files(target, sources) - compiler = self._get_cl_compiler(target) - build_args = compiler.get_buildtype_args(self.buildtype) - build_args += compiler.get_optimization_args(self.optimization) - build_args += compiler.get_debug_args(self.debug) - build_args += compiler.sanitizer_compile_args(self.sanitize) - buildtype_link_args = compiler.get_buildtype_linker_args(self.buildtype) - vscrt_type = self.environment.coredata.options[OptionKey('b_vscrt')] - target_name = target.name - if target.for_machine is MachineChoice.BUILD: - platform = self.build_platform - else: - platform = self.platform - - tfilename = os.path.splitext(target.get_filename()) - - (root, type_config) = self.create_basic_project(tfilename[0], - temp_dir=target.get_id(), - guid=guid, - conftype=conftype, - target_ext=tfilename[1], - target_platform=platform) - # vcxproj.filters file - root_filter = self.create_basic_project_filters() - - # FIXME: Should these just be set in create_basic_project(), even if - # irrelevant for current target? - - # FIXME: Meson's LTO support needs to be integrated here - ET.SubElement(type_config, 'WholeProgramOptimization').text = 'false' - # Let VS auto-set the RTC level - ET.SubElement(type_config, 'BasicRuntimeChecks').text = 'Default' - # Incremental linking increases code size - if '/INCREMENTAL:NO' in buildtype_link_args: - ET.SubElement(type_config, 'LinkIncremental').text = 'false' - - # Build information - compiles = ET.SubElement(root, 'ItemDefinitionGroup') - clconf = ET.SubElement(compiles, 'ClCompile') - # CRT type; debug or release - if vscrt_type.value == 'from_buildtype': - if self.buildtype == 'debug': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' - elif vscrt_type.value == 'static_from_buildtype': - if self.buildtype == 'debug': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' - elif vscrt_type.value == 'mdd': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' - elif vscrt_type.value == 'mt': - # FIXME, wrong - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' - elif vscrt_type.value == 'mtd': - # FIXME, wrong - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' - # Sanitizers - if '/fsanitize=address' in build_args: - ET.SubElement(type_config, 'EnableASAN').text = 'true' - # Debug format - if '/ZI' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue' - elif '/Zi' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase' - elif '/Z7' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle' - else: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'None' - # Runtime checks - if '/RTC1' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks' - elif '/RTCu' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck' - elif '/RTCs' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck' - # 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: - eh = self.environment.coredata.options[OptionKey('eh', machine=target.for_machine, lang='cpp')] - if eh.value == 'a': - ET.SubElement(clconf, 'ExceptionHandling').text = 'Async' - elif eh.value == 's': - ET.SubElement(clconf, 'ExceptionHandling').text = 'SyncCThrow' - elif eh.value == 'none': - ET.SubElement(clconf, 'ExceptionHandling').text = 'false' - else: # 'sc' or 'default' - ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync' - generated_files, custom_target_output_files, generated_files_include_dirs = self.generate_custom_generator_commands( - target, root) - (gen_src, gen_hdrs, gen_objs, gen_langs) = self.split_sources(generated_files) - (custom_src, custom_hdrs, custom_objs, custom_langs) = self.split_sources(custom_target_output_files) - gen_src += custom_src - gen_hdrs += custom_hdrs - gen_langs += custom_langs - + # Returns: (target_args,file_args), (target_defines,file_defines), (target_inc_dirs,file_inc_dirs) + def get_args_defines_and_inc_dirs(self, target, compiler, generated_files_include_dirs, proj_to_src_root, proj_to_src_dir, build_args): # Arguments, include dirs, defines for all files in the current target target_args = [] target_defines = [] @@ -1175,9 +1142,7 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None for d in reversed(target.get_external_deps()): # Cflags required by external deps might have UNIX-specific flags, # so filter them out if needed - if d.name == 'openmp': - ET.SubElement(clconf, 'OpenMPSupport').text = 'true' - else: + if d.name != 'openmp': d_compile_args = compiler.unix_args_to_native(d.get_compile_args()) for arg in d_compile_args: if arg.startswith(('-D', '/D')): @@ -1194,9 +1159,263 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None else: target_args.append(arg) - languages += gen_langs if '/Gw' in build_args: target_args.append('/Gw') + + return (target_args, file_args), (target_defines, file_defines), (target_inc_dirs, file_inc_dirs) + + @staticmethod + def get_build_args(compiler, buildtype: str, optimization_level: str, debug: bool, sanitize: str) -> T.List[str]: + build_args = compiler.get_buildtype_args(buildtype) + build_args += compiler.get_optimization_args(optimization_level) + build_args += compiler.get_debug_args(debug) + build_args += compiler.sanitizer_compile_args(sanitize) + + return build_args + + #Convert a list of compile arguments from - + # [ '-I..\\some\\dir\\include', '-I../../some/other/dir', '/MDd', '/W2', '/std:c++17', '/Od', '/Zi', '-DSOME_DEF=1', '-DANOTHER_DEF=someval', ...] + #to - + # 'SOME_DEF=1;ANOTHER_DEF=someval;' + #which is the format required by the visual studio project's NMakePreprocessorDefinitions field. + @staticmethod + def extract_nmake_preprocessor_defs(captured_build_args: list[str]) -> str: + defs = '' + for arg in captured_build_args: + if arg.startswith(('-D', '/D')): + defs += arg[2:] + ';' + return defs + + #Convert a list of compile arguments from - + # [ '-I..\\some\\dir\\include', '-I../../some/other/dir', '/MDd', '/W2', '/std:c++17', '/Od', '/Zi', '-DSOME_DEF=1', '-DANOTHER_DEF=someval', ...] + #to - + # '..\\some\\dir\\include;../../some/other/dir;' + #which is the format required by the visual studio project's NMakePreprocessorDefinitions field. + @staticmethod + def extract_nmake_include_paths(captured_build_args: list[str]) -> str: + paths = '' + for arg in captured_build_args: + if arg.startswith(('-I', '/I')): + paths += arg[2:] + ';' + return paths + + #Convert a list of compile arguments from - + # [ '-I..\\some\\dir\\include', '-I../../some/other/dir', '/MDd', '/W2', '/std:c++17', '/Od', '/Zi', '-DSOME_DEF=1', '-DANOTHER_DEF=someval', ...] + #to - + # '/MDd;/W2;/std:c++17;/Od/Zi' + #which is the format required by the visual studio project's NMakePreprocessorDefinitions field. + @staticmethod + def extract_intellisense_additional_compiler_options(captured_build_args: list[str]) -> str: + additional_opts = '' + for arg in captured_build_args: + if (not arg.startswith(('-D', '/D', '-I', '/I'))) and arg.startswith(('-', '/')): + additional_opts += arg + ';' + return additional_opts + + @staticmethod + def get_nmake_base_meson_command_and_exe_search_paths() -> T.Tuple[str, str]: + meson_cmd_list = mesonlib.get_meson_command() + assert (len(meson_cmd_list) == 1) or (len(meson_cmd_list) == 2) + # We expect get_meson_command() to either be of the form - + # 1: ['path/to/meson.exe'] + # or - + # 2: ['path/to/python.exe', 'and/path/to/meson.py'] + # so we'd like to ensure our makefile-style project invokes the same meson executable or python src as this instance. + exe_search_paths = os.path.dirname(meson_cmd_list[0]) + nmake_base_meson_command = os.path.basename(meson_cmd_list[0]) + if len(meson_cmd_list) != 1: + # We expect to be dealing with case '2', shown above. + # With Windows, it's also possible that we get a path to the second element of meson_cmd_list that contains spaces + # (e.g. 'and/path to/meson.py'). So, because this will end up directly in the makefile/NMake command lines, we'd + # better always enclose it in quotes. Only strictly necessary for paths with spaces but no harm for paths without - + nmake_base_meson_command += ' \"' + meson_cmd_list[1] + '\"' + exe_search_paths += ';' + os.path.dirname(meson_cmd_list[1]) + + # Additionally, in some cases, we appear to have to add 'C:\Windows\system32;C:\Windows' to the 'Path' environment (via the + # ExecutablePath element), without which, the 'meson compile ...' (NMakeBuildCommandLine) command can fail (failure to find + # stdio.h and similar), so something is quietly switching some critical build behaviour based on the presence of these in + # the 'Path'. + # Not sure if this ultimately comes down to some 'find and guess' hidden behaviours within meson or within MSVC tools, but + # I guess some projects may implicitly rely on this behaviour. + # Things would be cleaner, more robust, repeatable, and portable if meson (and msvc tools) replaced all this kind of + # find/guess behaviour with the requirement that things just be explicitly specified by the user. + # An example of this can be seen with - + # 1: Download https://github.com/facebook/zstd source + # 2: cd to the 'zstd-dev\build\meson' dir + # 3: meson setup -Dbin_programs=true -Dbin_contrib=true --genvslite vs2022 builddir_vslite + # 4: Open the generated 'builddir_vslite_vs\zstd.sln' and build through a project, which should explicitly add the above to + # the project's 'Executable Directories' paths and build successfully. + # 5: Remove 'C:\Windows\system32;C:\Windows;' from the same project's 'Executable Directories' paths and rebuild. + # This should now fail. + # It feels uncomfortable to do this but what better alternative is there (and might this introduce new problems)? - + exe_search_paths += ';C:\\Windows\\system32;C:\\Windows' + # A meson project that explicitly specifies compiler/linker tools and sdk/include paths is not going to have any problems + # with this addition. + + return (nmake_base_meson_command, exe_search_paths) + + def add_gen_lite_makefile_vcxproj_elements(self, + root: ET.Element, + platform: str, + target_ext: str, + captured_compile_args_per_buildtype_and_target: dict, + target, + proj_to_build_root: str, + primary_src_lang: T.Optional[str]) -> None: + ET.SubElement(root, 'ImportGroup', Label='ExtensionSettings') + ET.SubElement(root, 'ImportGroup', Label='Shared') + prop_sheets_grp = ET.SubElement(root, 'ImportGroup', Label='PropertySheets') + ET.SubElement(prop_sheets_grp, 'Import', {'Project': r'$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props', + 'Condition': r"exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')", + 'Label': 'LocalAppDataPlatform' + }) + ET.SubElement(root, 'PropertyGroup', Label='UserMacros') + + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + + # Relative path from this .vcxproj to the directory containing the set of '..._[debug/debugoptimized/release]' setup meson build dirs. + proj_to_multiconfigured_builds_parent_dir = os.path.join(proj_to_build_root, '..') + + # Conditional property groups per configuration (buildtype). E.g. - + # + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + for buildtype in multi_config_buildtype_list: + per_config_prop_group = ET.SubElement(root, 'PropertyGroup', Condition=f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{platform}\'') + (_, build_dir_tail) = os.path.split(self.src_to_build) + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + ET.SubElement(per_config_prop_group, 'OutDir').text = f'{proj_to_build_dir_for_buildtype}\\' + ET.SubElement(per_config_prop_group, 'IntDir').text = f'{proj_to_build_dir_for_buildtype}\\' + ET.SubElement(per_config_prop_group, 'NMakeBuildCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}"' + ET.SubElement(per_config_prop_group, 'NMakeOutput').text = f'$(OutDir){target.name}{target_ext}' + captured_build_args = captured_compile_args_per_buildtype_and_target[buildtype][target.get_id()] + # 'captured_build_args' is a dictionary, mapping from each src file type to a list of compile args to use for that type. + # Usually, there's just one but we could have multiple src types. However, since there's only one field for the makefile + # project's NMake... preprocessor/include intellisense fields, we'll just use the first src type we have to fill in + # these fields. Then, any src files in this VS project that aren't of this first src type will then need to override + # its intellisense fields instead of simply referencing the values in the project. + ET.SubElement(per_config_prop_group, 'NMakeReBuildCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}" --clean && {nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}"' + ET.SubElement(per_config_prop_group, 'NMakeCleanCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}" --clean' + # Need to set the 'ExecutablePath' element for the above NMake... commands to be able to invoke the meson command. + ET.SubElement(per_config_prop_group, 'ExecutablePath').text = exe_search_paths + # We may not have any src files and so won't have a primary src language. In which case, we've nothing to fill in for this target's intellisense fields - + if primary_src_lang: + primary_src_type_build_args = captured_build_args[primary_src_lang] + ET.SubElement(per_config_prop_group, 'NMakePreprocessorDefinitions').text = Vs2010Backend.extract_nmake_preprocessor_defs(primary_src_type_build_args) + ET.SubElement(per_config_prop_group, 'NMakeIncludeSearchPath').text = Vs2010Backend.extract_nmake_include_paths(primary_src_type_build_args) + ET.SubElement(per_config_prop_group, 'AdditionalOptions').text = Vs2010Backend.extract_intellisense_additional_compiler_options(primary_src_type_build_args) + + # Unless we explicitly specify the following empty path elements, the project is assigned a load of nasty defaults that fill these + # with values like - + # $(VC_IncludePath);$(WindowsSDK_IncludePath); + # which are all based on the current install environment (a recipe for non-reproducibility problems), not the paths that will be used by + # the actual meson compile jobs. Although these elements look like they're only for MSBuild operations, they're not needed with our simple, + # lite/makefile-style projects so let's just remove them in case they do get used/confused by intellisense. + ET.SubElement(per_config_prop_group, 'IncludePath') + ET.SubElement(per_config_prop_group, 'ExternalIncludePath') + ET.SubElement(per_config_prop_group, 'ReferencePath') + ET.SubElement(per_config_prop_group, 'LibraryPath') + ET.SubElement(per_config_prop_group, 'LibraryWPath') + ET.SubElement(per_config_prop_group, 'SourcePath') + ET.SubElement(per_config_prop_group, 'ExcludePath') + + def add_non_makefile_vcxproj_elements( + self, + root: ET.Element, + type_config: ET.Element, + target, + platform: str, + subsystem, + build_args, + target_args, + target_defines, + target_inc_dirs, + file_args + ) -> None: + compiler = self._get_cl_compiler(target) + buildtype_link_args = compiler.get_buildtype_linker_args(self.buildtype) + + # Prefix to use to access the build root from the vcxproj dir + down = self.target_to_build_root(target) + + # FIXME: Should the following just be set in create_basic_project(), even if + # irrelevant for current target? + + # FIXME: Meson's LTO support needs to be integrated here + ET.SubElement(type_config, 'WholeProgramOptimization').text = 'false' + # Let VS auto-set the RTC level + ET.SubElement(type_config, 'BasicRuntimeChecks').text = 'Default' + # Incremental linking increases code size + if '/INCREMENTAL:NO' in buildtype_link_args: + ET.SubElement(type_config, 'LinkIncremental').text = 'false' + + # Build information + compiles = ET.SubElement(root, 'ItemDefinitionGroup') + 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 = self.environment.coredata.options[OptionKey('b_vscrt')] + if vscrt_type.value == 'from_buildtype': + if self.buildtype == 'debug': + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' + else: + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' + elif vscrt_type.value == 'static_from_buildtype': + if self.buildtype == 'debug': + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' + else: + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' + elif vscrt_type.value == 'mdd': + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' + elif vscrt_type.value == 'mt': + # FIXME, wrong + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' + elif vscrt_type.value == 'mtd': + # FIXME, wrong + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' + else: + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' + # Sanitizers + if '/fsanitize=address' in build_args: + ET.SubElement(type_config, 'EnableASAN').text = 'true' + # Debug format + if '/ZI' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue' + elif '/Zi' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase' + elif '/Z7' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle' + else: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'None' + # Runtime checks + if '/RTC1' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks' + elif '/RTCu' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck' + elif '/RTCs' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck' + # 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: + eh = self.environment.coredata.options[OptionKey('eh', machine=target.for_machine, lang='cpp')] + if eh.value == 'a': + ET.SubElement(clconf, 'ExceptionHandling').text = 'Async' + elif eh.value == 's': + ET.SubElement(clconf, 'ExceptionHandling').text = 'SyncCThrow' + elif eh.value == 'none': + ET.SubElement(clconf, 'ExceptionHandling').text = 'false' + else: # 'sc' or 'default' + ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync' + if len(target_args) > 0: target_args.append('%(AdditionalOptions)') ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(target_args) @@ -1233,25 +1452,6 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Speed' # Note: SuppressStartupBanner is /NOLOGO and is 'true' by default self.generate_lang_standard_info(file_args, clconf) - pch_sources = {} - if self.environment.coredata.options.get(OptionKey('b_pch')): - for lang in ['c', 'cpp']: - pch = target.get_pch(lang) - if not pch: - continue - if compiler.id == 'msvc': - if len(pch) == 1: - # Auto generate PCH. - src = os.path.join(down, self.create_msvc_pch_implementation(target, lang, pch[0])) - pch_header_dir = os.path.dirname(os.path.join(proj_to_src_dir, pch[0])) - else: - src = os.path.join(proj_to_src_dir, pch[1]) - pch_header_dir = None - pch_sources[lang] = [pch[0], src, lang, pch_header_dir] - else: - # I don't know whether its relevant but let's handle other compilers - # used with a vs backend - pch_sources[lang] = [pch[0], None, lang, None] resourcecompile = ET.SubElement(compiles, 'ResourceCompile') ET.SubElement(resourcecompile, 'PreprocessorDefinitions') @@ -1359,12 +1559,6 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None additional_links.append(linkname) for lib in self.get_custom_target_provided_libraries(target): additional_links.append(self.relpath(lib, self.get_target_dir(target))) - additional_objects = [] - for o in self.flatten_object_list(target, down)[0]: - assert isinstance(o, str) - additional_objects.append(o) - for o in custom_objs: - additional_objects.append(o) if len(extra_link_args) > 0: extra_link_args.append('%(AdditionalOptions)') @@ -1390,7 +1584,7 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None ET.SubElement(link, 'ModuleDefinitionFile').text = relpath if self.debug: pdb = ET.SubElement(link, 'ProgramDataBaseFileName') - pdb.text = f'$(OutDir){target_name}.pdb' + pdb.text = f'$(OutDir){target.name}.pdb' targetmachine = ET.SubElement(link, 'TargetMachine') if target.for_machine is MachineChoice.BUILD: targetplatform = platform.lower() @@ -1414,6 +1608,116 @@ def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str) -> None if not self.environment.coredata.get_option(OptionKey('debug')): ET.SubElement(link, 'SetChecksum').text = 'true' + # 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 + # a better (simple) alternative, for now, we'll repoint these generated sources (which will be incorrectly + # pointing to non-existent files under our '[builddir]_vs' directory) to the appropriate location under one of + # our buildtype build directores (e.g. '[builddir]_debug'). + # This will at least allow the user to open the files of generated sources listed in the solution explorer, + # once a build/compile has generated these sources. + # + # This modifies the paths in 'gen_files' in place, as opposed to returning a new list of modified paths. + def relocate_generated_file_paths_to_concrete_build_dir(self, gen_files: T.List[str], target: T.Union[build.Target, build.CustomTargetIndex]) -> None: + (_, build_dir_tail) = os.path.split(self.src_to_build) + meson_build_dir_for_buildtype = build_dir_tail[:-2] + coredata.get_genvs_default_buildtype_list()[0] # Get the first buildtype suffixed dir (i.e. '[builddir]_debug') from '[builddir]_vs' + # Relative path from this .vcxproj to the directory containing the set of '..._[debug/debugoptimized/release]' setup meson build dirs. + proj_to_build_root = self.target_to_build_root(target) + proj_to_multiconfigured_builds_parent_dir = os.path.join(proj_to_build_root, '..') + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + relocate_to_concrete_builddir_target = os.path.normpath(os.path.join(proj_to_build_dir_for_buildtype, self.get_target_dir(target))) + for idx, file_path in enumerate(gen_files): + gen_files[idx] = os.path.normpath(os.path.join(relocate_to_concrete_builddir_target, file_path)) + + # Returns bool indicating whether the .vcxproj has been generated. + # Under some circumstances, it's unnecessary to create some .vcxprojs, so, when generating the .sln, + # we need to respect that not all targets will have generated a project. + def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str, captured_compile_args_per_buildtype_and_target: dict = None) -> bool: + mlog.debug(f'Generating vcxproj {target.name}.') + subsystem = 'Windows' + self.handled_target_deps[target.get_id()] = [] + + if self.gen_lite: + if not isinstance(target, build.BuildTarget): + # Since we're going to delegate all building to the one true meson build command, we don't need + # to generate .vcxprojs for targets that don't add any source files or just perform custom build + # commands. These are targets of types CustomTarget or RunTarget. So let's just skip generating + # these otherwise insubstantial non-BuildTarget targets. + return False + conftype = 'Makefile' + elif isinstance(target, build.Executable): + conftype = 'Application' + if target.gui_app is not None: + if not target.gui_app: + subsystem = 'Console' + else: + # If someone knows how to set the version properly, + # please send a patch. + subsystem = target.win_subsystem.split(',')[0] + elif isinstance(target, build.StaticLibrary): + conftype = 'StaticLibrary' + elif isinstance(target, build.SharedLibrary): + conftype = 'DynamicLibrary' + elif isinstance(target, build.CustomTarget): + self.gen_custom_target_vcxproj(target, ofname, guid) + return True + elif isinstance(target, build.RunTarget): + self.gen_run_target_vcxproj(target, ofname, guid) + return True + elif isinstance(target, build.CompileTarget): + self.gen_compile_target_vcxproj(target, ofname, guid) + return True + else: + raise MesonException(f'Unknown target type for {target.get_basename()}') + + (sources, headers, objects, _languages) = self.split_sources(target.sources) + if target.is_unity: + sources = self.generate_unity_files(target, sources) + if target.for_machine is MachineChoice.BUILD: + platform = self.build_platform + else: + platform = self.platform + + tfilename = os.path.splitext(target.get_filename()) + + (root, type_config) = self.create_basic_project(tfilename[0], + temp_dir=target.get_id(), + guid=guid, + conftype=conftype, + target_ext=tfilename[1], + target_platform=platform) + + # vcxproj.filters file + root_filter = self.create_basic_project_filters() + + generated_files, custom_target_output_files, generated_files_include_dirs = self.generate_custom_generator_commands( + target, root) + (gen_src, gen_hdrs, gen_objs, _gen_langs) = self.split_sources(generated_files) + (custom_src, custom_hdrs, custom_objs, _custom_langs) = self.split_sources(custom_target_output_files) + gen_src += custom_src + gen_hdrs += custom_hdrs + + compiler = self._get_cl_compiler(target) + build_args = Vs2010Backend.get_build_args(compiler, self.buildtype, self.optimization, self.debug, self.sanitize) + + assert isinstance(target, (build.Executable, build.SharedLibrary, build.StaticLibrary, build.SharedModule)), 'for mypy' + # Prefix to use to access the build root from the vcxproj dir + proj_to_build_root = self.target_to_build_root(target) + # Prefix to use to access the source tree's root from the vcxproj dir + proj_to_src_root = os.path.join(proj_to_build_root, self.build_to_src) + # Prefix to use to access the source tree's subdir from the vcxproj dir + proj_to_src_dir = os.path.join(proj_to_src_root, self.get_target_dir(target)) + + (target_args, file_args), (target_defines, file_defines), (target_inc_dirs, file_inc_dirs) = self.get_args_defines_and_inc_dirs( + target, compiler, generated_files_include_dirs, proj_to_src_root, proj_to_src_dir, build_args) + + if self.gen_lite: + assert captured_compile_args_per_buildtype_and_target is not None + primary_src_lang = get_primary_source_lang(target.sources, custom_src) + self.add_gen_lite_makefile_vcxproj_elements(root, platform, tfilename[1], captured_compile_args_per_buildtype_and_target, target, proj_to_build_root, primary_src_lang) + else: + self.add_non_makefile_vcxproj_elements(root, type_config, target, platform, subsystem, build_args, target_args, target_defines, target_inc_dirs, file_args) + meson_file_group = ET.SubElement(root, 'ItemGroup') ET.SubElement(meson_file_group, 'None', Include=os.path.join(proj_to_src_dir, build_filename)) @@ -1427,16 +1731,41 @@ def path_normalize_add(path, lis): else: return False + pch_sources = {} + if self.environment.coredata.options.get(OptionKey('b_pch')): + for lang in ['c', 'cpp']: + pch = target.get_pch(lang) + if not pch: + continue + if compiler.id == 'msvc': + if len(pch) == 1: + # Auto generate PCH. + src = os.path.join(proj_to_build_root, self.create_msvc_pch_implementation(target, lang, pch[0])) + pch_header_dir = os.path.dirname(os.path.join(proj_to_src_dir, pch[0])) + else: + src = os.path.join(proj_to_src_dir, pch[1]) + pch_header_dir = None + pch_sources[lang] = [pch[0], src, lang, pch_header_dir] + else: + # I don't know whether its relevant but let's handle other compilers + # used with a vs backend + pch_sources[lang] = [pch[0], None, lang, None] + list_filters_path = set() previous_includes = [] if len(headers) + len(gen_hdrs) + len(target.extra_files) + len(pch_sources) > 0: + if self.gen_lite and gen_hdrs: + # Although we're constructing our .vcxproj under our '..._vs' directory, we want to reference generated files + # in our concrete build directories (e.g. '..._debug'), where generated files will exist after building. + self.relocate_generated_file_paths_to_concrete_build_dir(gen_hdrs, target) + # Filter information filter_group_include = ET.SubElement(root_filter, 'ItemGroup') inc_hdrs = ET.SubElement(root, 'ItemGroup') for h in headers: - relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, h.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_includes): self.add_filter_info(list_filters_path, filter_group_include, 'ClInclude', relpath, h.subdir) ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath) @@ -1445,7 +1774,7 @@ def path_normalize_add(path, lis): self.add_filter_info(list_filters_path, filter_group_include, 'ClInclude', h) ET.SubElement(inc_hdrs, 'CLInclude', Include=h) for h in target.extra_files: - relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, h.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_includes): self.add_filter_info(list_filters_path, filter_group_include, 'ClInclude', relpath, h.subdir) ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath) @@ -1457,50 +1786,70 @@ def path_normalize_add(path, lis): previous_sources = [] if len(sources) + len(gen_src) + len(pch_sources) > 0: + if self.gen_lite: + # Get data to fill in intellisense fields for sources that can't reference the project-wide values + defs_paths_opts_per_lang_and_buildtype = get_non_primary_lang_intellisense_fields( + captured_compile_args_per_buildtype_and_target, + target.get_id(), + primary_src_lang) + if gen_src: + # Although we're constructing our .vcxproj under our '..._vs' directory, we want to reference generated files + # in our concrete build directories (e.g. '..._debug'), where generated files will exist after building. + self.relocate_generated_file_paths_to_concrete_build_dir(gen_src, target) + # Filter information filter_group_compile = ET.SubElement(root_filter, 'ItemGroup') inc_src = ET.SubElement(root, 'ItemGroup') for s in sources: - relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, s.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_sources): self.add_filter_info(list_filters_path, filter_group_compile, 'CLCompile', relpath, s.subdir) inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=relpath) - lang = Vs2010Backend.lang_from_source_file(s) - self.add_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - self.add_include_dirs(lang, inc_cl, file_inc_dirs) - ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ - self.object_filename_from_source(target, s) + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, relpath, defs_paths_opts_per_lang_and_buildtype, platform) + else: + lang = Vs2010Backend.lang_from_source_file(s) + self.add_pch(pch_sources, lang, inc_cl) + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + self.add_include_dirs(lang, inc_cl, file_inc_dirs) + ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ + self.object_filename_from_source(target, s) for s in gen_src: if path_normalize_add(s, previous_sources): self.add_filter_info(list_filters_path, filter_group_compile, 'CLCompile', s) inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=s) - lang = Vs2010Backend.lang_from_source_file(s) - self.add_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - self.add_include_dirs(lang, inc_cl, file_inc_dirs) - s = File.from_built_file(target.get_subdir(), s) - ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ - self.object_filename_from_source(target, s) + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, s, defs_paths_opts_per_lang_and_buildtype, platform) + else: + lang = Vs2010Backend.lang_from_source_file(s) + self.add_pch(pch_sources, lang, inc_cl) + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + self.add_include_dirs(lang, inc_cl, file_inc_dirs) + s = File.from_built_file(target.get_subdir(), s) + ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ + self.object_filename_from_source(target, s) for lang, headers in pch_sources.items(): impl = headers[1] if impl and path_normalize_add(impl, previous_sources): self.add_filter_info(list_filters_path, filter_group_compile, 'CLCompile', impl, 'pch') inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=impl) self.create_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - pch_header_dir = pch_sources[lang][3] - if pch_header_dir: - inc_dirs = copy.deepcopy(file_inc_dirs) - inc_dirs[lang] = [pch_header_dir] + inc_dirs[lang] + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, impl, defs_paths_opts_per_lang_and_buildtype, platform) else: - inc_dirs = file_inc_dirs - self.add_include_dirs(lang, inc_cl, inc_dirs) - # XXX: Do we need to set the object file name here too? + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + pch_header_dir = pch_sources[lang][3] + if pch_header_dir: + inc_dirs = copy.deepcopy(file_inc_dirs) + inc_dirs[lang] = [pch_header_dir] + inc_dirs[lang] + else: + inc_dirs = file_inc_dirs + self.add_include_dirs(lang, inc_cl, inc_dirs) + # XXX: Do we need to set the object file name here too? # Filter information filter_group = ET.SubElement(root_filter, 'ItemGroup') @@ -1508,11 +1857,18 @@ def path_normalize_add(path, lis): filter = ET.SubElement(filter_group, 'Filter', Include=filter_dir) ET.SubElement(filter, 'UniqueIdentifier').text = '{' + str(uuid.uuid4()) + '}' + additional_objects = [] + for o in self.flatten_object_list(target, proj_to_build_root)[0]: + assert isinstance(o, str) + additional_objects.append(o) + for o in custom_objs: + additional_objects.append(o) + previous_objects = [] if self.has_objects(objects, additional_objects, gen_objs): inc_objs = ET.SubElement(root, 'ItemGroup') for s in objects: - relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, s.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_objects): ET.SubElement(inc_objs, 'Object', Include=relpath) for s in additional_objects: @@ -1522,80 +1878,191 @@ def path_normalize_add(path, lis): ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) - self.add_target_deps(root, target) + if not self.gen_lite: + # Injecting further target dependencies into this vcxproj implies and forces a Visual Studio BUILD dependency, + # which we don't want when using 'genvslite'. A gen_lite build as little involvement with the visual studio's + # build system as possible. + self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) self._prettyprint_vcxproj_xml(ET.ElementTree(root_filter), ofname + '.filters') + return True + + def gen_regenproj(self): + # To fully adapt the REGEN work for a 'genvslite' solution, to check timestamps, settings, and regenerate the + # '[builddir]_vs' solution/vcxprojs, as well as regenerating the accompanying buildtype-suffixed ninja build + # directories (from which we need to first collect correct, updated preprocessor defs and compiler options in + # order to fill in the regenerated solution's intellisense settings) would require some non-trivial intrusion + # into the 'meson --internal regencheck ./meson-private' execution path (and perhaps also the '--internal + # regenerate' and even 'meson setup --reconfigure' code). So, for now, we'll instead give the user a simpler + # 'reconfigure' utility project that just runs 'meson setup --reconfigure [builddir]_[buildtype] [srcdir]' on + # each of the ninja build dirs. + # + # FIXME: That will keep the building and compiling correctly configured but obviously won't update the + # solution and vcxprojs, which may allow solution src files and intellisense options to go out-of-date; the + # user would still have to manually 'meson setup --genvslite [vsxxxx] [builddir] [srcdir]' to fully regenerate + # a complete and correct solution. + if self.gen_lite: + project_name = 'RECONFIGURE' + ofname = os.path.join(self.environment.get_build_dir(), 'RECONFIGURE.vcxproj') + conftype = 'Makefile' + # I find the REGEN project doesn't work; it fails to invoke the appropriate - + # python meson.py --internal regencheck builddir\meson-private + # command, despite the fact that manually running such a command in a shell runs just fine. + # Running/building the regen project produces the error - + # ...Microsoft.CppBuild.targets(460,5): error MSB8020: The build tools for ClangCL (Platform Toolset = 'ClangCL') cannot be found. To build using the ClangCL build tools, please install ... + # Not sure why but a simple makefile-style project that executes the full '...regencheck...' command actually works (and seems a little simpler). + # Although I've limited this change to only happen under '--genvslite', perhaps ... + # FIXME : Should all utility projects use the simpler and less problematic makefile-style project? + else: + project_name = 'REGEN' + ofname = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') + conftype = 'Utility' - def gen_regenproj(self, project_name, ofname): guid = self.environment.coredata.regen_guid (root, type_config) = self.create_basic_project(project_name, temp_dir='regen-temp', - guid=guid) - - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - regen_command = self.environment.get_build_command() + ['--internal', 'regencheck'] - cmd_templ = '''call %s > NUL + guid=guid, + conftype=conftype + ) + + if self.gen_lite: + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + all_configs_prop_group = ET.SubElement(root, 'PropertyGroup') + + # Multi-line command to reconfigure all buildtype-suffixed build dirs + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this RECONFIGURE.vcxproj will always be in the '[buildir]_vs' dir. + proj_to_src_dir = self.build_to_src + reconfigure_all_cmd = '' + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + reconfigure_all_cmd += f'{nmake_base_meson_command} setup --reconfigure "{proj_to_build_dir_for_buildtype}" "{proj_to_src_dir}"\n' + ET.SubElement(all_configs_prop_group, 'NMakeBuildCommandLine').text = reconfigure_all_cmd + ET.SubElement(all_configs_prop_group, 'NMakeReBuildCommandLine').text = reconfigure_all_cmd + ET.SubElement(all_configs_prop_group, 'NMakeCleanCommandLine').text = '' + + #Need to set the 'ExecutablePath' element for the above NMake... commands to be able to execute + ET.SubElement(all_configs_prop_group, 'ExecutablePath').text = exe_search_paths + else: + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + regen_command = self.environment.get_build_command() + ['--internal', 'regencheck'] + cmd_templ = '''call %s > NUL "%s" "%s"''' - regen_command = cmd_templ % \ - (self.get_vcvars_command(), '" "'.join(regen_command), self.environment.get_scratch_dir()) - self.add_custom_build(root, 'regen', regen_command, deps=self.get_regen_filelist(), - outputs=[Vs2010Backend.get_regen_stampfile(self.environment.get_build_dir())], - msg='Checking whether solution needs to be regenerated.') + regen_command = cmd_templ % \ + (self.get_vcvars_command(), '" "'.join(regen_command), self.environment.get_scratch_dir()) + self.add_custom_build(root, 'regen', regen_command, deps=self.get_regen_filelist(), + outputs=[Vs2010Backend.get_regen_stampfile(self.environment.get_build_dir())], + msg='Checking whether solution needs to be regenerated.') + ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') ET.SubElement(root, 'ImportGroup', Label='ExtensionTargets') self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_testproj(self, target_name, ofname): + def gen_testproj(self): + project_name = 'RUN_TESTS' + ofname = os.path.join(self.environment.get_build_dir(), f'{project_name}.vcxproj') guid = self.environment.coredata.test_guid - (root, type_config) = self.create_basic_project(target_name, - temp_dir='test-temp', - guid=guid) + if self.gen_lite: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid, + conftype='Makefile' + ) + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this .vcxproj will always be in the '[buildir]_vs' dir. + # Add appropriate 'test' commands for the 'build' action of this project, for all buildtypes + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + test_cmd = f'{nmake_base_meson_command} test -C "{proj_to_build_dir_for_buildtype}" --no-rebuild' + if not self.environment.coredata.get_option(OptionKey('stdsplit')): + test_cmd += ' --no-stdsplit' + if self.environment.coredata.get_option(OptionKey('errorlogs')): + test_cmd += ' --print-errorlogs' + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{self.platform}\'' + prop_group = ET.SubElement(root, 'PropertyGroup', Condition=condition) + ET.SubElement(prop_group, 'NMakeBuildCommandLine').text = test_cmd + #Need to set the 'ExecutablePath' element for the NMake... commands to be able to execute + ET.SubElement(prop_group, 'ExecutablePath').text = exe_search_paths + else: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='test-temp', + guid=guid) + + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + # FIXME: No benchmarks? + test_command = self.environment.get_build_command() + ['test', '--no-rebuild'] + if not self.environment.coredata.get_option(OptionKey('stdsplit')): + test_command += ['--no-stdsplit'] + if self.environment.coredata.get_option(OptionKey('errorlogs')): + test_command += ['--print-errorlogs'] + self.serialize_tests() + self.add_custom_build(root, 'run_tests', '"%s"' % ('" "'.join(test_command))) - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - # FIXME: No benchmarks? - test_command = self.environment.get_build_command() + ['test', '--no-rebuild'] - if not self.environment.coredata.get_option(OptionKey('stdsplit')): - test_command += ['--no-stdsplit'] - if self.environment.coredata.get_option(OptionKey('errorlogs')): - test_command += ['--print-errorlogs'] - self.serialize_tests() - self.add_custom_build(root, 'run_tests', '"%s"' % ('" "'.join(test_command))) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_installproj(self, target_name, ofname): - self.create_install_data_files() - + def gen_installproj(self): + project_name = 'RUN_INSTALL' + ofname = os.path.join(self.environment.get_build_dir(), f'{project_name}.vcxproj') guid = self.environment.coredata.install_guid - (root, type_config) = self.create_basic_project(target_name, - temp_dir='install-temp', - guid=guid) + if self.gen_lite: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid, + conftype='Makefile' + ) + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this .vcxproj will always be in the '[buildir]_vs' dir. + # Add appropriate 'install' commands for the 'build' action of this project, for all buildtypes + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + install_cmd = f'{nmake_base_meson_command} install -C "{proj_to_build_dir_for_buildtype}" --no-rebuild' + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{self.platform}\'' + prop_group = ET.SubElement(root, 'PropertyGroup', Condition=condition) + ET.SubElement(prop_group, 'NMakeBuildCommandLine').text = install_cmd + #Need to set the 'ExecutablePath' element for the NMake... commands to be able to execute + ET.SubElement(prop_group, 'ExecutablePath').text = exe_search_paths + else: + self.create_install_data_files() + + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid) + + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + install_command = self.environment.get_build_command() + ['install', '--no-rebuild'] + self.add_custom_build(root, 'run_install', '"%s"' % ('" "'.join(install_command))) - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - install_command = self.environment.get_build_command() + ['install', '--no-rebuild'] - self.add_custom_build(root, 'run_install', '"%s"' % ('" "'.join(install_command))) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) @@ -1643,8 +2110,11 @@ def generate_debug_information(self, link: ET.Element) -> None: ET.SubElement(link, 'GenerateDebugInformation').text = 'true' def add_regen_dependency(self, root: ET.Element) -> None: - regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') - self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid) + # For now, with 'genvslite' solutions, REGEN is replaced by the lighter-weight RECONFIGURE utility that is + # no longer a forced build dependency. See comment in 'gen_regenproj' + if not self.gen_lite: + regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') + 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: pass diff --git a/mesonbuild/backend/vs2022backend.py b/mesonbuild/backend/vs2022backend.py index ca35ac39df17..ea715d87d8e8 100644 --- a/mesonbuild/backend/vs2022backend.py +++ b/mesonbuild/backend/vs2022backend.py @@ -28,8 +28,8 @@ class Vs2022Backend(Vs2010Backend): name = 'vs2022' - def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): - super().__init__(build, interpreter) + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter], gen_lite: bool = False): + super().__init__(build, interpreter, gen_lite=gen_lite) self.sln_file_version = '12.00' self.sln_version_comment = 'Version 17' if self.environment is not None: diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py index 9c88eecb4b38..f8a5c2370634 100644 --- a/mesonbuild/backend/xcodebackend.py +++ b/mesonbuild/backend/xcodebackend.py @@ -21,7 +21,7 @@ from .. import dependencies from .. import mesonlib from .. import mlog -from ..mesonlib import MesonException, OptionKey +from ..mesonlib import MesonBugException, MesonException, OptionKey if T.TYPE_CHECKING: from ..interpreter import Interpreter @@ -254,7 +254,14 @@ def object_filename_from_source(self, target, source): obj_path = f'{project}.build/{buildtype}/{tname}.build/Objects-normal/{arch}/{stem}.o' return obj_path - def generate(self): + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect the xcode backend to generate with \'capture = True\'') + if captured_compile_args_per_buildtype_and_target: + raise MesonBugException('We do not expect the xcode backend to be given a valid \'captured_compile_args_per_buildtype_and_target\'') self.serialize_tests() # Cache the result as the method rebuilds the array every time it is called. self.build_targets = self.build.get_build_targets() diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index b8f51322b8e4..dda0f38f212d 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -134,11 +134,14 @@ def is_header(fname: 'mesonlib.FileOrString') -> bool: suffix = fname.split('.')[-1] return suffix in header_suffixes +def is_source_suffix(suffix: str) -> bool: + return suffix in source_suffixes + def is_source(fname: 'mesonlib.FileOrString') -> bool: if isinstance(fname, mesonlib.File): fname = fname.fname suffix = fname.split('.')[-1].lower() - return suffix in source_suffixes + return is_source_suffix(suffix) def is_assembly(fname: 'mesonlib.FileOrString') -> bool: if isinstance(fname, mesonlib.File): diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 27b1b91e4402..50763a313379 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -72,6 +72,8 @@ stable_version = '.'.join(stable_version_array) backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode', 'none'] +genvslitelist = ['vs2022'] +buildtypelist = ['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'] DEFAULT_YIELDING = False @@ -79,6 +81,10 @@ _T = T.TypeVar('_T') +def get_genvs_default_buildtype_list() -> list: + return buildtypelist[1:-2] # just debug, debugoptimized, and release for now but this should probably be configurable through some extra option, alongside --genvslite. + + class MesonVersionMismatchException(MesonException): '''Build directory generated with Meson version is incompatible with current version''' def __init__(self, old_version: str, current_version: str) -> None: @@ -1248,8 +1254,17 @@ def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffi (OptionKey('auto_features'), BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')), (OptionKey('backend'), BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist, readonly=True)), + (OptionKey('genvslite'), + BuiltinOption( + UserComboOption, + 'Setup multiple buildtype-suffixed ninja-backend build directories (e.g. builddir_[debug/release/etc.]) ' + 'and generate [builddir]_vs containing a Visual Studio solution with multiple configurations that invoke a meson compile of the newly ' + 'setup build directories, as appropriate for the current build configuration (buildtype)', + 'vs2022', + choices=genvslitelist) + ), (OptionKey('buildtype'), BuiltinOption(UserComboOption, 'Build type to use', 'debug', - choices=['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'])), + choices=buildtypelist)), (OptionKey('debug'), BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)), (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'], yielding=False)), diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 0526f9f0103f..f6133b73c151 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1128,23 +1128,30 @@ def set_backend(self) -> None: # The backend is already set when parsing subprojects if self.backend is not None: return - backend = self.coredata.get_option(OptionKey('backend')) from ..backend import backends - self.backend = backends.get_backend_from_name(backend, self.build, self) + + if OptionKey('genvslite') in self.user_defined_options.cmd_line_options.keys(): + # Use of the '--genvslite vsxxxx' option ultimately overrides any '--backend xxx' + # option the user may specify. + backend_name = self.coredata.get_option(OptionKey('genvslite')) + self.backend = backends.get_genvslite_backend(backend_name, self.build, self) + else: + backend_name = self.coredata.get_option(OptionKey('backend')) + self.backend = backends.get_backend_from_name(backend_name, self.build, self) if self.backend is None: - raise InterpreterException(f'Unknown backend "{backend}".') - if backend != self.backend.name: + raise InterpreterException(f'Unknown backend "{backend_name}".') + if backend_name != self.backend.name: if self.backend.name.startswith('vs'): mlog.log('Auto detected Visual Studio backend:', mlog.bold(self.backend.name)) if not self.environment.first_invocation: - raise MesonBugException(f'Backend changed from {backend} to {self.backend.name}') + raise MesonBugException(f'Backend changed from {backend_name} to {self.backend.name}') self.coredata.set_option(OptionKey('backend'), self.backend.name, first_invocation=True) # Only init backend options on first invocation otherwise it would # override values previously set from command line. if self.environment.first_invocation: - self.coredata.init_backend_options(backend) + self.coredata.init_backend_options(backend_name) options = {k: v for k, v in self.environment.options.items() if k.is_backend()} self.coredata.set_options(options) diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index e7bf3c2a1673..dc6d97ede0d8 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -173,15 +173,21 @@ def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) -> raise MesonException(f'Directory is not empty and does not contain a previous build:\n{build_dir}') return src_dir, build_dir - def generate(self) -> None: + # See class Backend's 'generate' for comments on capture args and returned dictionary. + def generate(self, + capture: bool = False, + captured_compile_args_per_buildtype_and_target: dict = None) -> T.Optional[dict]: env = environment.Environment(self.source_dir, self.build_dir, self.options) mlog.initialize(env.get_log_dir(), self.options.fatal_warnings) if self.options.profile: mlog.set_timestamp_start(time.monotonic()) with mesonlib.BuildDirLock(self.build_dir): - self._generate(env) + return self._generate(env, capture = capture, captured_compile_args_per_buildtype_and_target = captured_compile_args_per_buildtype_and_target) - def _generate(self, env: environment.Environment) -> None: + def _generate(self, + env: environment.Environment, + capture: bool, + captured_compile_args_per_buildtype_and_target: dict) -> T.Optional[dict]: # Get all user defined options, including options that have been defined # during a previous invocation or using meson configure. user_defined_options = argparse.Namespace(**vars(self.options)) @@ -230,6 +236,7 @@ def _generate(self, env: environment.Environment) -> None: raise cdf: T.Optional[str] = None + captured_compile_args = None try: dumpfile = os.path.join(env.get_scratch_dir(), 'build.dat') # We would like to write coredata as late as possible since we use the existence of @@ -246,7 +253,10 @@ def _generate(self, env: environment.Environment) -> None: fname = os.path.join(self.build_dir, 'meson-logs', fname) profile.runctx('intr.backend.generate()', globals(), locals(), filename=fname) else: - intr.backend.generate() + captured_compile_args = intr.backend.generate( + capture = capture, + captured_compile_args_per_buildtype_and_target = captured_compile_args_per_buildtype_and_target) + build.save(b, dumpfile) if env.first_invocation: # Use path resolved by coredata because they could have been @@ -298,17 +308,56 @@ def _generate(self, env: environment.Environment) -> None: os.unlink(cdf) raise + return captured_compile_args + def finalize_postconf_hooks(self, b: build.Build, intr: interpreter.Interpreter) -> None: b.devenv.append(intr.backend.get_devenv()) for mod in intr.modules.values(): mod.postconf_hook(b) +def run_genvslite_setup(options: argparse.Namespace) -> None: + # With --genvslite, we essentially want to invoke multiple 'setup' iterations. I.e. - + # meson setup ... builddirprefix_debug + # meson setup ... builddirprefix_debugoptimized + # meson setup ... builddirprefix_release + # along with also setting up a new, thin/lite visual studio solution and projects with the multiple debug/opt/release configurations that + # invoke the appropriate 'meson compile ...' build commands upon the normal visual studio build/rebuild/clean actions, instead of using + # the native VS/msbuild system. + builddir_prefix = options.builddir + genvsliteval = options.cmd_line_options.pop(mesonlib.OptionKey('genvslite')) + # The command line may specify a '--backend' option, which doesn't make sense in conjunction with + # '--genvslite', where we always want to use a ninja back end - + k_backend = mesonlib.OptionKey('backend') + if k_backend in options.cmd_line_options.keys(): + if options.cmd_line_options[k_backend] != 'ninja': + raise MesonException('Explicitly specifying a backend option with \'genvslite\' is not necessary (the ninja backend is always used) but specifying a non-ninja backend conflicts with a \'genvslite\' setup') + else: + options.cmd_line_options[k_backend] = 'ninja' + buildtypes_list = coredata.get_genvs_default_buildtype_list() + captured_compile_args_per_buildtype_and_target = {} + + for buildtypestr in buildtypes_list: + options.builddir = f'{builddir_prefix}_{buildtypestr}' # E.g. builddir_release + options.cmd_line_options[mesonlib.OptionKey('buildtype')] = buildtypestr + app = MesonApp(options) + captured_compile_args_per_buildtype_and_target[buildtypestr] = app.generate(capture = True) + #Now for generating the 'lite' solution and project files, which will use these builds we've just set up, above. + options.builddir = f'{builddir_prefix}_vs' + options.cmd_line_options[mesonlib.OptionKey('genvslite')] = genvsliteval + app = MesonApp(options) + app.generate(capture = False, captured_compile_args_per_buildtype_and_target = captured_compile_args_per_buildtype_and_target) + def run(options: T.Union[argparse.Namespace, T.List[str]]) -> int: if not isinstance(options, argparse.Namespace): parser = argparse.ArgumentParser() add_arguments(parser) options = parser.parse_args(options) coredata.parse_cmd_line_options(options) - app = MesonApp(options) - app.generate() + + if mesonlib.OptionKey('genvslite') in options.cmd_line_options.keys(): + run_genvslite_setup(options) + else: + app = MesonApp(options) + app.generate() + return 0 diff --git a/test cases/unit/114 genvslite/main.cpp b/test cases/unit/114 genvslite/main.cpp new file mode 100644 index 000000000000..ca250bdd604d --- /dev/null +++ b/test cases/unit/114 genvslite/main.cpp @@ -0,0 +1,10 @@ +#include + +int main() { +#ifdef NDEBUG + printf("Non-debug\n"); +#else + printf("Debug\n"); +#endif + return 0; +} diff --git a/test cases/unit/114 genvslite/meson.build b/test cases/unit/114 genvslite/meson.build new file mode 100644 index 000000000000..3445d7f33aa4 --- /dev/null +++ b/test cases/unit/114 genvslite/meson.build @@ -0,0 +1,5 @@ +project('genvslite', 'cpp', + default_options : ['b_ndebug=if-release'] + ) + +exe = executable('genvslite', 'main.cpp') diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py index ea95b15ac50a..4b16e7d8253b 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -207,7 +207,8 @@ def init(self, srcdir, *, extra_args = [] if not isinstance(extra_args, list): extra_args = [extra_args] - args = [srcdir, self.builddir] + build_and_src_dir_args = [self.builddir, srcdir] + args = [] if default_args: args += ['--prefix', self.prefix] if self.libdir: @@ -219,7 +220,7 @@ def init(self, srcdir, *, self.privatedir = os.path.join(self.builddir, 'meson-private') if inprocess: try: - returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args, override_envvars) + returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args + build_and_src_dir_args, override_envvars) except Exception as e: if not allow_fail: self._print_meson_log() @@ -245,7 +246,7 @@ def init(self, srcdir, *, raise RuntimeError('Configure failed') else: try: - out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir) + out = self._run(self.setup_command + args + extra_args + build_and_src_dir_args, override_envvars=override_envvars, workdir=workdir) except SkipTest: raise SkipTest('Project requested skipping: ' + srcdir) except Exception: diff --git a/unittests/windowstests.py b/unittests/windowstests.py index c81d924e8b37..36a1f3f105fa 100644 --- a/unittests/windowstests.py +++ b/unittests/windowstests.py @@ -184,6 +184,93 @@ def test_msvc_cpp17(self): # to the right reason). return self.build() + + @skipIf(is_cygwin(), 'Test only applicable to Windows') + def test_genvslite(self): + # The test framework itself might be forcing a specific, non-ninja backend across a set of tests, which + # includes this test. E.g. - + # > python.exe run_unittests.py --backend=vs WindowsTests + # Since that explicitly specifies a backend that's incompatible with (and essentially meaningless in + # conjunction with) 'genvslite', we should skip further genvslite testing. + if self.backend is not Backend.ninja: + raise SkipTest('Test only applies when using the Ninja backend') + + testdir = os.path.join(self.unit_test_dir, '114 genvslite') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Test only applies when MSVC tools are available.') + + # We want to run the genvslite setup. I.e. - + # meson setup --genvslite vs2022 ... + # which we should expect to generate the set of _debug/_debugoptimized/_release suffixed + # build directories. Then we want to check that the solution/project build hooks (like clean, + # build, and rebuild) end up ultimately invoking the 'meson compile ...' of the appropriately + # suffixed build dir, for which we need to use 'msbuild.exe' + + # Find 'msbuild.exe' + msbuildprog = ExternalProgram('msbuild.exe') + self.assertTrue(msbuildprog.found(), msg='msbuild.exe not found') + + # Setup with '--genvslite ...' + self.new_builddir() + + # Firstly, we'd like to check that meson errors if the user explicitly specifies a non-ninja backend + # during setup. + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.init(testdir, extra_args=['--genvslite', 'vs2022', '--backend', 'vs']) + self.assertIn("specifying a non-ninja backend conflicts with a 'genvslite' setup", cm.exception.stdout) + + # Wrap the following bulk of setup and msbuild invocation testing in a try-finally because any exception, + # failure, or success must always clean up any of the suffixed build dir folders that may have been generated. + try: + # Since this + self.init(testdir, extra_args=['--genvslite', 'vs2022']) + # We need to bear in mind that the BasePlatformTests framework creates and cleans up its own temporary + # build directory. However, 'genvslite' creates a set of suffixed build directories which we'll have + # to clean up ourselves. See 'finally' block below. + + # We intentionally skip the - + # self.build() + # step because we're wanting to test compilation/building through the solution/project's interface. + + # Execute the debug and release builds through the projects 'Build' hooks + genvslite_vcxproj_path = str(os.path.join(self.builddir+'_vs', 'genvslite@exe.vcxproj')) + # This use-case of invoking the .sln/.vcxproj build hooks, not through Visual Studio itself, but through + # 'msbuild.exe', in a VS tools command prompt environment (e.g. "x64 Native Tools Command Prompt for VS 2022"), is a + # problem: Such an environment sets the 'VSINSTALLDIR' variable which, mysteriously, has the side-effect of causing + # the spawned 'meson compile' command to fail to find 'ninja' (and even when ninja can be found elsewhere, all the + # compiler binaries that ninja wants to run also fail to be found). The PATH environment variable in the child python + # (and ninja) processes are fundamentally stripped down of all the critical search paths required to run the ninja + # compile work ... ONLY when 'VSINSTALLDIR' is set; without 'VSINSTALLDIR' set, the meson compile command does search + # for and find ninja (ironically, it finds it under the path where VSINSTALLDIR pointed!). + # For the above reason, this testing works around this bizarre behaviour by temporarily removing any 'VSINSTALLDIR' + # variable, prior to invoking the builds - + current_env = os.environ.copy() + current_env.pop('VSINSTALLDIR', None) + subprocess.check_call( + ['msbuild', '-target:Build', '-property:Configuration=debug', genvslite_vcxproj_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=current_env) + subprocess.check_call( + ['msbuild', '-target:Build', '-property:Configuration=release', genvslite_vcxproj_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=current_env) + + # Check this has actually built the appropriate exes + output_debug = subprocess.check_output(str(os.path.join(self.builddir+'_debug', 'genvslite.exe'))) + self.assertEqual( output_debug, b'Debug\r\n' ) + output_release = subprocess.check_output(str(os.path.join(self.builddir+'_release', 'genvslite.exe'))) + self.assertEqual( output_release, b'Non-debug\r\n' ) + + finally: + # Clean up our special suffixed temporary build dirs + suffixed_build_dirs = glob(self.builddir+'_*', recursive=False) + for build_dir in suffixed_build_dirs: + shutil.rmtree(build_dir) def test_install_pdb_introspection(self): testdir = os.path.join(self.platform_test_dir, '1 basic')