From 36bf53bdfdc617eaf4ca07905335914b5d661769 Mon Sep 17 00:00:00 2001 From: GertyP <18214721+GertyP@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:29:57 +0100 Subject: [PATCH] Experimental 'genvslite' WIP. (#11049) * Capture all compile args from the first round of ninja backend generation for all languages used in building the targets so that these args, defines, and include paths can be applied to the .vcxproj's intellisense fields for all buildtypes/configurations. Solution generation is now set up for mutiple build configurations (buildtypes) when using '--genvslite'. All generated vcxprojs invoke the same high-level meson compile to build all targets; there's no selective target building (could add this later). Related to this, we skip pointlessly generating vcxprojs for targets that aren't buildable (BuildTarget-derived), which aren't of interest to the user anyway. When using --genvslite, no longer inject '' dependencies on which a generated .vcxproj depends because that imposes a forced visual studio build dependency, which we don't want, since we're essentially bypassing VS's build in favour of running 'meson compile ...'. When populating the vcxproj's shared intellisense defines, include paths, and compiler options fields, we choose the most frequent src file language, since this means more project src files can simply reference the project shared fields and fewer files of non-primary language types need to populate their full set of intellisense fields. This makes for smaller .vcxproj files. Paths for generated source/header/etc files, left alone, would be added to solution projects relative to the '..._vs' build directory, where they're never generated; they're generated under the respective '..._[debug/opt/release]' ninja build directories that correspond to the solution build configuration. Although VS doesn't allow conditional src/header listings in vcxprojs (at least not in a simple way that I'm aware of), we can ensure these generated sources get adjusted to at least reference locations under one of the concrete build directories (I've chosen '..._debug') under which they will be generated. Testing with --genvslite has revealed that, in some cases, the presence of 'c:\windows\system32;c:\windows' on the 'Path' environment variable (via the make-style project's ExecutablePath element) is critical to getting the 'meson compile ...' build to succeed. Not sure whether this is some 'find and guess' implicit defaults behaviour within meson or within the MSVC compiler that some projects may rely on. Feels weird but not sure of a better solution than forcibly adding these to the Path environment variable (the Executable Path property of the project). Added a new windows-only test to windowstests.py ('test_genvslite') to exercise the --genvslite option along with checking that the 'msbuild' command invokes the 'meson compile ...' of the build-type-appropriate-suffixed temporary build dir and checks expected program output. Check and report error if user specifies a non-ninja backend with a 'genvslite' setup, since that conflicts with the stated behaviour of genvslite. Also added this test case to 'WindowsTests.test_genvslite' I had problems tracking down some problematic environment variable behaviour, which appears to need a work-around. See further notes on VSINSTALLDIR, in windowstests.py, test_genvslite. 'meson setup --help' clearly states that positional arguments are ... [builddir] [sourcedir]. However, BasePlatformTests.init(...) was passing these in the order [sourcedir] [builddir]. This was producing failures, saying, "ERROR: Neither directory contains a build file meson.build." but when using the correct ordering, setup now succeeds. Changed regen, run_tests, and run_install utility projects to be simpler makefile projects instead, with commands to invoke the appropriate '...meson.py --internal regencheck ...' (or install/test) on the '[builddir]_[buildtype]' as appropriate for the curent VS configuration. Also, since the 'regen.vcxproj' utility didn't work correctly with '--genvslite' setup build dirs, and getting it to fully work would require more non-trivial intrusion into new parts of meson (i.e. '--internal regencheck', '--internal regenerate', and perhaps also 'setup --reconfigure'), for now, the REGEN project is replaced with a simpler, lighter-weight RECONFIGURE utility proj, which is unlinked from any solution build dependencies and which simply runs 'meson setup --reconfigure [builddir]_[buildtype] [srcdir]' on each of the ninja-backend build dirs for each buildtype. Yes, although this will enable the building/compiling to be correctly configured, it can leave the solution/vcxprojs stale and out-of-date, it's simple for the user to 'meson setup --genvslite ...' to fully regenerate an updated, correct solution again. However, I've noted this down as a 'fixme' to consider implementing the full regen behaviour for the genvslite case. * Review feedback changes - - Avoid use of 'captured_compile_args_per_buildtype_and_target' as an 'out' param. - Factored a little msetup.py, 'run(...)' macro/looping setup steps, for genvslite, out into a 'run_genvslite_setup' func. * Review feedback: Fixed missing spaces between multi-line strings. * 'backend_name' assignment gets immediately overwritten in 'genvslite' case so moved it into else/non-genvslite block. * Had to bump up 'test cases/unit/113 genvslites/...' up to 114; it collided with a newly added test dir again. * Changed validation of 'capture' and 'captured_compile_args_...' to use MesonBugException instead of MesonException. * Changed some function param and closing brace indentation. --- docs/markdown/Builtin-options.md | 21 + docs/markdown/snippets/gen_vslite.md | 11 + mesonbuild/backend/backends.py | 19 +- mesonbuild/backend/ninjabackend.py | 49 +- mesonbuild/backend/nonebackend.py | 12 +- mesonbuild/backend/vs2010backend.py | 1128 +++++++++++++++------ mesonbuild/backend/vs2022backend.py | 4 +- mesonbuild/backend/xcodebackend.py | 11 +- mesonbuild/compilers/compilers.py | 5 +- mesonbuild/coredata.py | 17 +- mesonbuild/interpreter/interpreter.py | 19 +- mesonbuild/msetup.py | 61 +- test cases/unit/114 genvslite/main.cpp | 10 + test cases/unit/114 genvslite/meson.build | 5 + unittests/baseplatformtests.py | 7 +- unittests/windowstests.py | 87 ++ 16 files changed, 1112 insertions(+), 354 deletions(-) create mode 100644 docs/markdown/snippets/gen_vslite.md create mode 100644 test cases/unit/114 genvslite/main.cpp create mode 100644 test cases/unit/114 genvslite/meson.build diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index c714dc707..fed893e10 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 000000000..e647b0429 --- /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 dec1e68cc..9e247b6fc 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 @@ class Backend: 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 ba614fd56..213984765 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -38,7 +38,7 @@ from ..arglist import CompilerArgs 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 @@ class NinjaBackend(backends.Backend): 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 @@ class NinjaBackend(backends.Backend): 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 @@ class NinjaBackend(backends.Backend): 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 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) 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 cf7c3dde9..79ee7b2e1 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 d6b2f44c4..501525a8f 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -22,15 +22,18 @@ import uuid 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 @@ class Vs2010Backend(backends.Backend): 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 ca35ac39d..ea715d87d 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 9c88eecb4..f8a5c2370 100644 --- a/mesonbuild/backend/xcodebackend.py +++ b/mesonbuild/backend/xcodebackend.py @@ -21,7 +21,7 @@ from .. import build 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 @@ class XCodeBackend(backends.Backend): 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 b8f51322b..dda0f38f2 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 27b1b91e4..50763a313 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -72,6 +72,8 @@ if stable_version.endswith('.99'): 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 @@ DEFAULT_YIELDING = False _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 @@ BUILTIN_CORE_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([ (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 0526f9f01..f6133b73c 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1128,23 +1128,30 @@ class Interpreter(InterpreterBase, HoldableObject): # 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 e7bf3c2a1..dc6d97ede 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -173,15 +173,21 @@ class MesonApp: 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 @@ class MesonApp: 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 @@ class MesonApp: 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 @@ class MesonApp: 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 000000000..ca250bdd6 --- /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 000000000..3445d7f33 --- /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 ea95b15ac..4b16e7d82 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -207,7 +207,8 @@ class BasePlatformTests(TestCase): 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 @@ class BasePlatformTests(TestCase): 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 @@ class BasePlatformTests(TestCase): 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 c81d924e8..36a1f3f10 100644 --- a/unittests/windowstests.py +++ b/unittests/windowstests.py @@ -184,6 +184,93 @@ class WindowsTests(BasePlatformTests): # 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')